Repository: cygnusx-1-org/continuum Branch: master Commit: aa44bc460d13 Files: 1434 Total size: 9.7 MB Directory structure: gitextract_8bwcyaie/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── actions/ │ │ └── github-custom-issue-closer │ └── workflows/ │ ├── build.yml │ └── close-issues-custom-pattern.yml ├── .gitignore ├── CHANGELOG.md ├── FAQ.md ├── GIPHY.md ├── LICENSE ├── README.md ├── SETUP-old.md ├── SETUP.md ├── TESTS.md ├── app/ │ ├── Modules/ │ │ └── customtextview-2.1.aar │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── kotlin/ │ │ └── ru/ │ │ └── otus/ │ │ └── pandina/ │ │ ├── screens/ │ │ │ ├── CustomizePostFilterScreen.kt │ │ │ ├── FilteredPostsScreen.kt │ │ │ ├── MainScreen.kt │ │ │ ├── UserAgreementFragment.kt │ │ │ └── navigation/ │ │ │ ├── LoginScreen.kt │ │ │ ├── NavigationViewLayout.kt │ │ │ └── settings/ │ │ │ ├── ActionPanel.kt │ │ │ ├── SettingsScreen.kt │ │ │ ├── ThemeScreen.kt │ │ │ ├── font/ │ │ │ │ ├── FontPreviewScreen.kt │ │ │ │ └── FontScreen.kt │ │ │ ├── interfaceScreen/ │ │ │ │ ├── CustomizeTabsScreen.kt │ │ │ │ └── InterfaceScreen.kt │ │ │ └── notification/ │ │ │ └── NotificationScreen.kt │ │ ├── tests/ │ │ │ ├── APIKeysTest.kt │ │ │ ├── BaseTest.kt │ │ │ ├── LoginTest.kt │ │ │ ├── MainTest.kt │ │ │ └── SettingsTest.kt │ │ └── utils/ │ │ └── NotificationDialogHelper.kt │ ├── debug/ │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── ml/ │ │ │ └── docilealligator/ │ │ │ └── infinityforreddit/ │ │ │ ├── APIResult.kt │ │ │ ├── ActionState.kt │ │ │ ├── AppComponent.java │ │ │ ├── AppModule.java │ │ │ ├── CommentModerationActionHandler.kt │ │ │ ├── Constants.java │ │ │ ├── Converters.kt │ │ │ ├── CustomFontReceiver.java │ │ │ ├── DataLoadState.kt │ │ │ ├── DownloadProgressResponseBody.java │ │ │ ├── FetchPostFilterAndConcatenatedSubredditNames.java │ │ │ ├── FetchVideoLinkListener.java │ │ │ ├── Infinity.java │ │ │ ├── NetworkModule.java │ │ │ ├── NetworkState.java │ │ │ ├── PostModerationActionHandler.kt │ │ │ ├── ProxyEnabledGlideModule.java │ │ │ ├── RecyclerViewContentScrollingInterface.java │ │ │ ├── RedditDataRoomDatabase.java │ │ │ ├── SaveMemoryCenterInisdeDownsampleStrategy.java │ │ │ ├── SetAsWallpaperCallback.java │ │ │ ├── SingleLiveEvent.java │ │ │ ├── VideoLinkFetcher.java │ │ │ ├── WallpaperSetter.java │ │ │ ├── account/ │ │ │ │ ├── Account.java │ │ │ │ ├── AccountDao.java │ │ │ │ ├── AccountDaoKt.kt │ │ │ │ ├── AccountRepository.java │ │ │ │ ├── AccountViewModel.java │ │ │ │ └── FetchMyInfo.java │ │ │ ├── activities/ │ │ │ │ ├── AccountPostsActivity.java │ │ │ │ ├── AccountSavedThingActivity.java │ │ │ │ ├── ActivityToolbarInterface.java │ │ │ │ ├── AppBarStateChangeListener.java │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── CommentActivity.java │ │ │ │ ├── CommentFilterPreferenceActivity.java │ │ │ │ ├── CommentFilterUsageListingActivity.java │ │ │ │ ├── CopyMultiRedditActivity.kt │ │ │ │ ├── CreateMultiRedditActivity.java │ │ │ │ ├── CustomThemeListingActivity.java │ │ │ │ ├── CustomThemePreviewActivity.java │ │ │ │ ├── CustomizeCommentFilterActivity.java │ │ │ │ ├── CustomizePostFilterActivity.java │ │ │ │ ├── CustomizeThemeActivity.java │ │ │ │ ├── EditCommentActivity.java │ │ │ │ ├── EditMultiRedditActivity.java │ │ │ │ ├── EditPostActivity.java │ │ │ │ ├── EditProfileActivity.java │ │ │ │ ├── FilteredPostsActivity.java │ │ │ │ ├── FullMarkdownActivity.java │ │ │ │ ├── HistoryActivity.java │ │ │ │ ├── InboxActivity.java │ │ │ │ ├── LinkResolverActivity.java │ │ │ │ ├── LockScreenActivity.java │ │ │ │ ├── LoginActivity.java │ │ │ │ ├── LoginChromeCustomTabActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── PostFilterPreferenceActivity.java │ │ │ │ ├── PostFilterUsageListingActivity.java │ │ │ │ ├── PostGalleryActivity.java │ │ │ │ ├── PostImageActivity.java │ │ │ │ ├── PostLinkActivity.java │ │ │ │ ├── PostPollActivity.java │ │ │ │ ├── PostTextActivity.java │ │ │ │ ├── PostVideoActivity.java │ │ │ │ ├── QRCodeScannerActivity.java │ │ │ │ ├── ReportActivity.java │ │ │ │ ├── RulesActivity.java │ │ │ │ ├── SearchActivity.java │ │ │ │ ├── SearchHistoryActivity.java │ │ │ │ ├── SearchResultActivity.java │ │ │ │ ├── SearchSubredditsResultActivity.java │ │ │ │ ├── SearchUsersResultActivity.java │ │ │ │ ├── SelectUserFlairActivity.java │ │ │ │ ├── SelectedSubredditsAndUsersActivity.java │ │ │ │ ├── SendPrivateMessageActivity.java │ │ │ │ ├── SettingsActivity.java │ │ │ │ ├── ShareDataResolverActivity.java │ │ │ │ ├── SubmitCrosspostActivity.java │ │ │ │ ├── SubredditMultiselectionActivity.java │ │ │ │ ├── SubscribedThingListingActivity.java │ │ │ │ ├── SuicidePreventionActivity.java │ │ │ │ ├── UploadImageEnabledActivity.java │ │ │ │ ├── UserMultiselectionActivity.java │ │ │ │ ├── ViewImageOrGifActivity.java │ │ │ │ ├── ViewImgurMediaActivity.java │ │ │ │ ├── ViewMultiRedditDetailActivity.java │ │ │ │ ├── ViewPostDetailActivity.java │ │ │ │ ├── ViewPrivateMessagesActivity.java │ │ │ │ ├── ViewRedditGalleryActivity.java │ │ │ │ ├── ViewSubredditDetailActivity.java │ │ │ │ ├── ViewUserDetailActivity.java │ │ │ │ ├── ViewVideoActivity.java │ │ │ │ ├── ViewVideoActivityBindingAdapter.java │ │ │ │ ├── WebViewActivity.java │ │ │ │ └── WikiActivity.java │ │ │ ├── adapters/ │ │ │ │ ├── AccountChooserRecyclerViewAdapter.java │ │ │ │ ├── AcknowledgementRecyclerViewAdapter.java │ │ │ │ ├── CommentFilterUsageEmbeddedRecyclerViewAdapter.java │ │ │ │ ├── CommentFilterUsageRecyclerViewAdapter.java │ │ │ │ ├── CommentFilterWithUsageRecyclerViewAdapter.java │ │ │ │ ├── CommentsListingRecyclerViewAdapter.java │ │ │ │ ├── CommentsRecyclerViewAdapter.java │ │ │ │ ├── CrashReportsRecyclerViewAdapter.java │ │ │ │ ├── CustomThemeListingRecyclerViewAdapter.java │ │ │ │ ├── CustomizeThemeRecyclerViewAdapter.java │ │ │ │ ├── FlairBottomSheetRecyclerViewAdapter.java │ │ │ │ ├── FollowedUsersRecyclerViewAdapter.java │ │ │ │ ├── MarkdownBottomBarRecyclerViewAdapter.java │ │ │ │ ├── MessageRecyclerViewAdapter.java │ │ │ │ ├── MultiRedditListingRecyclerViewAdapter.java │ │ │ │ ├── OnlineCustomThemeListingRecyclerViewAdapter.java │ │ │ │ ├── Paging3LoadingStateAdapter.java │ │ │ │ ├── PostDetailRecyclerViewAdapter.java │ │ │ │ ├── PostFilterUsageRecyclerViewAdapter.java │ │ │ │ ├── PostFilterWithUsageRecyclerViewAdapter.java │ │ │ │ ├── PostGalleryTypeImageRecyclerViewAdapter.java │ │ │ │ ├── PostRecyclerViewAdapter.java │ │ │ │ ├── PrivateMessagesDetailRecyclerViewAdapter.java │ │ │ │ ├── RedditGallerySubmissionRecyclerViewAdapter.java │ │ │ │ ├── ReportReasonRecyclerViewAdapter.java │ │ │ │ ├── RulesRecyclerViewAdapter.java │ │ │ │ ├── SearchActivityRecyclerViewAdapter.java │ │ │ │ ├── SelectedSubredditsRecyclerViewAdapter.java │ │ │ │ ├── SettingsSearchAdapter.java │ │ │ │ ├── SubredditAutocompleteRecyclerViewAdapter.java │ │ │ │ ├── SubredditListingRecyclerViewAdapter.java │ │ │ │ ├── SubredditMultiselectionRecyclerViewAdapter.java │ │ │ │ ├── SubscribedSubredditsRecyclerViewAdapter.java │ │ │ │ ├── TranslationFragmentRecyclerViewAdapter.java │ │ │ │ ├── UploadedImagesRecyclerViewAdapter.java │ │ │ │ ├── UserFlairRecyclerViewAdapter.java │ │ │ │ ├── UserListingRecyclerViewAdapter.java │ │ │ │ ├── UserMultiselectionRecyclerViewAdapter.java │ │ │ │ └── navigationdrawer/ │ │ │ │ ├── AccountManagementSectionRecyclerViewAdapter.java │ │ │ │ ├── AccountSectionRecyclerViewAdapter.java │ │ │ │ ├── FavoriteSubscribedSubredditsSectionRecyclerViewAdapter.java │ │ │ │ ├── HeaderSectionRecyclerViewAdapter.java │ │ │ │ ├── NavigationDrawerRecyclerViewMergedAdapter.java │ │ │ │ ├── PostFilterUsageEmbeddedRecyclerViewAdapter.java │ │ │ │ ├── PostSectionRecyclerViewAdapter.java │ │ │ │ ├── PreferenceSectionRecyclerViewAdapter.java │ │ │ │ ├── RedditSectionRecyclerViewAdapter.java │ │ │ │ └── SubscribedSubredditsRecyclerViewAdapter.java │ │ │ ├── apis/ │ │ │ │ ├── DownloadFile.java │ │ │ │ ├── ImgurAPI.java │ │ │ │ ├── OhMyDlAPI.kt │ │ │ │ ├── PushshiftAPI.java │ │ │ │ ├── RedditAPI.java │ │ │ │ ├── RedditAPIKt.kt │ │ │ │ ├── RedgifsAPI.java │ │ │ │ ├── RevedditAPI.java │ │ │ │ ├── ServerAPI.java │ │ │ │ ├── StreamableAPI.java │ │ │ │ ├── TitleSuggestion.java │ │ │ │ └── VReddIt.java │ │ │ ├── asynctasks/ │ │ │ │ ├── AccountManagement.java │ │ │ │ ├── AddSubredditOrUserToMultiReddit.java │ │ │ │ ├── BackupSettings.java │ │ │ │ ├── ChangeThemeName.java │ │ │ │ ├── CheckIsFollowingUser.java │ │ │ │ ├── CheckIsSubscribedToSubreddit.java │ │ │ │ ├── DeleteAllPostLayouts.java │ │ │ │ ├── DeleteAllReadPosts.java │ │ │ │ ├── DeleteAllSortTypes.java │ │ │ │ ├── DeleteAllSubreddits.java │ │ │ │ ├── DeleteAllThemes.java │ │ │ │ ├── DeleteAllUsers.java │ │ │ │ ├── DeleteMultiredditInDatabase.java │ │ │ │ ├── DeleteTheme.java │ │ │ │ ├── GetCustomTheme.java │ │ │ │ ├── InsertCustomTheme.java │ │ │ │ ├── InsertMultireddit.java │ │ │ │ ├── InsertSubredditData.java │ │ │ │ ├── InsertSubscribedThings.java │ │ │ │ ├── InsertUserData.java │ │ │ │ ├── LoadSubredditIcon.java │ │ │ │ ├── LoadUserData.java │ │ │ │ ├── ParseAndInsertNewAccount.java │ │ │ │ ├── RestoreSettings.java │ │ │ │ ├── SaveBitmapImageToFile.java │ │ │ │ ├── SaveGIFToFile.java │ │ │ │ └── SetAsWallpaper.java │ │ │ ├── bottomsheetfragments/ │ │ │ │ ├── AccountChooserBottomSheetFragment.java │ │ │ │ ├── CommentFilterOptionsBottomSheetFragment.java │ │ │ │ ├── CommentFilterUsageOptionsBottomSheetFragment.java │ │ │ │ ├── CommentModerationActionBottomSheetFragment.kt │ │ │ │ ├── CommentMoreBottomSheetFragment.java │ │ │ │ ├── CopyTextBottomSheetFragment.java │ │ │ │ ├── CreateThemeBottomSheetFragment.java │ │ │ │ ├── CustomThemeOptionsBottomSheetFragment.java │ │ │ │ ├── FABMoreOptionsBottomSheetFragment.java │ │ │ │ ├── FilteredThingFABMoreOptionsBottomSheetFragment.java │ │ │ │ ├── FlairBottomSheetFragment.java │ │ │ │ ├── GiphyGifInfoBottomSheetFragment.java │ │ │ │ ├── ImportantInfoBottomSheetFragment.java │ │ │ │ ├── KarmaInfoBottomSheetFragment.java │ │ │ │ ├── MultiRedditOptionsBottomSheetFragment.java │ │ │ │ ├── NewCommentFilterUsageBottomSheetFragment.java │ │ │ │ ├── NewPostFilterUsageBottomSheetFragment.java │ │ │ │ ├── PlaybackSpeedBottomSheetFragment.java │ │ │ │ ├── PostCommentSortTypeBottomSheetFragment.java │ │ │ │ ├── PostFilterOptionsBottomSheetFragment.java │ │ │ │ ├── PostFilterUsageOptionsBottomSheetFragment.java │ │ │ │ ├── PostLayoutBottomSheetFragment.java │ │ │ │ ├── PostModerationActionBottomSheetFragment.kt │ │ │ │ ├── PostOptionsBottomSheetFragment.java │ │ │ │ ├── PostTypeBottomSheetFragment.java │ │ │ │ ├── SearchPostSortTypeBottomSheetFragment.java │ │ │ │ ├── SearchUserAndSubredditSortTypeBottomSheetFragment.java │ │ │ │ ├── SelectOrCaptureImageBottomSheetFragment.java │ │ │ │ ├── SelectSubredditsOrUsersOptionsBottomSheetFragment.java │ │ │ │ ├── SetAsWallpaperBottomSheetFragment.java │ │ │ │ ├── SetRedditGalleryItemCaptionAndUrlBottomSheetFragment.java │ │ │ │ ├── ShareBottomSheetFragment.java │ │ │ │ ├── SortTimeBottomSheetFragment.java │ │ │ │ ├── SortTypeBottomSheetFragment.java │ │ │ │ ├── UploadedImagesBottomSheetFragment.java │ │ │ │ ├── UrlMenuBottomSheetFragment.java │ │ │ │ └── UserThingSortTypeBottomSheetFragment.java │ │ │ ├── broadcastreceivers/ │ │ │ │ ├── DownloadedMediaDeleteActionBroadcastReceiver.java │ │ │ │ ├── NetworkWifiStatusReceiver.java │ │ │ │ └── WallpaperChangeReceiver.java │ │ │ ├── comment/ │ │ │ │ ├── Comment.java │ │ │ │ ├── CommentDataSource.java │ │ │ │ ├── CommentDataSourceFactory.java │ │ │ │ ├── CommentDraft.kt │ │ │ │ ├── CommentDraftDao.kt │ │ │ │ ├── CommentViewModel.java │ │ │ │ ├── FetchComment.java │ │ │ │ ├── ParseComment.java │ │ │ │ └── SendComment.java │ │ │ ├── commentfilter/ │ │ │ │ ├── CommentFilter.java │ │ │ │ ├── CommentFilterDao.java │ │ │ │ ├── CommentFilterUsage.java │ │ │ │ ├── CommentFilterUsageDao.java │ │ │ │ ├── CommentFilterUsageViewModel.java │ │ │ │ ├── CommentFilterWithUsage.java │ │ │ │ ├── CommentFilterWithUsageViewModel.java │ │ │ │ ├── DeleteCommentFilter.java │ │ │ │ ├── DeleteCommentFilterUsage.java │ │ │ │ ├── FetchCommentFilter.java │ │ │ │ ├── SaveCommentFilter.java │ │ │ │ └── SaveCommentFilterUsage.java │ │ │ ├── customtheme/ │ │ │ │ ├── CustomTheme.java │ │ │ │ ├── CustomThemeDao.java │ │ │ │ ├── CustomThemeDaoKt.kt │ │ │ │ ├── CustomThemeSettingsItem.java │ │ │ │ ├── CustomThemeViewModel.java │ │ │ │ ├── CustomThemeWrapper.java │ │ │ │ ├── CustomThemeWrapperReceiver.java │ │ │ │ ├── LocalCustomThemeRepository.java │ │ │ │ ├── OnlineCustomThemeFilter.java │ │ │ │ ├── OnlineCustomThemeMetadata.java │ │ │ │ ├── OnlineCustomThemePagingSource.java │ │ │ │ └── OnlineCustomThemeRepository.java │ │ │ ├── customviews/ │ │ │ │ ├── AdjustableTouchSlopItemTouchHelper.java │ │ │ │ ├── AspectRatioGifImageView.java │ │ │ │ ├── ClickableMotionLayout.java │ │ │ │ ├── ColorPickerDialog.java │ │ │ │ ├── CommentIndentationView.java │ │ │ │ ├── CustomDrawerLayout.kt │ │ │ │ ├── CustomToroContainer.java │ │ │ │ ├── GlideGifImageViewFactory.java │ │ │ │ ├── InterceptTouchEventLinearLayout.java │ │ │ │ ├── LandscapeExpandedRoundedBottomSheetDialogFragment.java │ │ │ │ ├── LinearLayoutManagerBugFixed.java │ │ │ │ ├── LollipopBugFixedWebView.java │ │ │ │ ├── LoopAvailableExoCreator.java │ │ │ │ ├── MovableFloatingActionButton.java │ │ │ │ ├── NavigationWrapper.java │ │ │ │ ├── SpoilerOnClickTextView.java │ │ │ │ ├── SwipeLockInterface.java │ │ │ │ ├── SwipeLockLinearLayout.java │ │ │ │ ├── SwipeLockLinearLayoutManager.java │ │ │ │ ├── SwipeLockView.java │ │ │ │ ├── TableHorizontalScrollView.java │ │ │ │ ├── ThemedMaterialSwitch.kt │ │ │ │ ├── TouchInterceptableMaterialCardView.kt │ │ │ │ ├── ViewPagerBugFixed.java │ │ │ │ ├── compose/ │ │ │ │ │ ├── AppTheme.kt │ │ │ │ │ ├── CustomAppBar.kt │ │ │ │ │ ├── CustomImage.kt │ │ │ │ │ ├── CustomLoadingIndicator.kt │ │ │ │ │ ├── CustomSwitch.kt │ │ │ │ │ ├── CustomText.kt │ │ │ │ │ └── CustomTextField.kt │ │ │ │ ├── preference/ │ │ │ │ │ ├── CustomFontEditTextPreference.java │ │ │ │ │ ├── CustomFontListPreference.java │ │ │ │ │ ├── CustomFontPreference.java │ │ │ │ │ ├── CustomFontPreferenceCategory.java │ │ │ │ │ ├── CustomFontPreferenceFragmentCompat.java │ │ │ │ │ ├── CustomFontPreferenceWithBackground.kt │ │ │ │ │ ├── CustomFontSwitchPreference.java │ │ │ │ │ ├── CustomStyleEditTextPreferenceDialogFragmentCompat.kt │ │ │ │ │ ├── CustomStyleListPreferenceDialogFragmentCompat.kt │ │ │ │ │ └── SliderPreference.kt │ │ │ │ └── slidr/ │ │ │ │ ├── ColorPanelSlideListener.java │ │ │ │ ├── ConfigPanelSlideListener.java │ │ │ │ ├── FragmentPanelSlideListener.java │ │ │ │ ├── Slidr.java │ │ │ │ ├── model/ │ │ │ │ │ ├── SlidrConfig.java │ │ │ │ │ ├── SlidrInterface.java │ │ │ │ │ ├── SlidrListener.java │ │ │ │ │ ├── SlidrListenerAdapter.java │ │ │ │ │ └── SlidrPosition.java │ │ │ │ ├── util/ │ │ │ │ │ └── ViewDragHelper.java │ │ │ │ └── widget/ │ │ │ │ ├── ScrimRenderer.java │ │ │ │ └── SliderPanel.java │ │ │ ├── events/ │ │ │ │ ├── ChangeAppLockEvent.java │ │ │ │ ├── ChangeAutoplayNsfwVideosEvent.java │ │ │ │ ├── ChangeCompactLayoutToolbarHiddenByDefaultEvent.java │ │ │ │ ├── ChangeDataSavingModeEvent.java │ │ │ │ ├── ChangeDefaultLinkPostLayoutEvent.java │ │ │ │ ├── ChangeDefaultPostLayoutEvent.java │ │ │ │ ├── ChangeDefaultPostLayoutUnfoldedEvent.java │ │ │ │ ├── ChangeDisableImagePreviewEvent.java │ │ │ │ ├── ChangeDisableSwipingBetweenTabsEvent.java │ │ │ │ ├── ChangeEasierToWatchInFullScreenEvent.java │ │ │ │ ├── ChangeEnableSwipeActionSwitchEvent.java │ │ │ │ ├── ChangeFixedHeightPreviewInCardEvent.java │ │ │ │ ├── ChangeHideFabInPostFeedEvent.java │ │ │ │ ├── ChangeHideKarmaEvent.java │ │ │ │ ├── ChangeHidePostFlairEvent.java │ │ │ │ ├── ChangeHidePostTypeEvent.java │ │ │ │ ├── ChangeHideSubredditAndUserPrefixEvent.java │ │ │ │ ├── ChangeHideTextPostContent.java │ │ │ │ ├── ChangeHideTheNumberOfCommentsEvent.java │ │ │ │ ├── ChangeHideTheNumberOfVotesEvent.java │ │ │ │ ├── ChangeInboxCountEvent.java │ │ │ │ ├── ChangeLockBottomAppBarEvent.java │ │ │ │ ├── ChangeLongPressToHideToolbarInCompactLayoutEvent.java │ │ │ │ ├── ChangeMuteAutoplayingVideosEvent.java │ │ │ │ ├── ChangeMuteNSFWVideoEvent.java │ │ │ │ ├── ChangeNSFWBlurEvent.java │ │ │ │ ├── ChangeNSFWEvent.java │ │ │ │ ├── ChangeNetworkStatusEvent.java │ │ │ │ ├── ChangeOnlyDisablePreviewInVideoAndGifPostsEvent.java │ │ │ │ ├── ChangePostFeedMaxResolutionEvent.java │ │ │ │ ├── ChangePostLayoutEvent.java │ │ │ │ ├── ChangePullToRefreshEvent.java │ │ │ │ ├── ChangeRememberMutingOptionInPostFeedEvent.java │ │ │ │ ├── ChangeRequireAuthToAccountSectionEvent.java │ │ │ │ ├── ChangeSavePostFeedScrolledPositionEvent.java │ │ │ │ ├── ChangeShowAbsoluteNumberOfVotesEvent.java │ │ │ │ ├── ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent.java │ │ │ │ ├── ChangeShowElapsedTimeEvent.java │ │ │ │ ├── ChangeSpoilerBlurEvent.java │ │ │ │ ├── ChangeStartAutoplayVisibleAreaOffsetEvent.java │ │ │ │ ├── ChangeSwipeActionEvent.java │ │ │ │ ├── ChangeSwipeActionThresholdEvent.java │ │ │ │ ├── ChangeTimeFormatEvent.java │ │ │ │ ├── ChangeVibrateWhenActionTriggeredEvent.java │ │ │ │ ├── ChangeVideoAutoplayEvent.java │ │ │ │ ├── ChangeVoteButtonsPositionEvent.java │ │ │ │ ├── FinishViewMediaActivityEvent.kt │ │ │ │ ├── FlairSelectedEvent.java │ │ │ │ ├── GoBackToMainPageEvent.java │ │ │ │ ├── NeedForPostListFromPostFragmentEvent.java │ │ │ │ ├── NewUserLoggedInEvent.java │ │ │ │ ├── PassPrivateMessageEvent.java │ │ │ │ ├── PassPrivateMessageIndexEvent.java │ │ │ │ ├── PostUpdateEventToPostDetailFragment.java │ │ │ │ ├── PostUpdateEventToPostList.java │ │ │ │ ├── ProvidePostListToViewPostDetailActivityEvent.java │ │ │ │ ├── RecreateActivityEvent.java │ │ │ │ ├── RefreshMultiRedditsEvent.java │ │ │ │ ├── RepliedToPrivateMessageEvent.java │ │ │ │ ├── ShowDividerInCompactLayoutPreferenceEvent.java │ │ │ │ ├── ShowThumbnailOnTheLeftInCompactLayoutEvent.java │ │ │ │ ├── SubmitChangeAvatarEvent.java │ │ │ │ ├── SubmitChangeBannerEvent.java │ │ │ │ ├── SubmitCrosspostEvent.java │ │ │ │ ├── SubmitGalleryPostEvent.java │ │ │ │ ├── SubmitImagePostEvent.java │ │ │ │ ├── SubmitPollPostEvent.java │ │ │ │ ├── SubmitSaveProfileEvent.java │ │ │ │ ├── SubmitTextOrLinkPostEvent.java │ │ │ │ ├── SubmitVideoOrGifPostEvent.java │ │ │ │ ├── SwitchAccountEvent.java │ │ │ │ └── ToggleSecureModeEvent.java │ │ │ ├── font/ │ │ │ │ ├── ContentFontFamily.java │ │ │ │ ├── ContentFontStyle.java │ │ │ │ ├── FontFamily.java │ │ │ │ ├── FontStyle.java │ │ │ │ ├── TitleFontFamily.java │ │ │ │ └── TitleFontStyle.java │ │ │ ├── fragments/ │ │ │ │ ├── CommentsListingFragment.java │ │ │ │ ├── CustomThemeListingFragment.java │ │ │ │ ├── FollowedUsersListingFragment.java │ │ │ │ ├── FragmentCommunicator.java │ │ │ │ ├── HistoryPostFragment.java │ │ │ │ ├── InboxFragment.java │ │ │ │ ├── MorePostsInfoFragment.java │ │ │ │ ├── MultiRedditListingFragment.java │ │ │ │ ├── PostFragment.java │ │ │ │ ├── PostFragmentBase.java │ │ │ │ ├── SidebarFragment.java │ │ │ │ ├── SubredditListingFragment.java │ │ │ │ ├── SubscribedSubredditsListingFragment.java │ │ │ │ ├── ThemePreviewCommentsFragment.java │ │ │ │ ├── ThemePreviewPostsFragment.java │ │ │ │ ├── UserListingFragment.java │ │ │ │ ├── ViewImgurImageFragment.java │ │ │ │ ├── ViewImgurVideoFragment.java │ │ │ │ ├── ViewImgurVideoFragmentBindingAdapter.java │ │ │ │ ├── ViewPostDetailFragment.java │ │ │ │ ├── ViewRedditGalleryImageOrGifFragment.java │ │ │ │ ├── ViewRedditGalleryVideoFragment.java │ │ │ │ └── ViewRedditGalleryVideoFragmentBindingAdapter.java │ │ │ ├── markdown/ │ │ │ │ ├── BlockQuoteWithExceptionParser.java │ │ │ │ ├── CustomMarkwonAdapter.java │ │ │ │ ├── Emote.java │ │ │ │ ├── EmoteCloseBracketInlineProcessor.java │ │ │ │ ├── EmoteInlineProcessor.java │ │ │ │ ├── EmotePlugin.java │ │ │ │ ├── EmoteSpanFactory.java │ │ │ │ ├── EvenBetterLinkMovementMethod.java │ │ │ │ ├── GiphyGifBlock.java │ │ │ │ ├── GiphyGifBlockParser.java │ │ │ │ ├── GiphyGifPlugin.java │ │ │ │ ├── ImageAndGifBlock.java │ │ │ │ ├── ImageAndGifBlockParser.java │ │ │ │ ├── ImageAndGifEntry.java │ │ │ │ ├── ImageAndGifPlugin.java │ │ │ │ ├── MarkdownUtils.java │ │ │ │ ├── RedditHeadingParser.java │ │ │ │ ├── RedditHeadingPlugin.java │ │ │ │ ├── RichTextJSONConverter.java │ │ │ │ ├── SpoilerAwareMovementMethod.java │ │ │ │ ├── SpoilerClosingInlineProcessor.java │ │ │ │ ├── SpoilerNode.java │ │ │ │ ├── SpoilerOpeningBracket.java │ │ │ │ ├── SpoilerOpeningBracketStorage.java │ │ │ │ ├── SpoilerOpeningInlineProcessor.java │ │ │ │ ├── SpoilerParserPlugin.java │ │ │ │ ├── SpoilerSpan.java │ │ │ │ ├── Superscript.java │ │ │ │ ├── SuperscriptClosingInlineProcessor.java │ │ │ │ ├── SuperscriptOpening.java │ │ │ │ ├── SuperscriptOpeningBracket.java │ │ │ │ ├── SuperscriptOpeningInlineProcessor.java │ │ │ │ ├── SuperscriptOpeningStorage.java │ │ │ │ ├── SuperscriptPlugin.java │ │ │ │ ├── SuperscriptSpan.java │ │ │ │ ├── UploadedImageBlock.java │ │ │ │ ├── UploadedImageBlockParser.java │ │ │ │ └── UploadedImagePlugin.java │ │ │ ├── message/ │ │ │ │ ├── ComposeMessage.java │ │ │ │ ├── FetchMessage.java │ │ │ │ ├── Message.java │ │ │ │ ├── MessageDataSource.java │ │ │ │ ├── MessageDataSourceFactory.java │ │ │ │ ├── MessageViewModel.java │ │ │ │ ├── ParseMessage.java │ │ │ │ ├── ReadMessage.java │ │ │ │ └── ReplyMessage.java │ │ │ ├── moderation/ │ │ │ │ ├── CommentModerationEvent.kt │ │ │ │ └── PostModerationEvent.kt │ │ │ ├── multireddit/ │ │ │ │ ├── AnonymousMultiredditSubreddit.java │ │ │ │ ├── AnonymousMultiredditSubredditDao.java │ │ │ │ ├── AnonymousMultiredditSubredditDaoKt.kt │ │ │ │ ├── CreateMultiReddit.java │ │ │ │ ├── DeleteMultiReddit.java │ │ │ │ ├── EditMultiReddit.java │ │ │ │ ├── ExpandedSubredditInMultiReddit.java │ │ │ │ ├── FavoriteMultiReddit.java │ │ │ │ ├── FetchMultiRedditInfo.java │ │ │ │ ├── FetchMyMultiReddits.java │ │ │ │ ├── MultiReddit.java │ │ │ │ ├── MultiRedditDao.java │ │ │ │ ├── MultiRedditDaoKt.kt │ │ │ │ ├── MultiRedditJSONModel.java │ │ │ │ ├── MultiRedditRepository.java │ │ │ │ ├── MultiRedditViewModel.java │ │ │ │ ├── ParseMultiReddit.java │ │ │ │ └── SubredditInMultiReddit.java │ │ │ ├── network/ │ │ │ │ ├── AccessTokenAuthenticator.java │ │ │ │ ├── AnyAccountAccessTokenAuthenticator.java │ │ │ │ ├── RedgifsAccessTokenAuthenticator.java │ │ │ │ ├── ServerAccessTokenAuthenticator.java │ │ │ │ ├── SortTypeConverter.java │ │ │ │ └── SortTypeConverterFactory.java │ │ │ ├── post/ │ │ │ │ ├── FetchPost.java │ │ │ │ ├── FetchRules.java │ │ │ │ ├── FetchStreamableVideo.java │ │ │ │ ├── HidePost.java │ │ │ │ ├── HistoryPostPagingSource.java │ │ │ │ ├── HistoryPostViewModel.java │ │ │ │ ├── ImgurMedia.java │ │ │ │ ├── LoadingMorePostsStatus.java │ │ │ │ ├── MarkPostAsReadInterface.java │ │ │ │ ├── ParsePost.java │ │ │ │ ├── PollPayload.java │ │ │ │ ├── Post.java │ │ │ │ ├── PostPagingSource.java │ │ │ │ ├── PostViewModel.java │ │ │ │ ├── RedditGalleryPayload.java │ │ │ │ └── SubmitPost.java │ │ │ ├── postfilter/ │ │ │ │ ├── DeletePostFilter.java │ │ │ │ ├── DeletePostFilterUsage.java │ │ │ │ ├── PostFilter.java │ │ │ │ ├── PostFilterDao.java │ │ │ │ ├── PostFilterUsage.java │ │ │ │ ├── PostFilterUsageDao.java │ │ │ │ ├── PostFilterUsageViewModel.java │ │ │ │ ├── PostFilterWithUsage.java │ │ │ │ ├── PostFilterWithUsageViewModel.java │ │ │ │ ├── SavePostFilter.java │ │ │ │ └── SavePostFilterUsage.java │ │ │ ├── readpost/ │ │ │ │ ├── InsertReadPost.java │ │ │ │ ├── NullReadPostsList.java │ │ │ │ ├── ReadPost.java │ │ │ │ ├── ReadPostDao.java │ │ │ │ ├── ReadPostsList.java │ │ │ │ ├── ReadPostsListInterface.java │ │ │ │ └── ReadPostsUtils.java │ │ │ ├── recentsearchquery/ │ │ │ │ ├── InsertRecentSearchQuery.java │ │ │ │ ├── RecentSearchQuery.java │ │ │ │ ├── RecentSearchQueryDao.java │ │ │ │ ├── RecentSearchQueryRepository.java │ │ │ │ └── RecentSearchQueryViewModel.java │ │ │ ├── repositories/ │ │ │ │ ├── CommentActivityRepository.kt │ │ │ │ ├── CopyMultiRedditActivityRepository.kt │ │ │ │ └── EditCommentActivityRepository.kt │ │ │ ├── services/ │ │ │ │ ├── DownloadMediaService.java │ │ │ │ ├── DownloadRedditVideoService.java │ │ │ │ ├── EditProfileService.java │ │ │ │ └── SubmitPostService.java │ │ │ ├── settings/ │ │ │ │ ├── APIKeysPreferenceFragment.java │ │ │ │ ├── AboutPreferenceFragment.java │ │ │ │ ├── Acknowledgement.java │ │ │ │ ├── AcknowledgementFragment.java │ │ │ │ ├── AdvancedPreferenceFragment.java │ │ │ │ ├── CommentPreferenceFragment.java │ │ │ │ ├── CrashReportsFragment.java │ │ │ │ ├── CreditsPreferenceFragment.java │ │ │ │ ├── CustomizeBottomAppBarFragment.java │ │ │ │ ├── CustomizeMainPageTabsFragment.java │ │ │ │ ├── DataSavingModePreferenceFragment.java │ │ │ │ ├── DebugPreferenceFragment.java │ │ │ │ ├── DownloadLocationPreferenceFragment.java │ │ │ │ ├── FontPreferenceFragment.java │ │ │ │ ├── FontPreviewFragment.java │ │ │ │ ├── GesturesAndButtonsPreferenceFragment.java │ │ │ │ ├── ImmersiveInterfacePreferenceFragment.java │ │ │ │ ├── InterfacePreferenceFragment.java │ │ │ │ ├── MainPreferenceFragment.java │ │ │ │ ├── MiscellaneousPreferenceFragment.java │ │ │ │ ├── NavigationDrawerPreferenceFragment.java │ │ │ │ ├── NotificationPreferenceFragment.java │ │ │ │ ├── NsfwAndSpoilerFragment.java │ │ │ │ ├── NumberOfColumnsInPostFeedPreferenceFragment.java │ │ │ │ ├── PostDetailsPreferenceFragment.java │ │ │ │ ├── PostHistoryFragment.java │ │ │ │ ├── PostPreferenceFragment.java │ │ │ │ ├── ProxyPreferenceFragment.java │ │ │ │ ├── SecurityPreferenceFragment.java │ │ │ │ ├── SettingsSearchFragment.java │ │ │ │ ├── SettingsSearchItem.java │ │ │ │ ├── SettingsSearchRegistry.java │ │ │ │ ├── SortTypePreferenceFragment.java │ │ │ │ ├── SwipeActionPreferenceFragment.java │ │ │ │ ├── ThemePreferenceFragment.java │ │ │ │ ├── TimeFormatPreferenceFragment.java │ │ │ │ ├── Translation.java │ │ │ │ ├── TranslationFragment.java │ │ │ │ └── VideoPreferenceFragment.java │ │ │ ├── subreddit/ │ │ │ │ ├── FetchFlairs.java │ │ │ │ ├── FetchSubredditData.java │ │ │ │ ├── Flair.java │ │ │ │ ├── ParseSubredditData.java │ │ │ │ ├── Rule.java │ │ │ │ ├── SubredditDao.java │ │ │ │ ├── SubredditData.java │ │ │ │ ├── SubredditListingDataSource.java │ │ │ │ ├── SubredditListingDataSourceFactory.java │ │ │ │ ├── SubredditListingViewModel.java │ │ │ │ ├── SubredditRepository.java │ │ │ │ ├── SubredditSettingData.java │ │ │ │ ├── SubredditSubscription.java │ │ │ │ ├── SubredditViewModel.java │ │ │ │ ├── SubredditWithSelection.java │ │ │ │ └── shortcut/ │ │ │ │ └── ShortcutManager.java │ │ │ ├── subscribedsubreddit/ │ │ │ │ ├── SubscribedSubredditDao.java │ │ │ │ ├── SubscribedSubredditData.java │ │ │ │ ├── SubscribedSubredditRepository.java │ │ │ │ └── SubscribedSubredditViewModel.java │ │ │ ├── subscribeduser/ │ │ │ │ ├── SubscribedUserDao.java │ │ │ │ ├── SubscribedUserData.java │ │ │ │ ├── SubscribedUserRepository.java │ │ │ │ └── SubscribedUserViewModel.java │ │ │ ├── thing/ │ │ │ │ ├── DeleteThing.java │ │ │ │ ├── FavoriteThing.java │ │ │ │ ├── FetchRedgifsVideoLinks.java │ │ │ │ ├── FetchSubscribedThing.java │ │ │ │ ├── GiphyGif.java │ │ │ │ ├── MediaMetadata.java │ │ │ │ ├── ReplyNotificationsToggle.java │ │ │ │ ├── ReportReason.java │ │ │ │ ├── ReportThing.java │ │ │ │ ├── SaveThing.java │ │ │ │ ├── SelectThingReturnKey.java │ │ │ │ ├── SortType.java │ │ │ │ ├── SortTypeSelectionCallback.java │ │ │ │ ├── StreamableVideo.java │ │ │ │ ├── TrendingSearch.java │ │ │ │ ├── UploadedImage.java │ │ │ │ └── VoteThing.java │ │ │ ├── user/ │ │ │ │ ├── BlockUser.java │ │ │ │ ├── FetchUserData.java │ │ │ │ ├── FetchUserFlairs.java │ │ │ │ ├── SelectUserFlair.java │ │ │ │ ├── UserDao.java │ │ │ │ ├── UserData.java │ │ │ │ ├── UserFlair.java │ │ │ │ ├── UserFollowing.java │ │ │ │ ├── UserListingDataSource.java │ │ │ │ ├── UserListingDataSourceFactory.java │ │ │ │ ├── UserListingViewModel.java │ │ │ │ ├── UserProfileImagesBatchLoader.java │ │ │ │ ├── UserRepository.java │ │ │ │ ├── UserViewModel.java │ │ │ │ └── UserWithSelection.java │ │ │ ├── utils/ │ │ │ │ ├── APIUtils.java │ │ │ │ ├── AppRestartHelper.java │ │ │ │ ├── ColorUtils.kt │ │ │ │ ├── CommentScrollPositionCache.java │ │ │ │ ├── CustomThemeSharedPreferencesUtils.java │ │ │ │ ├── EditProfileUtils.java │ │ │ │ ├── GlideImageGetter.java │ │ │ │ ├── JSONUtils.java │ │ │ │ ├── MaterialYouUtils.java │ │ │ │ ├── NotificationUtils.java │ │ │ │ ├── ShareScreenshotUtils.kt │ │ │ │ ├── SharedPreferencesLiveData.kt │ │ │ │ ├── SharedPreferencesUtils.java │ │ │ │ ├── UploadImageUtils.java │ │ │ │ └── Utils.java │ │ │ ├── videoautoplay/ │ │ │ │ ├── BaseMeter.java │ │ │ │ ├── CacheManager.java │ │ │ │ ├── Config.java │ │ │ │ ├── DefaultExoCreator.java │ │ │ │ ├── ExoCreator.java │ │ │ │ ├── ExoPlayable.java │ │ │ │ ├── ExoPlayerViewHelper.java │ │ │ │ ├── MediaSourceBuilder.java │ │ │ │ ├── MultiPlayPlayerSelector.kt │ │ │ │ ├── Playable.java │ │ │ │ ├── PlayableImpl.java │ │ │ │ ├── PlayerDispatcher.java │ │ │ │ ├── PlayerSelector.java │ │ │ │ ├── ToroExo.java │ │ │ │ ├── ToroExoPlayer.java │ │ │ │ ├── ToroPlayer.java │ │ │ │ ├── ToroUtil.java │ │ │ │ ├── annotations/ │ │ │ │ │ ├── Beta.java │ │ │ │ │ ├── RemoveIn.java │ │ │ │ │ └── Sorted.java │ │ │ │ ├── helper/ │ │ │ │ │ └── ToroPlayerHelper.java │ │ │ │ ├── media/ │ │ │ │ │ ├── DrmMedia.java │ │ │ │ │ ├── PlaybackInfo.java │ │ │ │ │ └── VolumeInfo.java │ │ │ │ └── widget/ │ │ │ │ ├── Common.java │ │ │ │ ├── Container.java │ │ │ │ ├── PlaybackInfoCache.java │ │ │ │ ├── PlayerManager.java │ │ │ │ └── PressablePlayerSelector.java │ │ │ ├── viewmodels/ │ │ │ │ ├── CommentActivityViewModel.kt │ │ │ │ ├── CopyMultiRedditActivityViewModel.kt │ │ │ │ ├── EditCommentActivityViewModel.kt │ │ │ │ ├── ViewPostDetailActivityViewModel.java │ │ │ │ └── ViewPostDetailFragmentViewModel.kt │ │ │ └── worker/ │ │ │ ├── MaterialYouWorker.java │ │ │ └── PullNotificationWorker.java │ │ └── res/ │ │ ├── anim/ │ │ │ ├── enter_from_left.xml │ │ │ ├── enter_from_right.xml │ │ │ ├── exit_to_left.xml │ │ │ ├── exit_to_right.xml │ │ │ ├── slide_out_down.xml │ │ │ └── slide_out_up.xml │ │ ├── drawable/ │ │ │ ├── background_autoplay.xml │ │ │ ├── circular_background.xml │ │ │ ├── edit_text_cursor.xml │ │ │ ├── error_image.xml │ │ │ ├── exo_control_button_background.xml │ │ │ ├── exo_player_control_button_circular_background.xml │ │ │ ├── exo_player_control_view_background.xml │ │ │ ├── flag_brazil.xml │ │ │ ├── flag_bulgaria.xml │ │ │ ├── flag_china.xml │ │ │ ├── flag_croatia.xml │ │ │ ├── flag_france.xml │ │ │ ├── flag_germany.xml │ │ │ ├── flag_greece.xml │ │ │ ├── flag_hungary.xml │ │ │ ├── flag_india.xml │ │ │ ├── flag_israel.xml │ │ │ ├── flag_italy.xml │ │ │ ├── flag_japan.xml │ │ │ ├── flag_netherlands.xml │ │ │ ├── flag_norway.xml │ │ │ ├── flag_poland.xml │ │ │ ├── flag_portugal.xml │ │ │ ├── flag_romania.xml │ │ │ ├── flag_russia.xml │ │ │ ├── flag_somalia.xml │ │ │ ├── flag_south_korea.xml │ │ │ ├── flag_spain.xml │ │ │ ├── flag_sweden.xml │ │ │ ├── flag_turkey.xml │ │ │ ├── flag_vietnam.xml │ │ │ ├── ic_about_day_night_24dp.xml │ │ │ ├── ic_access_time_day_night_24dp.xml │ │ │ ├── ic_account_circle_day_night_24dp.xml │ │ │ ├── ic_add_24dp.xml │ │ │ ├── ic_add_a_photo_24dp.xml │ │ │ ├── ic_add_a_photo_day_night_24dp.xml │ │ │ ├── ic_add_circle_outline_day_night_24dp.xml │ │ │ ├── ic_add_day_night_24dp.xml │ │ │ ├── ic_advanced_day_night_24dp.xml │ │ │ ├── ic_amoled_theme_preference_day_night_24dp.xml │ │ │ ├── ic_anonymous_day_night_24dp.xml │ │ │ ├── ic_apply_to_day_night_24dp.xml │ │ │ ├── ic_approve_24dp.xml │ │ │ ├── ic_archive_outline.xml │ │ │ ├── ic_arrow_back_24dp.xml │ │ │ ├── ic_arrow_back_white_24dp.xml │ │ │ ├── ic_arrow_downward_day_night_24dp.xml │ │ │ ├── ic_arrow_downward_grey_24dp.xml │ │ │ ├── ic_arrow_upward_day_night_24dp.xml │ │ │ ├── ic_arrow_upward_grey_24dp.xml │ │ │ ├── ic_baseline_arrow_drop_down_24dp.xml │ │ │ ├── ic_baseline_arrow_drop_up_24dp.xml │ │ │ ├── ic_best_24.xml │ │ │ ├── ic_bold_black_24dp.xml │ │ │ ├── ic_bookmark_border_day_night_24dp.xml │ │ │ ├── ic_bookmark_border_grey_24dp.xml │ │ │ ├── ic_bookmark_border_toolbar_24dp.xml │ │ │ ├── ic_bookmark_day_night_24dp.xml │ │ │ ├── ic_bookmark_grey_24dp.xml │ │ │ ├── ic_bookmark_toolbar_24dp.xml │ │ │ ├── ic_bookmarks_day_night_24dp.xml │ │ │ ├── ic_call_split_24.xml │ │ │ ├── ic_check_circle_day_night_24dp.xml │ │ │ ├── ic_check_circle_toolbar_24dp.xml │ │ │ ├── ic_close_24dp.xml │ │ │ ├── ic_code_24dp.xml │ │ │ ├── ic_color_lens_day_night_24dp.xml │ │ │ ├── ic_comment_grey_24dp.xml │ │ │ ├── ic_comment_toolbar_24dp.xml │ │ │ ├── ic_controversial_24.xml │ │ │ ├── ic_copy_16dp.xml │ │ │ ├── ic_copy_day_night_24dp.xml │ │ │ ├── ic_crosspost_24dp.xml │ │ │ ├── ic_current_user_14dp.xml │ │ │ ├── ic_dark_theme_24dp.xml │ │ │ ├── ic_dark_theme_preference_day_night_24dp.xml │ │ │ ├── ic_data_saving_mode_day_night_24dp.xml │ │ │ ├── ic_delete_all_24.xml │ │ │ ├── ic_delete_day_night_24dp.xml │ │ │ ├── ic_distinguish_as_mod_24dp.xml │ │ │ ├── ic_dot_outline.xml │ │ │ ├── ic_download_day_night_24dp.xml │ │ │ ├── ic_downvote_24dp.xml │ │ │ ├── ic_downvote_filled_24dp.xml │ │ │ ├── ic_edit_day_night_24dp.xml │ │ │ ├── ic_error_outline_black_day_night_24dp.xml │ │ │ ├── ic_error_outline_white_24dp.xml │ │ │ ├── ic_error_white_36dp.xml │ │ │ ├── ic_exit_day_night_24dp.xml │ │ │ ├── ic_expand_less_grey_24dp.xml │ │ │ ├── ic_expand_more_grey_24dp.xml │ │ │ ├── ic_fast_forward_24dp.xml │ │ │ ├── ic_fast_rewind_24dp.xml │ │ │ ├── ic_favorite_24dp.xml │ │ │ ├── ic_favorite_border_24dp.xml │ │ │ ├── ic_file_download_toolbar_white_24dp.xml │ │ │ ├── ic_filter_day_night_24dp.xml │ │ │ ├── ic_font_size_day_night_24dp.xml │ │ │ ├── ic_fullscreen_white_rounded_24dp.xml │ │ │ ├── ic_gallery_day_night_24dp.xml │ │ │ ├── ic_gesture_day_night_24dp.xml │ │ │ ├── ic_gif_24dp.xml │ │ │ ├── ic_give_award_day_night_24dp.xml │ │ │ ├── ic_hide_post_day_night_24dp.xml │ │ │ ├── ic_hide_read_posts_day_night_24dp.xml │ │ │ ├── ic_history_day_night_24dp.xml │ │ │ ├── ic_home_day_night_24dp.xml │ │ │ ├── ic_hot_24dp.xml │ │ │ ├── ic_image_day_night_24dp.xml │ │ │ ├── ic_import_day_night_24dp.xml │ │ │ ├── ic_inbox_day_night_24dp.xml │ │ │ ├── ic_info_preference_day_night_24dp.xml │ │ │ ├── ic_interface_day_night_24dp.xml │ │ │ ├── ic_italic_black_24dp.xml │ │ │ ├── ic_key_day_night_24dp.xml │ │ │ ├── ic_keyboard_arrow_down_24dp.xml │ │ │ ├── ic_keyboard_double_arrow_up_day_night_24dp.xml │ │ │ ├── ic_language_day_night_24dp.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_launcher_foreground_monochrome.xml │ │ │ ├── ic_light_theme_24dp.xml │ │ │ ├── ic_light_theme_preference_day_night_24dp.xml │ │ │ ├── ic_link_day_night_24dp.xml │ │ │ ├── ic_link_post_type_indicator_day_night_24dp.xml │ │ │ ├── ic_link_round_black_24dp.xml │ │ │ ├── ic_lock_day_night_24dp.xml │ │ │ ├── ic_log_out_day_night_24dp.xml │ │ │ ├── ic_login_24dp.xml │ │ │ ├── ic_mark_nsfw_24dp.xml │ │ │ ├── ic_mic_14dp.xml │ │ │ ├── ic_miscellaneous_day_night_24dp.xml │ │ │ ├── ic_mod_24dp.xml │ │ │ ├── ic_more_vert_grey_24dp.xml │ │ │ ├── ic_multi_reddit_day_night_24dp.xml │ │ │ ├── ic_mute_24dp.xml │ │ │ ├── ic_mute_preferences_day_night_24dp.xml │ │ │ ├── ic_new_24.xml │ │ │ ├── ic_notification.xml │ │ │ ├── ic_notifications_day_night_24dp.xml │ │ │ ├── ic_nsfw_off_day_night_24dp.xml │ │ │ ├── ic_nsfw_on_day_night_24dp.xml │ │ │ ├── ic_open_link_day_night_24dp.xml │ │ │ ├── ic_ordered_list_black_24dp.xml │ │ │ ├── ic_pause_24dp.xml │ │ │ ├── ic_play_arrow_24dp.xml │ │ │ ├── ic_play_circle_24dp.xml │ │ │ ├── ic_play_circle_36dp.xml │ │ │ ├── ic_playback_speed_day_night_24dp.xml │ │ │ ├── ic_playback_speed_toolbar_24dp.xml │ │ │ ├── ic_poll_day_night_24dp.xml │ │ │ ├── ic_post_layout_day_night_24dp.xml │ │ │ ├── ic_preview_day_night_24dp.xml │ │ │ ├── ic_privacy_policy_day_night_24dp.xml │ │ │ ├── ic_quote_24dp.xml │ │ │ ├── ic_quote_left_24dp.xml │ │ │ ├── ic_random_day_night_24dp.xml │ │ │ ├── ic_refresh_day_night_24dp.xml │ │ │ ├── ic_remove_24dp.xml │ │ │ ├── ic_reply_day_night_24dp.xml │ │ │ ├── ic_reply_grey_24dp.xml │ │ │ ├── ic_report_day_night_24dp.xml │ │ │ ├── ic_rising_24dp.xml │ │ │ ├── ic_save_to_database_day_night_24dp.xml │ │ │ ├── ic_search_day_night_24dp.xml │ │ │ ├── ic_search_toolbar_24dp.xml │ │ │ ├── ic_security_day_night_24dp.xml │ │ │ ├── ic_select_photo_24dp.xml │ │ │ ├── ic_select_photo_day_night_24dp.xml │ │ │ ├── ic_select_query_24dp.xml │ │ │ ├── ic_send_black_24dp.xml │ │ │ ├── ic_send_toolbar_24dp.xml │ │ │ ├── ic_settings_day_night_24dp.xml │ │ │ ├── ic_share_day_night_24dp.xml │ │ │ ├── ic_share_grey_24dp.xml │ │ │ ├── ic_share_toolbar_white_24dp.xml │ │ │ ├── ic_sort_day_night_24dp.xml │ │ │ ├── ic_sort_toolbar_24dp.xml │ │ │ ├── ic_spam_24dp.xml │ │ │ ├── ic_spoiler_24dp.xml │ │ │ ├── ic_spoiler_black_24dp.xml │ │ │ ├── ic_stick_post_24dp.xml │ │ │ ├── ic_strikethrough_black_24dp.xml │ │ │ ├── ic_submit_post_day_night_24dp.xml │ │ │ ├── ic_subreddit_day_night_24dp.xml │ │ │ ├── ic_subscriptions_bottom_app_bar_day_night_24dp.xml │ │ │ ├── ic_superscript_24dp.xml │ │ │ ├── ic_text_day_night_24dp.xml │ │ │ ├── ic_thumbtack_24dp.xml │ │ │ ├── ic_title_24dp.xml │ │ │ ├── ic_top_24.xml │ │ │ ├── ic_undistinguish_as_mod_24dp.xml │ │ │ ├── ic_unlock_24dp.xml │ │ │ ├── ic_unmark_nsfw_24dp.xml │ │ │ ├── ic_unmark_spoiler_24dp.xml │ │ │ ├── ic_unmute_24dp.xml │ │ │ ├── ic_unordered_list_24dp.xml │ │ │ ├── ic_unstick_post_24dp.xml │ │ │ ├── ic_upvote_24dp.xml │ │ │ ├── ic_upvote_filled_24dp.xml │ │ │ ├── ic_upvote_ratio_18dp.xml │ │ │ ├── ic_user_agreement_day_night_24dp.xml │ │ │ ├── ic_user_day_night_24dp.xml │ │ │ ├── ic_verified_user_14dp.xml │ │ │ ├── ic_video_day_night_24dp.xml │ │ │ ├── ic_video_quality_24dp.xml │ │ │ ├── ic_volume_off_32dp.xml │ │ │ ├── ic_volume_up_32dp.xml │ │ │ ├── ic_wallpaper_24dp.xml │ │ │ ├── ic_wallpaper_both_day_night_24dp.xml │ │ │ ├── ic_wallpaper_home_screen_day_night_24dp.xml │ │ │ ├── ic_wallpaper_lock_screen_day_night_24dp.xml │ │ │ ├── play_button_round_background.xml │ │ │ ├── preference_background_bottom.xml │ │ │ ├── preference_background_middle.xml │ │ │ ├── preference_background_top.xml │ │ │ ├── preference_background_top_and_bottom.xml │ │ │ ├── private_message_ballon.xml │ │ │ ├── splash_screen.xml │ │ │ ├── thumbnail_compact_layout_rounded_edge.xml │ │ │ └── trending_search_title_background.xml │ │ ├── drawable-night/ │ │ │ ├── ic_about_day_night_24dp.xml │ │ │ ├── ic_access_time_day_night_24dp.xml │ │ │ ├── ic_account_circle_day_night_24dp.xml │ │ │ ├── ic_add_a_photo_day_night_24dp.xml │ │ │ ├── ic_add_circle_outline_day_night_24dp.xml │ │ │ ├── ic_add_day_night_24dp.xml │ │ │ ├── ic_advanced_day_night_24dp.xml │ │ │ ├── ic_amoled_theme_preference_day_night_24dp.xml │ │ │ ├── ic_anonymous_day_night_24dp.xml │ │ │ ├── ic_apply_to_day_night_24dp.xml │ │ │ ├── ic_arrow_downward_day_night_24dp.xml │ │ │ ├── ic_arrow_upward_day_night_24dp.xml │ │ │ ├── ic_bookmark_border_day_night_24dp.xml │ │ │ ├── ic_bookmark_day_night_24dp.xml │ │ │ ├── ic_bookmarks_day_night_24dp.xml │ │ │ ├── ic_check_circle_day_night_24dp.xml │ │ │ ├── ic_color_lens_day_night_24dp.xml │ │ │ ├── ic_copy_day_night_24dp.xml │ │ │ ├── ic_dark_theme_preference_day_night_24dp.xml │ │ │ ├── ic_data_saving_mode_day_night_24dp.xml │ │ │ ├── ic_delete_day_night_24dp.xml │ │ │ ├── ic_download_day_night_24dp.xml │ │ │ ├── ic_edit_day_night_24dp.xml │ │ │ ├── ic_error_outline_black_day_night_24dp.xml │ │ │ ├── ic_exit_day_night_24dp.xml │ │ │ ├── ic_filter_day_night_24dp.xml │ │ │ ├── ic_font_size_day_night_24dp.xml │ │ │ ├── ic_gallery_day_night_24dp.xml │ │ │ ├── ic_gesture_day_night_24dp.xml │ │ │ ├── ic_give_award_day_night_24dp.xml │ │ │ ├── ic_hide_post_day_night_24dp.xml │ │ │ ├── ic_hide_read_posts_day_night_24dp.xml │ │ │ ├── ic_history_day_night_24dp.xml │ │ │ ├── ic_home_day_night_24dp.xml │ │ │ ├── ic_image_day_night_24dp.xml │ │ │ ├── ic_import_day_night_24dp.xml │ │ │ ├── ic_inbox_day_night_24dp.xml │ │ │ ├── ic_info_preference_day_night_24dp.xml │ │ │ ├── ic_interface_day_night_24dp.xml │ │ │ ├── ic_key_day_night_24dp.xml │ │ │ ├── ic_keyboard_double_arrow_up_day_night_24dp.xml │ │ │ ├── ic_language_day_night_24dp.xml │ │ │ ├── ic_light_theme_preference_day_night_24dp.xml │ │ │ ├── ic_link_day_night_24dp.xml │ │ │ ├── ic_link_post_type_indicator_day_night_24dp.xml │ │ │ ├── ic_lock_day_night_24dp.xml │ │ │ ├── ic_log_out_day_night_24dp.xml │ │ │ ├── ic_miscellaneous_day_night_24dp.xml │ │ │ ├── ic_multi_reddit_day_night_24dp.xml │ │ │ ├── ic_mute_preferences_day_night_24dp.xml │ │ │ ├── ic_notifications_day_night_24dp.xml │ │ │ ├── ic_nsfw_off_day_night_24dp.xml │ │ │ ├── ic_nsfw_on_day_night_24dp.xml │ │ │ ├── ic_open_link_day_night_24dp.xml │ │ │ ├── ic_playback_speed_day_night_24dp.xml │ │ │ ├── ic_poll_day_night_24dp.xml │ │ │ ├── ic_post_layout_day_night_24dp.xml │ │ │ ├── ic_preview_day_night_24dp.xml │ │ │ ├── ic_privacy_policy_day_night_24dp.xml │ │ │ ├── ic_random_day_night_24dp.xml │ │ │ ├── ic_refresh_day_night_24dp.xml │ │ │ ├── ic_reply_day_night_24dp.xml │ │ │ ├── ic_report_day_night_24dp.xml │ │ │ ├── ic_save_to_database_day_night_24dp.xml │ │ │ ├── ic_search_day_night_24dp.xml │ │ │ ├── ic_security_day_night_24dp.xml │ │ │ ├── ic_select_photo_day_night_24dp.xml │ │ │ ├── ic_settings_day_night_24dp.xml │ │ │ ├── ic_share_day_night_24dp.xml │ │ │ ├── ic_sort_day_night_24dp.xml │ │ │ ├── ic_submit_post_day_night_24dp.xml │ │ │ ├── ic_subreddit_day_night_24dp.xml │ │ │ ├── ic_subscriptions_bottom_app_bar_day_night_24dp.xml │ │ │ ├── ic_text_day_night_24dp.xml │ │ │ ├── ic_user_agreement_day_night_24dp.xml │ │ │ ├── ic_user_day_night_24dp.xml │ │ │ ├── ic_video_day_night_24dp.xml │ │ │ ├── ic_wallpaper_both_day_night_24dp.xml │ │ │ ├── ic_wallpaper_home_screen_day_night_24dp.xml │ │ │ └── ic_wallpaper_lock_screen_day_night_24dp.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_background.xml │ │ ├── drawable-xhdpi/ │ │ │ └── ic_cancel_24dp.xml │ │ ├── font/ │ │ │ ├── atkinson_hyperlegible.xml │ │ │ ├── atkinson_hyperlegible_bold_version.xml │ │ │ ├── balsamiq_sans.xml │ │ │ ├── balsamiq_sans_bold_version.xml │ │ │ ├── harmonia_sans.xml │ │ │ ├── inter.xml │ │ │ ├── manrope.xml │ │ │ ├── noto_sans.xml │ │ │ ├── noto_sans_bold_version.xml │ │ │ ├── roboto_condensed.xml │ │ │ └── roboto_condensed_bold_version.xml │ │ ├── layout/ │ │ │ ├── activity_account_posts.xml │ │ │ ├── activity_account_saved_thing.xml │ │ │ ├── activity_comment.xml │ │ │ ├── activity_comment_filter_preference.xml │ │ │ ├── activity_comment_filter_usage_listing.xml │ │ │ ├── activity_comment_full_markdown.xml │ │ │ ├── activity_create_multi_reddit.xml │ │ │ ├── activity_custom_theme_listing.xml │ │ │ ├── activity_customize_comment_filter.xml │ │ │ ├── activity_customize_post_filter.xml │ │ │ ├── activity_customize_theme.xml │ │ │ ├── activity_edit_comment.xml │ │ │ ├── activity_edit_multi_reddit.xml │ │ │ ├── activity_edit_post.xml │ │ │ ├── activity_edit_profile.xml │ │ │ ├── activity_fetch_random_subreddit_or_post.xml │ │ │ ├── activity_filtered_thing.xml │ │ │ ├── activity_history.xml │ │ │ ├── activity_inbox.xml │ │ │ ├── activity_lock_screen.xml │ │ │ ├── activity_login.xml │ │ │ ├── activity_login_chrome_custom_tab.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_post_filter_application.xml │ │ │ ├── activity_post_filter_preference.xml │ │ │ ├── activity_post_gallery.xml │ │ │ ├── activity_post_image.xml │ │ │ ├── activity_post_link.xml │ │ │ ├── activity_post_poll.xml │ │ │ ├── activity_post_text.xml │ │ │ ├── activity_post_video.xml │ │ │ ├── activity_qrcode_scanner.xml │ │ │ ├── activity_report.xml │ │ │ ├── activity_rules.xml │ │ │ ├── activity_search.xml │ │ │ ├── activity_search_history.xml │ │ │ ├── activity_search_result.xml │ │ │ ├── activity_search_subreddits_result.xml │ │ │ ├── activity_search_users_result.xml │ │ │ ├── activity_select_user_flair.xml │ │ │ ├── activity_selected_subreddits.xml │ │ │ ├── activity_send_private_message.xml │ │ │ ├── activity_settings.xml │ │ │ ├── activity_submit_crosspost.xml │ │ │ ├── activity_subscribed_subreddits_multiselection.xml │ │ │ ├── activity_subscribed_thing_listing.xml │ │ │ ├── activity_subscribed_users_multiselection.xml │ │ │ ├── activity_suicide_prevention.xml │ │ │ ├── activity_theme_preview.xml │ │ │ ├── activity_view_image_or_gif.xml │ │ │ ├── activity_view_imgur_media.xml │ │ │ ├── activity_view_multi_reddit_detail.xml │ │ │ ├── activity_view_post_detail.xml │ │ │ ├── activity_view_private_messages.xml │ │ │ ├── activity_view_reddit_gallery.xml │ │ │ ├── activity_view_subreddit_detail.xml │ │ │ ├── activity_view_user_detail.xml │ │ │ ├── activity_view_video.xml │ │ │ ├── activity_view_video_zoomable.xml │ │ │ ├── activity_web_view.xml │ │ │ ├── activity_wiki.xml │ │ │ ├── adapter_default_entry.xml │ │ │ ├── adapter_table_block.xml │ │ │ ├── app_bar_main.xml │ │ │ ├── bottom_app_bar.xml │ │ │ ├── color_picker.xml │ │ │ ├── copy_text_material_dialog.xml │ │ │ ├── custom_barcode_scanner.xml │ │ │ ├── dialog_edit_flair.xml │ │ │ ├── dialog_edit_name.xml │ │ │ ├── dialog_edit_post_or_comment_filter_name_of_usage.xml │ │ │ ├── dialog_edit_text.xml │ │ │ ├── dialog_go_to_thing_edit_text.xml │ │ │ ├── dialog_insert_link.xml │ │ │ ├── dialog_select_header.xml │ │ │ ├── exo_autoplay_playback_control_view.xml │ │ │ ├── exo_autoplay_playback_control_view_legacy.xml │ │ │ ├── exo_playback_control_view.xml │ │ │ ├── floating_action_button.xml │ │ │ ├── fragment_account_chooser_bottom_sheet.xml │ │ │ ├── fragment_acknowledgement.xml │ │ │ ├── fragment_comment_filter_options_bottom_sheet.xml │ │ │ ├── fragment_comment_filter_usage_options_bottom_sheet.xml │ │ │ ├── fragment_comment_moderation_action_bottom_sheet.xml │ │ │ ├── fragment_comment_more_bottom_sheet.xml │ │ │ ├── fragment_comments_listing.xml │ │ │ ├── fragment_copy_text_bottom_sheet.xml │ │ │ ├── fragment_crash_reports.xml │ │ │ ├── fragment_create_theme_bottom_sheet.xml │ │ │ ├── fragment_custom_theme_listing.xml │ │ │ ├── fragment_custom_theme_options_bottom_sheet.xml │ │ │ ├── fragment_customize_bottom_app_bar.xml │ │ │ ├── fragment_customize_main_page_tabs.xml │ │ │ ├── fragment_fab_more_options_bottom_sheet.xml │ │ │ ├── fragment_filtered_thing_fab_more_options_bottom_sheet.xml │ │ │ ├── fragment_flair_bottom_sheet.xml │ │ │ ├── fragment_followed_users_listing.xml │ │ │ ├── fragment_font_preview.xml │ │ │ ├── fragment_giphy_gif_info_bottom_sheet.xml │ │ │ ├── fragment_history_post.xml │ │ │ ├── fragment_important_info_bottom_sheet.xml │ │ │ ├── fragment_inbox.xml │ │ │ ├── fragment_karma_info_bottom_sheet.xml │ │ │ ├── fragment_moderation_action_bottom_sheet.xml │ │ │ ├── fragment_more_posts_info.xml │ │ │ ├── fragment_multi_reddit_listing.xml │ │ │ ├── fragment_multi_reddit_options_bottom_sheet.xml │ │ │ ├── fragment_new_comment_filter_usage_bottom_sheet.xml │ │ │ ├── fragment_new_post_filter_usage_bottom_sheet.xml │ │ │ ├── fragment_nsfw_and_spoiler.xml │ │ │ ├── fragment_playback_speed.xml │ │ │ ├── fragment_post.xml │ │ │ ├── fragment_post_comment_sort_type_bottom_sheet.xml │ │ │ ├── fragment_post_filter_options_bottom_sheet.xml │ │ │ ├── fragment_post_filter_usage_options_bottom_sheet.xml │ │ │ ├── fragment_post_history.xml │ │ │ ├── fragment_post_layout_bottom_sheet.xml │ │ │ ├── fragment_post_options_bottom_sheet.xml │ │ │ ├── fragment_post_type_bottom_sheet.xml │ │ │ ├── fragment_random_bottom_sheet.xml │ │ │ ├── fragment_search_post_sort_type_bottom_sheet.xml │ │ │ ├── fragment_search_user_and_subreddit_sort_type_bottom_sheet.xml │ │ │ ├── fragment_select_or_capture_image_bottom_sheet.xml │ │ │ ├── fragment_select_subreddits_or_users_options_bottom_sheet.xml │ │ │ ├── fragment_set_as_wallpaper_bottom_sheet.xml │ │ │ ├── fragment_set_reddit_gallery_item_caption_and_url_bottom_sheet.xml │ │ │ ├── fragment_settings_search.xml │ │ │ ├── fragment_share_link_bottom_sheet.xml │ │ │ ├── fragment_sidebar.xml │ │ │ ├── fragment_sort_time_bottom_sheet.xml │ │ │ ├── fragment_sort_type_bottom_sheet.xml │ │ │ ├── fragment_subreddit_listing.xml │ │ │ ├── fragment_subscribed_subreddits_listing.xml │ │ │ ├── fragment_theme_preview_comments.xml │ │ │ ├── fragment_theme_preview_posts.xml │ │ │ ├── fragment_translation.xml │ │ │ ├── fragment_uploaded_images_bottom_sheet.xml │ │ │ ├── fragment_url_menu_bottom_sheet.xml │ │ │ ├── fragment_user_listing.xml │ │ │ ├── fragment_user_thing_sort_type_bottom_sheet.xml │ │ │ ├── fragment_view_imgur_image.xml │ │ │ ├── fragment_view_imgur_video.xml │ │ │ ├── fragment_view_post_detail.xml │ │ │ ├── fragment_view_reddit_gallery_image_or_gif.xml │ │ │ ├── fragment_view_reddit_gallery_video.xml │ │ │ ├── item_acknowledgement.xml │ │ │ ├── item_award.xml │ │ │ ├── item_comment.xml │ │ │ ├── item_comment_filter_usage_embedded.xml │ │ │ ├── item_comment_filter_with_usage.xml │ │ │ ├── item_comment_footer_error.xml │ │ │ ├── item_comment_footer_loading.xml │ │ │ ├── item_comment_fully_collapsed.xml │ │ │ ├── item_crash_report.xml │ │ │ ├── item_custom_theme_color_item.xml │ │ │ ├── item_custom_theme_switch_item.xml │ │ │ ├── item_favorite_thing_divider.xml │ │ │ ├── item_filter_fragment_header.xml │ │ │ ├── item_flair.xml │ │ │ ├── item_footer_error.xml │ │ │ ├── item_footer_loading.xml │ │ │ ├── item_gallery_image_in_post_feed.xml │ │ │ ├── item_load_comments.xml │ │ │ ├── item_load_comments_failed_placeholder.xml │ │ │ ├── item_load_more_comments_placeholder.xml │ │ │ ├── item_markdown_bottom_bar.xml │ │ │ ├── item_message.xml │ │ │ ├── item_multi_reddit.xml │ │ │ ├── item_nav_drawer_account.xml │ │ │ ├── item_nav_drawer_divider.xml │ │ │ ├── item_nav_drawer_menu_group_title.xml │ │ │ ├── item_nav_drawer_menu_item.xml │ │ │ ├── item_nav_drawer_subscribed_thing.xml │ │ │ ├── item_no_comment_placeholder.xml │ │ │ ├── item_paging_3_load_state.xml │ │ │ ├── item_post_card_2_gallery_type.xml │ │ │ ├── item_post_card_2_text.xml │ │ │ ├── item_post_card_2_video_autoplay.xml │ │ │ ├── item_post_card_2_video_autoplay_legacy_controller.xml │ │ │ ├── item_post_card_2_with_preview.xml │ │ │ ├── item_post_card_3_gallery_type.xml │ │ │ ├── item_post_card_3_text.xml │ │ │ ├── item_post_card_3_video_type_autoplay.xml │ │ │ ├── item_post_card_3_video_type_autoplay_legacy_controller.xml │ │ │ ├── item_post_card_3_with_preview.xml │ │ │ ├── item_post_compact.xml │ │ │ ├── item_post_compact_2.xml │ │ │ ├── item_post_compact_2_right_thumbnail.xml │ │ │ ├── item_post_compact_right_thumbnail.xml │ │ │ ├── item_post_detail_gallery.xml │ │ │ ├── item_post_detail_image_and_gif_autoplay.xml │ │ │ ├── item_post_detail_link.xml │ │ │ ├── item_post_detail_no_preview.xml │ │ │ ├── item_post_detail_text.xml │ │ │ ├── item_post_detail_video_and_gif_preview.xml │ │ │ ├── item_post_detail_video_autoplay.xml │ │ │ ├── item_post_detail_video_autoplay_legacy_controller.xml │ │ │ ├── item_post_filter_usage.xml │ │ │ ├── item_post_filter_usage_embedded.xml │ │ │ ├── item_post_filter_with_usage.xml │ │ │ ├── item_post_gallery.xml │ │ │ ├── item_post_gallery_gallery_type.xml │ │ │ ├── item_post_gallery_type.xml │ │ │ ├── item_post_text.xml │ │ │ ├── item_post_video_type_autoplay.xml │ │ │ ├── item_post_video_type_autoplay_legacy_controller.xml │ │ │ ├── item_post_with_preview.xml │ │ │ ├── item_predefined_custom_theme.xml │ │ │ ├── item_private_message_received.xml │ │ │ ├── item_private_message_sent.xml │ │ │ ├── item_recent_search_query.xml │ │ │ ├── item_reddit_gallery_submission_add_image.xml │ │ │ ├── item_reddit_gallery_submission_image.xml │ │ │ ├── item_report_reason.xml │ │ │ ├── item_rule.xml │ │ │ ├── item_selected_subreddit.xml │ │ │ ├── item_settings_search.xml │ │ │ ├── item_shared_comment_row.xml │ │ │ ├── item_subreddit_listing.xml │ │ │ ├── item_subscribed_subreddit_multi_selection.xml │ │ │ ├── item_subscribed_thing.xml │ │ │ ├── item_subscribed_user_multi_selection.xml │ │ │ ├── item_theme_name.xml │ │ │ ├── item_theme_type_divider.xml │ │ │ ├── item_translation_contributor.xml │ │ │ ├── item_trending_search.xml │ │ │ ├── item_uploaded_image.xml │ │ │ ├── item_user_custom_theme.xml │ │ │ ├── item_user_flair.xml │ │ │ ├── item_user_listing.xml │ │ │ ├── item_view_all_comments.xml │ │ │ ├── markdown_image_and_gif_block.xml │ │ │ ├── nav_header_main.xml │ │ │ ├── preference_slider.xml │ │ │ ├── preference_switch.xml │ │ │ ├── shared_comment.xml │ │ │ ├── shared_post.xml │ │ │ ├── shared_post_with_comments.xml │ │ │ └── view_table_entry_cell.xml │ │ ├── layout-land/ │ │ │ ├── activity_customize_comment_filter.xml │ │ │ ├── activity_customize_post_filter.xml │ │ │ ├── activity_lock_screen.xml │ │ │ ├── activity_view_multi_reddit_detail.xml │ │ │ ├── activity_view_subreddit_detail.xml │ │ │ ├── activity_view_user_detail.xml │ │ │ ├── app_bar_main.xml │ │ │ └── fragment_view_post_detail.xml │ │ ├── layout-sw600dp/ │ │ │ ├── activity_customize_comment_filter.xml │ │ │ ├── activity_customize_post_filter.xml │ │ │ ├── activity_view_multi_reddit_detail.xml │ │ │ ├── activity_view_subreddit_detail.xml │ │ │ ├── activity_view_user_detail.xml │ │ │ ├── app_bar_main.xml │ │ │ └── fragment_view_post_detail.xml │ │ ├── menu/ │ │ │ ├── account_posts_activity.xml │ │ │ ├── account_saved_thing_activity.xml │ │ │ ├── activity_settings.xml │ │ │ ├── bottom_app_bar.xml │ │ │ ├── comment_activity.xml │ │ │ ├── crash_reports_fragment.xml │ │ │ ├── create_multi_reddit_activity.xml │ │ │ ├── customize_comment_filter_activity.xml │ │ │ ├── customize_post_filter_activity.xml │ │ │ ├── customize_theme_activity.xml │ │ │ ├── edit_comment_activity.xml │ │ │ ├── edit_multi_reddit_activity.xml │ │ │ ├── edit_post_activity.xml │ │ │ ├── edit_profile_activity.xml │ │ │ ├── filtered_posts_activity.xml │ │ │ ├── full_markdown_activity.xml │ │ │ ├── history_activity.xml │ │ │ ├── history_post_fragment.xml │ │ │ ├── inbox_activity.xml │ │ │ ├── main_activity.xml │ │ │ ├── navigation_rail_menu.xml │ │ │ ├── post_fragment.xml │ │ │ ├── post_gallery_activity.xml │ │ │ ├── post_image_activity.xml │ │ │ ├── post_link_activity.xml │ │ │ ├── post_poll_activity.xml │ │ │ ├── post_text_activity.xml │ │ │ ├── post_video_activity.xml │ │ │ ├── report_activity.xml │ │ │ ├── search_result_activity.xml │ │ │ ├── search_subreddits_result_activity.xml │ │ │ ├── search_users_result_activity.xml │ │ │ ├── selected_subreddits_activity.xml │ │ │ ├── send_private_message_activity.xml │ │ │ ├── submit_crosspost_activity.xml │ │ │ ├── subreddit_multiselection_activity.xml │ │ │ ├── subscribed_thing_listing_activity.xml │ │ │ ├── trending_activity.xml │ │ │ ├── user_multiselection_activity.xml │ │ │ ├── view_image_or_gif_activity.xml │ │ │ ├── view_imgur_image_fragment.xml │ │ │ ├── view_imgur_media_activity.xml │ │ │ ├── view_imgur_video_fragment.xml │ │ │ ├── view_multi_reddit_detail_activity.xml │ │ │ ├── view_post_detail_activity.xml │ │ │ ├── view_post_detail_fragment.xml │ │ │ ├── view_reddit_gallery_activity.xml │ │ │ ├── view_reddit_gallery_image_or_gif_fragment.xml │ │ │ ├── view_reddit_gallery_video_fragment.xml │ │ │ ├── view_subreddit_detail_activity.xml │ │ │ ├── view_user_detail_activity.xml │ │ │ ├── view_video_activity.xml │ │ │ └── web_view_activity.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── raw/ │ │ │ ├── lock_screen.json │ │ │ ├── love.json │ │ │ └── random_subreddit_or_post.json │ │ ├── res/ │ │ │ └── drawable/ │ │ │ └── baseline_comment_24.xml │ │ ├── values/ │ │ │ ├── arrays.xml │ │ │ ├── attr.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── ids.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-cs/ │ │ │ └── strings.xml │ │ ├── values-de/ │ │ │ └── strings.xml │ │ ├── values-el/ │ │ │ └── strings.xml │ │ ├── values-es/ │ │ │ └── strings.xml │ │ ├── values-fr/ │ │ │ └── strings.xml │ │ ├── values-hi/ │ │ │ └── strings.xml │ │ ├── values-hr/ │ │ │ └── strings.xml │ │ ├── values-hu/ │ │ │ └── strings.xml │ │ ├── values-it/ │ │ │ └── strings.xml │ │ ├── values-ja/ │ │ │ └── strings.xml │ │ ├── values-ko/ │ │ │ └── strings.xml │ │ ├── values-land-v28/ │ │ │ └── styles.xml │ │ ├── values-night/ │ │ │ └── colors.xml │ │ ├── values-night-v27/ │ │ │ └── styles.xml │ │ ├── values-night-v31/ │ │ │ └── styles.xml │ │ ├── values-nl/ │ │ │ └── strings.xml │ │ ├── values-pl/ │ │ │ └── strings.xml │ │ ├── values-pt/ │ │ │ └── strings.xml │ │ ├── values-pt-rBR/ │ │ │ └── strings.xml │ │ ├── values-ro/ │ │ │ └── strings.xml │ │ ├── values-ru/ │ │ │ └── strings.xml │ │ ├── values-so/ │ │ │ └── strings.xml │ │ ├── values-sw600dp/ │ │ │ └── attr.xml │ │ ├── values-ta/ │ │ │ └── strings.xml │ │ ├── values-tr-rTR/ │ │ │ └── strings.xml │ │ ├── values-uk/ │ │ │ └── strings.xml │ │ ├── values-v27/ │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── values-v28/ │ │ │ └── styles.xml │ │ ├── values-v31/ │ │ │ └── styles.xml │ │ ├── values-v36/ │ │ │ └── styles.xml │ │ ├── values-vi/ │ │ │ └── strings.xml │ │ ├── values-zh-rCN/ │ │ │ └── strings.xml │ │ ├── xml/ │ │ │ ├── about_preferences.xml │ │ │ ├── activity_motion_test_scene.xml │ │ │ ├── advanced_preferences.xml │ │ │ ├── api_keys_preferences.xml │ │ │ ├── comment_preferences.xml │ │ │ ├── credits_preferences.xml │ │ │ ├── data_saving_mode_preferences.xml │ │ │ ├── debug_preferences.xml │ │ │ ├── download_location_preferences.xml │ │ │ ├── file_paths.xml │ │ │ ├── font_preferences.xml │ │ │ ├── gestures_and_buttons_preferences.xml │ │ │ ├── immersive_interface_preferences.xml │ │ │ ├── interface_preferences.xml │ │ │ ├── item_post_with_preview_scene.xml │ │ │ ├── main_preferences.xml │ │ │ ├── miscellaneous_preferences.xml │ │ │ ├── navigation_drawer_preferences.xml │ │ │ ├── notification_preferences.xml │ │ │ ├── number_of_columns_in_post_feed_preferences.xml │ │ │ ├── post_details_preferences.xml │ │ │ ├── post_preferences.xml │ │ │ ├── proxy_preferences.xml │ │ │ ├── security_preferences.xml │ │ │ ├── sort_type_preferences.xml │ │ │ ├── swipe_action_preferences.xml │ │ │ ├── theme_preferences.xml │ │ │ ├── time_format_preferences.xml │ │ │ └── video_preferences.xml │ │ └── xml-sw600dp/ │ │ └── post_details_preferences.xml │ └── test/ │ └── resources/ │ └── allure.properties ├── build.gradle ├── discoverium.yml ├── fastlane/ │ └── metadata/ │ └── android/ │ └── en-US/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── scripts/ │ ├── release-builds.sh │ └── release-github.sh └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Keep our fork's versions of these files when merging from upstream .github/workflows/build.yml merge=ours .github/workflows/codeql-analysis.yml merge=ours ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: 'bug' assignees: '' --- **Describe the bug** **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** **Screenshots** **Smartphone (please complete the following information):** - Device: [e.g. A72] - OS: [e.g. One UI] - Version: [e.g. 7.4.0.5] **Additional context** ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: 'enhancement' assignees: '' --- **Is your feature request related to a problem? Please describe.** **Describe the solution you'd like** **Describe alternatives you've considered** **Additional context** ================================================ FILE: .github/actions/github-custom-issue-closer ================================================ name: 'Custom Issue Closer' description: 'Automatically close GitHub issues based on custom patterns in commit messages' author: 'Nathan Grennan ' inputs: github_token: description: 'GitHub token for API access' required: true default: ${{ github.token }} pattern: description: 'Custom regex pattern to match in commit messages' required: false default: '[d]?(?:\\s*:)?\\s*#(\\d+)$' runs: using: 'docker' image: 'Dockerfile' args: - ${{ inputs.github_token }} - ${{ inputs.pattern }} branding: icon: 'x-circle' color: 'red' ================================================ FILE: .github/workflows/build.yml ================================================ # This workflow overrides the upstream build.yml for this fork. # It runs a no-op job so GitHub doesn't report "no jobs were run". name: Build on: push: pull_request: workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - run: echo "Build workflow disabled for this fork" ================================================ FILE: .github/workflows/close-issues-custom-pattern.yml ================================================ name: Close Issues via Custom Patterns on: push: branches: - main - master pull_request: types: [closed] branches: - main - master jobs: close-issues: runs-on: ubuntu-24.04 permissions: actions: read issues: write packages: read if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true) steps: - name: Checkout repository uses: actions/checkout@v3 - name: Close issues based on commit messages uses: docker://ghcr.io/cygnusx-1-org/github-custom-issue-closer:0.0.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} # Optional: Override the default regex pattern # pattern: "completed[\\s:]+#(\\d+)" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/androidstudio # Edit at https://www.gitignore.io/?templates=androidstudio ### AndroidStudio ### # Covers files to be ignored for android development using Android Studio. # Built application files *.apk *.ap_ *.aab # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ /app/release /app/minifiedRelease # Gradle files .gradle/ build/ # Signing files .signing/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio local.properties /*/out /*/*/production captures/ .navigation/ *.ipr *~ *.swp # Android Patch gen-external-apklibs # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # NDK obj/ **/ndkHelperBin **/.cxx # IntelliJ IDEA *.iml *.iws /out/ # User-specific configurations .idea/ # OS-specific files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # Legacy Eclipse project files .classpath .project .cproject .settings/ # Mobile Tools for Java (J2ME) .mtj.tmp/ # Visual Studio Code .vscode/ # Package Files # *.war *.ear # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) hs_err_pid* ## Plugin-specific files: # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ### AndroidStudio Patch ### !/gradle/wrapper/gradle-wrapper.jar # Lint file app/lint-baseline.xml keystore.properties test.properties backup/ # Github token github.properties # End of https://www.gitignore.io/api/androidstudio ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG --- 8.1.4.5 / 2026-4-9 =========== Note v8a is the 64-bit build, and should be considered the default choose. * Fixed issue with the Reddit account's interface language being anything other than English at WebView login. Thanks to @wchill * Fixed Jump to Next Top-level Comment #244 8.1.4.4 / 2026-4-3 =========== Note v8a is the 64-bit build, and should be considered the default choose. * Added support for configuring the user agent and redirect uri directly in the app * Added support for logging in with Firefox as the external browser 8.1.4.3 / 2026-3-31 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Fixed WebView login 8.1.4.2 / 2026-3-27 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Added "Share as Image with Comments" and "Share as Image with this Comment Thread" * Fixed the colors of the "Share as Image" QR codes to always be white/black for usability with the Android Camera app 8.1.4.1 / 2026-3-27 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Updated to 8.1.4 * Added the ability to search Settings * Added post ids or post ids + comment ids to saved filenames 8.1.3.1 / 2026-3-16 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Fixed Link in comment doesnt render/link properly #238 * Fixed Ability to swap both the content and comments on Fold when unfolded #232 * Improved the user experience when adding a user to a multi-reddit 8.1.2.4 / 2026-3-6 =========== Note v8a is the 64-bit build, and should be considered the default choose. * Fixed issue with preview image quality in the split view 8.1.2.3 / 2026-3-4 =========== Note v8a is the 64-bit build, and should be considered the default choose. * Simplified adding a user profile to a multi-reddit * Fixed Add a multireddit description page #230 * Moved "Enable folding phone support" to "Settings | Miscellaneous" * Added "Default Post Layout for a Foldable Unfolded" in "Settings | Interface | Post" * Fixed Some gifs in comments fail to embed #227 * Fixed bug with uploading an image into a comment 8.1.2.2 / 2026-2-21 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Added retry logic to 500 errors when loading posts from a subreddit * Made subredditAPICallLimit always return 100 * Reverted halve the limit button 8.1.2.1 / 2026-2-18 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Added feature to allow the user to "Halve the post limit" on failure * Upgraded to 8.1.2 8.1.1.1 / 2026-2-12 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Fixed Gifs no longer showing up in comments #217 * Upgraded to 8.1.1 8.1.0.2 / 2026-1-23 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Fixed crash on start with 8.1.0.1 8.1.0.1 / 2026-1-23 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Added Remember Comment Scroll Position in Settings | Interface | Comment, disabled by default * Upgraded to 8.1.0 8.0.8.1 / 2025-11-16 ============= Note v8a is the 64-bit build, and should be considered the default choose. * Fixed Bottom Navigation Bar #196 * Fixed Video player control is not aligned properly #198 * Upgraded to 8.0.8 8.0.7.3 / 2025-11-9 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Fixed App closes when I try to open a post with a video #189 * Fixed issue with post image recycling while scrolling for auto-played video posts 8.0.7.2 / 2025-11-7 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Updated media3-exoplayer to 1.8.0 * Having Video issue on Huawei #186 8.0.7.1 / 2025-11-5 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Fixed Confirm to exit doesn't really exit #154 * Upgraded to 8.0.7 8.0.5.1 / 2025-10-17 ============= Note v8a is the 64-bit build, and should be considered the default choose. * Make double tapping tabs cause a scroll to the top (in Subreddit view) * Add contain subreddits/users in postFilter * Upgraded to 8.0.5 8.0.3.1 / 2025-9-17 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Ensure `mark posts as read` is also turned on before hiding read posts * Don't show the number of online subscribers in ViewSubredditDetailAct, aka the comments not loading * Always show read posts on any users page * Hide the + button on PostFilterUsageListingActivity as it does nothing * Upgraded to 8.0.3, aka Redgif sound 8.0.2.1 / 2025-9-8 =========== Note v8a is the 64-bit build, and should be considered the default choose. * Keep pinned posts pinned even when filtering or hiding read posts * Make double tapping tabs cause a scroll to the top #148 * Fix the + button in apply post filter not working correctly * Upgraded to 8.0.2 8.0.0.1 / 2025-8-21 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Re-enabled redgifs * Upgraded to 8.0.0 * Fix No sound in redgifs #129 7.5.1.3 / 2025-8-8 =========== Note v8a is the 64-bit build, and should be considered the default choose. * Added contains subreddit and users post filters #112 * Added thumbnails for crossposts * Fixed crash when trying to select the camera with no permission * Fixed Can't post my own GIFs #100 * Fixed Reduce the size of the placeholder preview image #53 - If you don't like the divide line for posts in the compact mode, "Settings | Interface | Post | Compact Layout | Show Divider" * Fixed "Remove Alls" -> "Remove All" #81 * Fixed Swap taps and long press in comments #111 * Fixed Duplicate image title downloads #101 7.5.1.2 / 2025-6-16 ============ * Ability to hide user prefix in comments * Fixed Make it so the user can choose the password for the backup and enter it for the restore #83 * Removed padding from comments to make them more compact * Merged "Keep screen on when autoplaying videos" from upstream * Fixed Download issue #98 * Fixed crash from not having a video location set 7.5.1.1 / 2025-5-28 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Synced with upstream to 7.5.1 * Fixed Bug: Failed to download Tumblr videos #48 * Fxied App crashes while trying to download an image #78 * Fixed Crash when trying to download an image without the download location set #77 * Fixed 4x All as default #75 7.5.0.2 / 2025-5-24 ============ Note v8a is the 64-bit build, and should be considered the default choose. * Reverted "* Fixed Reduce the size of the placeholder preview image #53" * Fixed NSFW toggle button #73 via reverting the change by Infinity * Fixed Please add the ability to have extra tabs at the top of the main page #64 * Changing app/build.gradle to build v7a and v8a builds 7.5.0.1 / 2025-5-12 ============ * Fixed Downloaded video contains no sound #57 * Fixed Reduce the size of the placeholder preview image #53 * Synced with upstream to 7.5.0 7.4.4.4 / 2025-4-20 ============ * Added support for inputting the client ID via a QR code * Added child comment count next to comment score when the comment is collapsed * Fixed Make the comments more compact #18 * Enabled "Swap Tap and Long Press Comments" by default * Fixed Incorrect FAB icon and action in bottom bar customization #39 * Fixed Sensible download names #13 * Fixed Simplify download path #38 7.4.4.3 / 2025-4-16 =================== * Changed internal name to org.cygnusx1.continuum #7 * Added support for Giphy API Key #20 * Changed the beginning of the backup filename to Continuum * Fixed Rename all instances of Sensitive Content to NSFW #19 * Removed toggle to allow not backing up accounts and api keys #21 #22 * Fixed “Swipe vertically to go back” still active on gifs and videos when disabled #6 * Removed all references to random since Reddit removed it #11 7.4.4.2 / 2025-4-14 =================== * Added support for backing up and restoring all settings other than Security * Added toggle to backup accounts and the client id, and it is enabled by default * Removed rate this app in About * Fixed link to subreddit in About * Removed more branding 7.4.4.1 / 2025-4-10 =================== * Initial release based on Infinity for Reddit * Removed most of the Infinity for Reddit branding * Added a new icon * Changed the user agent and redirect URL * Added a dynamic Client ID setting * Added Solarized Amoled theme ================================================ FILE: FAQ.md ================================================ # Frequently asked questions 1. Why can't I login? There can be issues with `Android System Webview`. See [Common errors](/SETUP.md#common-errors) in [SETUP.md](/SETUP.md). There is a known issue with the `Reddit` login JavaScript. It was discovered by wchill. It prevents logging in when the language if the language is anything other than `English`. So you need to set the language of your `Reddit` account to `English`. I have implemented a hack to workaround the issue in `8.1.4.5`, but it has a few issues. 1. It could break at anytime if `Reddit` rewrites their JavaScript. 2. The hack only works in the WebView not Chrome, Brave or Firefox. ## How to change the Reddit account interface language 1. Go to https://old.reddit.com/prefs/ 2. Change the `interface language`

Sometimes there can be popups on the login page, like a cookies popup, that block the keyboard appearing. Real browsers deal with these better than `Android System WebView`. There is also a button in the bottom right of the login screen that lets you use your default browser instead of `Android System WebView`. But the browser needs to be `Chrome` based like `Chrome`, `Chromium`, or `Brave` to work. It has been tested with `Firefox`, and it doesn't work. Using a VPN from a location other than your actual location, or if already using a VPN, not using a VPN. You may also get difference results on different internet connections, like your home, friend's home, grocery store, work, etc. Also if wifi isn't working try the cell connection. If the cell connection isn't working, try wifi. 2. Why can't I view `NSFW` content even through I have enabled the settings in `Settings | Content NSFW Filter`? The behavior of this seems to be per account. The known fix, when it is an issue, is to make a subreddit via the website as the user you want to use in `Continuum`. This makes you a `moderator` of your new subreddit. This seems to set a flag on the account that then solves the problem. ================================================ FILE: GIPHY.md ================================================ # Giphy API key creation Continuum has to use a BYOK(Bring your own key) model. Continuum is open source, and [Giphy](https://giphy.com/)(now owned by [Shutterstock](https://www.shutterstock.com/)) charges crazy money for apps to use their API. This is very akin to [Reddit](https://www.reddit.com/) API costs, but much worse. ## Instructions 1. Go [here](https://developers.giphy.com/) 2. Click the `Create Account` button drawing 3. Click `Sign Up` and create an account if needed, otherwise `Log In` drawing 4. Click the `Create an API Key` button drawing 5. Click `Select API` to pick `API` instead of `SDK` 6. Click the `Next Step` button drawing 7. Set `Your App Name` to `Continuum` 8. Set `Platform` to `Android` 9. Set the `App Description` to `Reddit Android client with Giphy support` 10. Check the box for `By checking this box, I am confirming that I have read & agree to the GIPHY API Terms` 11. Click the `Create API Key` button drawing You should theen see a box like this on the page with your API key. ![API Key](assets/screenshots/giphy_api_key.png) > [!NOTE] > > **kTWpBT9Zc5iZw8lZ0dTUcTukCwVHftSK** was a real API key, but has > since been deleted. It won't work for you, so don't try it. ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================

Continuum


   

A Reddit client on Android written in Java. It does not have any ads and it features a clean UI and smooth browsing experience
[Releases](https://github.com/cygnusx-1-org/continuum/releases) [License](https://github.com/cygnusx-1-org/continuum/blob/master/LICENSE) [GitHub issues](https://github.com/cygnusx-1-org/continuum/issues)
--- # Fork This project is a fork of [Infinity for Reddit](https://github.com/Docile-Alligator/Infinity-For-Reddit). One major enhancement is that it lets you set your own `Client ID` as a setting. This means you don't need to recompile it each time, or to use [ReVanced Manager](https://github.com/ReVanced/revanced-manager). # Setup See [SETUP.md](/SETUP.md) # FAQ See [FAQ.md](/FAQ.md) # Installation You can easily install and update Continuum with [Discoverium](https://github.com/cygnusx-1-org/Discoverium/) via its search button.

Get it on Discoverium     Get it on Github

# About The Project Unique features of **Continuum**: - Ability to use your own `Client ID`. - Ability to use your own Giphy gifs API key. - Ability to backup your accounts. - The max number of main page tabs has been increased to six. - Sensible download names. - Bug fixes and more...
Features from Infinity * Lazy mode: Automatic scrolling of posts enables you to enjoy amazing posts without moving your thumb. * Browsing posts * View comments * Expand and collapse comments section * Vote posts and comments * Save posts * Write comments * Edit comments and delete comments * Submit posts (text, link, image and video) * Edit posts (mark and unmark NSFW and spoiler and edit flair) and delete posts * See all the subscribed subreddits and followed users * View the messages * Get notifications of unread messages * etc...

(back to top)

# Contributing First off, thanks for taking the time to contribute! Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you have a suggestion that would make this better, please fork the repo and create a pull request. It's better to also open an issue describing the issue you want to fix. But it is not required. Don't forget to give the project a star! Thanks again! 1. Fork the Project 2. Create your Feature Branch from `master` (`git checkout -b feature/AmazingFeature`) 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 4. Push to the Branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request to the `master` Branch Here are other ways you can help: - [Report Bugs](https://github.com/cygnusx-1-org/continuum/issues/new?template=bug_report.md) - [Request Features](https://github.com/cygnusx-1-org/continuum/issues/new?template=feature_request.md)

(back to top)

# Related project [Slide](https://github.com/cygnusx-1-org/Slide) is another Android [Reddit](https://www.reddit.com/) client app. It is a fork of the original project. It is also in the [Google Play Store](https://play.google.com/store/apps/details?id=me.edgan.redditslide&hl=en_US). # License Distributed under the AGPL-3.0 License. See LICENSE for more information.

(back to top)

# Contact [u/edgan](https://www.reddit.com/user/edgan) - continuum@cygnusx-1.org (Owner) Project Link: [https://github.com/cygnusx-1-org/continuum](https://github.com/cygnusx-1-org/continuum)

(back to top)

================================================ FILE: SETUP-old.md ================================================ # Setup ## Giphy See [here](/GIPHY.md). ## Reddit Client ID A Reddit Client ID is needed to access Reddit from 3rd party clients. ### Reddit Client ID creation steps ![Create application](assets/screenshots/create_application.png) 1. Go to [reddit.com/prefs/apps](https://www.reddit.com/prefs/apps) and login if necessary 2. Click `create another app...`. Do not re-use any Client ID for any app other than Continuum. 3. Set the name to Continuum 4. Set the type to `installed app` 5. Set redirect uri to `continuum://localhost`. If the redirect uri is set incorrectly it won't work. 6. Complete the `reCAPTCHA` 7. Click `create app` 8. Copy the Client ID of your newly created app. It is recommended to save it in the notes of your entry for Reddit in your password manager. ![Client ID](assets/screenshots/client_id.png) > [!NOTE] > > This is just an example Client ID. It was created and deleted. Keep > yours private. ### Adding a Reddit Client ID to Continuum The method of adding a Client ID to Continuum depends on whether this is the first time the app is being set up. **Initial setup:** 1. Open Continuum and press `GET STARTED` 2. Select your theme colors, if you like, and press `DONE` 3. Enter your Client ID and press `OK` 4. Wait for Continuum to restart **Changing the Client ID:** 1. Go to `Settings in the side bar` drawing 2. Select `Reddit API Client ID` drawing 3. Press `Reddit API Client ID` drawing 4. Enter your Client ID drawing It is best to copy and paste it. 5. Press `OK` and Wait for Continuum to restart # Common errors The most likely cause for this is the `redirect uri` is set incorrectly. The big tell is if you can view Reddit in guest mode, aka without logging in. ## Correct username and password does not work Continuum depends on [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) by default for logging into Reddit. So if having the login issue, your best course of action would be to upgraded to the latest version of Android possible, and then the latest version of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) possible. Reddit's login password now requires [XHR](https://en.wikipedia.org/wiki/XMLHttpRequest) to work. Older versions of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) don't support [XHR](https://en.wikipedia.org/wiki/XMLHttpRequest). The current and known good version of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) is `131.0.6778.135`. ### WebView updating Updating [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) can be tricky. You likely can't search and see it in the [Google Play Store](https://play.google.com/store/games) app on your phone. The best way is to find the app in the `Apps` section of `Settings`. The search box in the top right can make it easier to find in the long list of apps. Once you select [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) go to the bottom. You can see your version. Click on `App details`. This will take you directly to [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) listing in the [Google Play Store](https://play.google.com/store/games) app. If there is an update available it will be shown. ### Alternative versions of WebView There are [Dev](https://play.google.com/store/apps/details?id=com.google.android.webview.dev), [Beta](https://play.google.com/store/apps/details?id=com.google.android.webview.beta), and [Canary](https://play.google.com/store/apps/details?id=com.google.android.webview.canary) versions of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview). These aren't recommended, but under rare circumstances they might be useful to get a newer version of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview). Once installed you need to enable [Developer options](https://developer.android.com/studio/debug/dev-options), you can go to them in `Settings`. Within is an option called `WebView implementation` where you can pick which `WebView` is active. ================================================ FILE: SETUP.md ================================================ # Setup ## Setup for older versions See [SETUP-old.md](/SETUP-old.md) ## Giphy See [here](/GIPHY.md). ## Reddit Client ID A Reddit Client ID is needed to access Reddit from 3rd party clients. ### Reddit Client ID creation steps > [!IMPORTANT] > > Reddit has recently changed the guidelines to create api key > [see here](https://www.reddit.com/r/redditdev/comments/1oug31u/introducing_the_responsible_builder_policy_new/) > we are not sure how it wll affect the app in the future but currently it seems most users are unable to create new api keys ![Create application](assets/screenshots/create_application.png) 1. Go to [reddit.com/prefs/apps](https://www.reddit.com/prefs/apps) and login if necessary 2. Click `create another app...`. Do not re-use any Client ID for any app other than Continuum. 3. Set the name to Continuum 4. Set the type to `installed app` 5. Set redirect uri to `continuum://localhost`. If the redirect uri is set incorrectly it won't work. 6. Complete the `reCAPTCHA` 7. Click `create app` 8. Copy the Client ID of your newly created app. It is recommended to save it in the notes of your entry for Reddit in your password manager. ![Client ID](assets/screenshots/client_id.png) > [!NOTE] > > This is just an example Client ID. It was created and deleted. Keep > yours private. > > *It is reccomended to create the Client ID using a non banned account.* ### Adding a Reddit Client ID to Continuum The method of adding a Client ID to Continuum depends on whether this is the first time the app is being set up. **Initial setup:** 1. Open Continuum and press `GET STARTED` 2. Select your theme colors, if you like, and press `DONE` 3. Enter your Client ID and press `OK` 4. Wait for Continuum to restart **Changing the Client ID:** 1. Go to `Settings in the side bar` drawing 2. Select `Reddit API Client ID` drawing 3. Press `Reddit API Client ID` drawing 4. Enter your `Client ID` drawing It is best to copy and paste it. 6. Press `OK` and Wait for Continuum to restart > [!NOTE] > > *This is only needed once, even if you have multiple accounts.* # Common errors > [!NOTE] > > *The most likely cause for this is the `redirect uri` is set incorrectly. The > big tell is if you can view Reddit in guest mode, aka without logging in.* ### Correct username and password does not work Continuum depends on [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) by default for logging into Reddit. So if having the login issue, your best course of action would be to upgraded to the latest version of Android possible, and then the latest version of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) possible. Reddit's login password now requires [XHR](https://en.wikipedia.org/wiki/XMLHttpRequest) to work. Older versions of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) don't support [XHR](https://en.wikipedia.org/wiki/XMLHttpRequest). The current and known good version of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) is `131.0.6778.135`. #### WebView updating Updating [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) can be tricky. You likely can't search and see it in the [Google Play Store](https://play.google.com/store/games) app on your phone. The best way is to find the app in the `Apps` section of `Settings`. The search box in the top right can make it easier to find in the long list of apps. Once you select [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) go to the bottom. You can see your version. Click on `App details`. This will take you directly to [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview) listing in the [Google Play Store](https://play.google.com/store/games) app. If there is an update available it will be shown. #### Alternative versions of WebView There are [Dev](https://play.google.com/store/apps/details?id=com.google.android.webview.dev), [Beta](https://play.google.com/store/apps/details?id=com.google.android.webview.beta), and [Canary](https://play.google.com/store/apps/details?id=com.google.android.webview.canary) versions of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview). These aren't recommended, but under rare circumstances they might be useful to get a newer version of [Android System Webview](https://play.google.com/store/apps/details?id=com.google.android.webview). Once installed you need to enable [Developer options](https://developer.android.com/studio/debug/dev-options), you can go to them in `Settings`. Within is an option called `WebView implementation` where you can pick which `WebView` is active. ================================================ FILE: TESTS.md ================================================ # Tests The tests were started by [manunia](https://github.com/manunia), and are written in [Kotlin](https://en.wikipedia.org/wiki/Kotlin_(programming_language)) with the [Kaspresso](https://github.com/KasperskyLab/Kaspresso) framework. Kaspresso is based on [Espresso](https://developer.android.com/training/testing/espresso) and [UI Automator](https://developer.android.com/training/testing/ui-automator). ## Setup test.properties: ``` REDDIT_CLIENT_ID=4vj5qn9RkC4i3WLv5OsuFb ``` ## Usage This has been tested with a real phone connected via a USB cable. I needs to be unlocked and USB debugging allowed. It will very likely work with an emulator. It installs a debug build of `Continuum` as the first step. Then it runs the tests, and then uninstalls the debug build of `Continuum`. This means it will replace your "normal" debug build, and wipe all it's settings. It also means tests need to be written to expect a fresh install each time. This means dealing with the `client ID`, system dialogs like permissions, etc. ``` ./gradlew connectedAndroidTest ``` ### Example output ``` > Task :app:connectedDebugAndroidTest Starting 8 tests on Pixel 8 Pro - 16 Pixel 8 Pro - 16 Tests 0/8 completed. (0 skipped) (0 failed) Pixel 8 Pro - 16 Tests 1/8 completed. (0 skipped) (0 failed) Pixel 8 Pro - 16 Tests 2/8 completed. (0 skipped) (0 failed) Pixel 8 Pro - 16 Tests 3/8 completed. (0 skipped) (0 failed) Pixel 8 Pro - 16 Tests 4/8 completed. (0 skipped) (0 failed) Pixel 8 Pro - 16 Tests 5/8 completed. (0 skipped) (0 failed) Pixel 8 Pro - 16 Tests 6/8 completed. (0 skipped) (0 failed) Pixel 8 Pro - 16 Tests 7/8 completed. (0 skipped) (0 failed) Finished 8 tests on Pixel 8 Pro - 16 BUILD SUCCESSFUL in 2m 19s 68 actionable tasks: 1 executed, 67 up-to-date ``` ## Lists of tests * APIKeysTest.kt - addRedditClientIdTest, tests the ability to enter a `client ID` * LoginTest.kt - loginTest, tests that the `Email or username` field is avaliable in the login screen * MainTest.kt - popularPostFilterTest, tests post filters by toggling the `Text` and `Link` off * SettingsTest.kt - setFontTest, tests setting a font - disableNotificationTest, tests the ability to disable notifications - setLightThemeTest, tests the ability to switch to the light theme - setDarkThemeTest, tests the ability to switch to the dark theme ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'com.gladed.androidgitversion' version '0.4.14' id 'com.github.ben-manes.versions' version '0.42.0' id 'com.github.breadmoirai.github-release' version '2.5.2' id 'com.diffplug.spotless' version '7.0.2' id 'org.jetbrains.kotlin.android' id("io.qameta.allure") version "2.12.0" id 'org.jetbrains.kotlin.kapt' id 'kotlin-parcelize' id("org.jetbrains.kotlin.plugin.compose") version "2.2.0" } def allureVersion = "2.16.0" def aspectJVersion = '1.9.21' configurations { agent { canBeResolved = true canBeConsumed = true } all { exclude module: 'httpclient' } } android { def keystorePropertiesFile = rootProject.file("keystore.properties") if (keystorePropertiesFile.exists()) { def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] } } } else { println("Warning: keystore.properties file not found. Skipping signing configuration.") } def testPropertiesFile = rootProject.file("test.properties") def testProperties = new Properties() if (testPropertiesFile.exists()) { testProperties.load(new FileInputStream(testPropertiesFile)) } else { println("Warning: test.properties file not found. Using default test values.") } applicationVariants.all { variant -> variant.outputs.all { output -> def appName = "continuum" def baseAbiVersion = output.getFilter(com.android.build.OutputFile.ABI) def buildType = variant.buildType def versionCode = variant.versionCode def versionName = variant.versionName def artifactName = "${appName}-${versionName}(${versionCode})" if (buildType == "debug") { artifactName = "${appName}-${buildType}-${baseAbiVersion}-${versionName}" } else { artifactName = "${appName}-${baseAbiVersion}-${versionName}" } // Assign the apk filename output.outputFileName = "${artifactName}.apk" } } compileSdk 36 defaultConfig { applicationId "org.cygnusx1.continuum" javaCompileOptions { annotationProcessorOptions { arguments = [eventBusIndex: 'ml.docilealligator.infinityforreddit.EventBusIndex'] } } minSdk 21 targetSdk 35 versionCode 213 versionName "8.1.4.5" testInstrumentationRunner "com.kaspersky.kaspresso.runner.KaspressoRunner" testInstrumentationRunnerArguments.put("REDDIT_CLIENT_ID", testProperties['REDDIT_CLIENT_ID'] ?: 'test_reddit_client_id_default') } buildFeatures { buildConfig = true viewBinding = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' if (keystorePropertiesFile.exists()) { signingConfig = signingConfigs.release } else { println("Warning: keystore.properties file not found. Skipping signing configuration for release.") } minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' resValue "string", "app_name", "Continuum" } minifiedRelease { initWith buildTypes.release zipAlignEnabled true minifyEnabled true shrinkResources = true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { applicationIdSuffix '.debug' versionNameSuffix '-debug' resValue "string", "app_name", "Continuum Debug" } } bundle { language { enableSplit = false } } compileOptions { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 } testOptions { animationsDisabled = true //execution 'ANDROIDX_TEST_ORCHESTRATOR' unitTests.returnDefaultValues = true unitTests.all { useJUnitPlatform() } } buildFeatures { buildConfig = true viewBinding = true compose = true } lint { baseline = file("lint-baseline.xml") disable 'MissingTranslation' } namespace = 'ml.docilealligator.infinityforreddit' splits { abi { enable = true reset() include 'armeabi-v7a', 'arm64-v8a' universalApk = false // Set to true if you also want a universal APK } } composeOptions { kotlinCompilerExtensionVersion = "1.5.14" } kotlinOptions { jvmTarget = '21' } } def githubPropertiesFile = rootProject.file("github.properties") if (githubPropertiesFile.exists()) { def githubProperties = new Properties() githubProperties.load(new FileInputStream(githubPropertiesFile)) githubRelease { def releaseNotes = System.getenv("RELEASE_NOTES") ?: "Automated release of version ${android.defaultConfig.versionName ?: "default-version"}" token = githubProperties["githubToken"] owner = "cygnusx-1-org" repo = "continuum" tagName = android.defaultConfig.versionName ?: "default-version" targetCommitish = "master" releaseName = "Release ${android.defaultConfig.versionName ?: "default-version"}" body = "$releaseNotes" draft = false prerelease = false overwrite = false // Dynamically set releaseAssets by searching the APK path def apkFiles = fileTree(dir: "build/outputs/apk/release/", include: "*.apk").files.toList() if (apkFiles.isEmpty()) { println("Warning: No APK files found in the directory 'build/outputs/apk/release/'.") } else { releaseAssets = apkFiles } } } else { println("Warning: github.properties file not found. Skipping GitHub release configuration.") } dependencies { /** AndroidX **/ implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'androidx.biometric:biometric:1.2.0-alpha05' implementation 'androidx.browser:browser:1.9.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.activity:activity:1.10.1' implementation 'androidx.fragment:fragment:1.8.8' implementation 'androidx.core:core-ktx:1.17.0' def lifecycleVersion = "2.9.4" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion" def pagingVersion = '3.3.6' implementation "androidx.paging:paging-runtime:$pagingVersion" implementation "androidx.paging:paging-guava:$pagingVersion" implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.4.0' def roomVersion = "2.7.2" implementation "androidx.room:room-runtime:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" implementation "androidx.room:room-guava:$roomVersion" implementation "androidx.room:room-ktx:$roomVersion" implementation 'androidx.viewpager2:viewpager2:1.1.0' implementation 'androidx.work:work-runtime:2.9.0' implementation 'com.google.android.material:material:1.14.0-alpha01' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-beta01" /** ExoPlayer **/ def media3_version = "1.8.0" implementation "androidx.media3:media3-exoplayer:$media3_version" implementation "androidx.media3:media3-exoplayer-dash:$media3_version" implementation "androidx.media3:media3-exoplayer-hls:$media3_version" implementation "androidx.media3:media3-ui:$media3_version" implementation "androidx.media3:media3-exoplayer-smoothstreaming:$media3_version" implementation "androidx.media3:media3-datasource-okhttp:$media3_version" /** Third-party **/ /**** Backend logic ****/ implementation 'javax.annotation:javax.annotation-api:1.3.2' implementation 'com.google.code.findbugs:jsr305:3.0.2' // HTTP clients def retrofitVersion = '3.0.0' implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-scalars:$retrofitVersion" implementation "com.squareup.retrofit2:adapter-guava:$retrofitVersion" implementation 'com.squareup.okhttp3:okhttp:5.3.0' // Dependency injection def daggerVersion = '2.59' implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" // Binding compileOnly 'com.android.databinding:viewbinding:8.5.1' // Events def eventbusVersion = "3.3.1" implementation "org.greenrobot:eventbus:$eventbusVersion" kapt "org.greenrobot:eventbus-annotation-processor:$eventbusVersion" // TransactionTooLargeException avoidance implementation 'com.github.livefront:bridge:v2.0.2' // Bundle-saving without boilerplate // NOTE: Deprecated def stateVersion = "1.4.1" implementation "com.evernote:android-state:$stateVersion" kapt "com.evernote:android-state-processor:$stateVersion" // Object to JSON // NOTE: Replace with Squareup's Moshi? implementation 'com.google.code.gson:gson:2.11.0' // Java library for zip files and streams implementation 'net.lingala.zip4j:zip4j:2.11.5' // IO functionality implementation 'commons-io:commons-io:2.16.1' // Crash reporting implementation 'com.github.FunkyMuse:Crashy:1.2.0' /**** User Interface (frontend) ****/ //Image loading def glideVersion = "5.0.5" implementation "com.github.bumptech.glide:glide:$glideVersion" kapt "com.github.bumptech.glide:compiler:$glideVersion" implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation "com.github.bumptech.glide:compose:1.0.0-beta08" implementation 'com.github.santalu:aspect-ratio-imageview:1.0.9' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.29' // APNG animation support for profile avatars implementation 'com.github.penfeizhou.android.animation:glide-plugin:3.0.5' def bivVersion = "1.8.1" implementation "com.github.piasy:BigImageViewer:$bivVersion" implementation "com.github.piasy:GlideImageLoader:$bivVersion" // Markdown def markwonVersion = "4.6.2" implementation "io.noties.markwon:core:$markwonVersion" implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" implementation "io.noties.markwon:linkify:$markwonVersion" implementation "io.noties.markwon:recycler-table:$markwonVersion" implementation "io.noties.markwon:simple-ext:$markwonVersion" implementation "io.noties.markwon:inline-parser:$markwonVersion" implementation "io.noties.markwon:image-glide:$markwonVersion" implementation 'com.atlassian.commonmark:commonmark-ext-gfm-tables:0.14.0' implementation 'me.saket:better-link-movement-method:2.2.0' // Animations implementation 'com.airbnb.android:lottie:6.4.1' // Loading ProgressBar implementation 'com.lsjwzh:materialloadingprogressbar:0.5.8-RELEASE' // Customizable TextView implementation files("Modules/customtextview-2.1.aar") // Dismiss gesturing implementation 'app.futured.hauler:hauler:5.0.0' // FlowLayout (auto-spacing) implementation 'com.nex3z:flow-layout:1.3.3' // RecyclerView fast scrolling implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'com.otaliastudios:zoomlayout:1.9.0' implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'com.giphy.sdk:ui:2.3.18' // QR code scanner implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'com.github.alexzhirkevich:custom-qr-generator:2.0.0-alpha01' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' // Compose def composeBom = platform('androidx.compose:compose-bom:2025.11.01') implementation composeBom androidTestImplementation composeBom implementation 'androidx.compose.material3:material3:1.5.0-alpha01' implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' //implementation 'androidx.compose.material3.adaptive:adaptive' implementation 'androidx.activity:activity-compose:1.10.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4' implementation 'androidx.compose.runtime:runtime-livedata' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' /**** Builds and flavors ****/ // debugImplementation because LeakCanary should only run in debug builds. //debugImplementation 'com.squareup.leakcanary:leakcanary-android:x.y' testImplementation platform("io.qameta.allure:allure-bom:$allureVersion") testImplementation "io.qameta.allure:allure-junit5" agent "org.aspectj:aspectjweaver:$aspectJVersion" androidTestImplementation "androidx.test.ext:junit:1.0.0" androidTestImplementation "androidx.test:rules:1.5.0" androidTestImplementation "androidx.test:runner:1.5.0" androidTestImplementation 'com.kaspersky.android-components:kaspresso:1.5.3' androidTestUtil "androidx.test:orchestrator:1.5.1" // Allure support androidTestImplementation "com.kaspersky.android-components:kaspresso-allure-support:1.5.3" } spotless { java { target '**/*.java' removeUnusedImports() trimTrailingWhitespace() leadingTabsToSpaces(4) } format 'misc', { target '**/*.gradle', '**/*.md', '**/.gitignore' leadingTabsToSpaces(4) trimTrailingWhitespace() } format 'xml', { target '**/*.xml' leadingTabsToSpaces(4) trimTrailingWhitespace() } } ================================================ FILE: app/proguard-rules.pro ================================================ # 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 ## Preferences reflection -keep class * extends androidx.preference.PreferenceFragmentCompat -keep public class ml.docilealligator.infinityforreddit.settings.** ## EventBus Rules -keepattributes *Annotation* -keepclassmembers class * { @org.greenrobot.eventbus.Subscribe ; } -keep enum org.greenrobot.eventbus.ThreadMode { *; } # And if you use AsyncExecutor: -keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent { (java.lang.Throwable); } -keepclassmembernames class com.google.android.exoplayer2.ui.PlayerControlView { java.lang.Runnable hideAction; void hideAfterTimeout(); } # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and # EnclosingMethod is required to use InnerClasses. -keepattributes Signature, InnerClasses, EnclosingMethod # Retrofit does reflection on method and parameter annotations. -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations # Retain service method parameters when optimizing. -keepclassmembers,allowshrinking,allowobfuscation interface * { @retrofit2.http.* ; } # Ignore annotation used for build tooling. -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement # Ignore JSR 305 annotations for embedding nullability information. -dontwarn javax.annotation.** # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy # and replaces all potential values with null. Explicitly keeping the interfaces prevents this. -if interface * { @retrofit2.http.* ; } -keep,allowobfuscation interface <1> # Gson uses generic type information stored in a class file when working with fields. Proguard # removes such information by default, so configure it to keep all of it. -keepattributes Signature # For using GSON @Expose annotation -keepattributes *Annotation* # Gson specific classes -dontwarn sun.misc.** #-keep class com.google.gson.stream.** { *; } # Application classes that will be serialized/deserialized over Gson -keep class ml.docilealligator.infinityforreddit.customtheme.CustomTheme { ; } -keep class ml.docilealligator.infinityforreddit.multireddit.MultiRedditJSONModel { ; } -keep class ml.docilealligator.infinityforreddit.multireddit.SubredditInMultiReddit { ; } -keep class ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData { ; } -keep class ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData { ; } -keep class ml.docilealligator.infinityforreddit.multireddit.MultiReddit { ; } -keep class ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubreddit { ; } -keep class ml.docilealligator.infinityforreddit.postfilter.PostFilter { ; } -keep class ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage { ; } -keep class ml.docilealligator.infinityforreddit.commentfilter.CommentFilter { ; } -keep class ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage { ; } # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) -keep class * extends com.google.gson.TypeAdapter -keep class * implements com.google.gson.TypeAdapterFactory -keep class * implements com.google.gson.JsonSerializer -keep class * implements com.google.gson.JsonDeserializer # Prevent R8 from leaving Data object members always null -keepclassmembers,allowobfuscation class * { @com.google.gson.annotations.SerializedName ; } -dontwarn kotlinx.parcelize.Parcelize ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/CustomizePostFilterScreen.kt ================================================ package ru.otus.pandina.screens import com.kaspersky.kaspresso.screens.KScreen import io.github.kakaocup.kakao.edit.KEditText import io.github.kakaocup.kakao.switch.KSwitch import io.github.kakaocup.kakao.text.KButton import io.github.kakaocup.kakao.text.KTextView import io.github.kakaocup.kakao.toolbar.KToolbar import ml.docilealligator.infinityforreddit.R object CustomizePostFilterScreen : KScreen() { val toolBar = KToolbar{withId(R.id.toolbar_customize_post_filter_activity)} val customizeFilterEditText = KEditText { withId(R.id.name_text_input_edit_text_customize_post_filter_activity)} val textFilterTextView = KTextView { withId(R.id.post_type_text_text_view_customize_post_filter_activity)} val textFilterCheckBox = KSwitch { withId(R.id.post_type_text_switch_customize_post_filter_activity)} val linkFilterTextView = KTextView { withId(R.id.post_type_link_text_view_customize_post_filter_activity)} val linkFilterCheckBox = KSwitch { withId(R.id.post_type_link_switch_customize_post_filter_activity)} val onlyNsfwTextView = KTextView { withId(R.id.only_nsfw_text_view_customize_post_filter_activity)} val saveButton = KButton { withId(R.id.action_save_customize_post_filter_activity)} override val layoutId: Int? = null override val viewClass: Class<*>? = null } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/FilteredPostsScreen.kt ================================================ package ru.otus.pandina.screens import android.view.View import com.kaspersky.kaspresso.screens.KScreen import io.github.kakaocup.kakao.image.KImageView import io.github.kakaocup.kakao.recycler.KRecyclerItem import io.github.kakaocup.kakao.recycler.KRecyclerView import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KButton import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.activities.FilteredPostsActivity import org.hamcrest.Matcher object FilteredPostsScreen : KScreen() { val filterButton = KButton { withId(R.id.fab_filtered_thing_activity) } val moreOptions = KImageView { withContentDescription("More options")} val changePostLayout = KTextView { withText("Change Post Layout")} val postFragmentList = KRecyclerView( builder = { withId(R.id.recycler_view_post_fragment) }, itemTypeBuilder = { itemType(::PostFragmentItem) } ) class PostFragmentItem(parent: Matcher) : KRecyclerItem(parent) { // Try most common title views - if one fails, the test framework will try others val title = KTextView(parent) { withId(R.id.title_text_view_item_post_with_preview) } val titleGallery = KTextView(parent) { withId(R.id.title_text_view_item_post_gallery_type) } val image = KImageView(parent) { withId(R.id.image_view_item_post_with_preview) } val galleryImage = KImageView(parent) { withResourceName("image_view_item_post_gallery")} } override val layoutId: Int? = R.layout.activity_filtered_thing override val viewClass: Class<*>? = FilteredPostsActivity::class.java } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/MainScreen.kt ================================================ package ru.otus.pandina.screens import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.tabs.KTabLayout import io.github.kakaocup.kakao.text.KButton import ml.docilealligator.infinityforreddit.R object MainScreen : Screen() { val button = KButton { withId(R.id.fab_main_activity) } val tabLayout = KTabLayout { withId(R.id.tab_layout_main_activity) } val navButton = KButton { withContentDescription("Open navigation drawer") } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/UserAgreementFragment.kt ================================================ package ru.otus.pandina.screens import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KButton import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R object UserAgreementFragment : Screen() { val alertTitle = KTextView { withId(R.id.alertTitle)} val dontAgreeButton = KButton { withText("Don't Agree")} val agreeButton = KButton { withText("Agree")} } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/LoginScreen.kt ================================================ package ru.otus.pandina.screens.navigation import io.github.kakaocup.kakao.edit.KEditText import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KButton import io.github.kakaocup.kakao.web.KWebView import ml.docilealligator.infinityforreddit.R object LoginScreen : Screen() { val userNameField = KEditText { withText("Username")} val loginPasswordField = KEditText { withResourceName("loginPassword")} val loginButton = KButton { withText("Log In")} val webView = KWebView { withId(R.id.webview_login_activity)} } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/NavigationViewLayout.kt ================================================ package ru.otus.pandina.screens.navigation import android.view.View import io.github.kakaocup.kakao.image.KImageView import io.github.kakaocup.kakao.recycler.KRecyclerItem import io.github.kakaocup.kakao.recycler.KRecyclerView import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R import org.hamcrest.Matcher object NavigationViewLayout : Screen() { val navBanner = KImageView{withId(R.id.banner_image_view_nav_header_main)} val accountNameTextView = KTextView { withId(R.id.name_text_view_nav_header_main) } val karmaTextView = KTextView { withId(R.id.karma_text_view_nav_header_main) } val accountSwitcher = KImageView { withId(R.id.account_switcher_image_view_nav_header_main) } val addAccountButton = KImageView { withId(R.id.image_view_item_nav_drawer_menu_item) } val addAccountTextView = KTextView { withId(R.id.text_view_item_nav_drawer_menu_item) } val subscriptions = KTextView { withText("Subscriptions") } val multireddit = KTextView { withText("Multireddit") } val historyIcon = KImageView { withId(R.id.image_view_item_nav_drawer_menu_item) } val trending = KTextView { withText("Trending") } val darkThemeIcon = KImageView { withDrawable(R.drawable.ic_dark_theme_24dp) } val darkTheme = KTextView { withText("Dark Theme") } val lightThemeIcon = KImageView { withDrawable(R.drawable.ic_light_theme_24dp) } val lightTheme = KTextView { withText("Light Theme") } val settings = KTextView { withText("Settings") } val nawDrawerRecyclerView = KRecyclerView( builder = {withId(R.id.nav_drawer_recycler_view_main_activity)}, itemTypeBuilder = {itemType(NavigationViewLayout::NawDrawerRecyclerItem)} ) class NawDrawerRecyclerItem(parent : Matcher) : KRecyclerItem(parent) { } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/ActionPanel.kt ================================================ package ru.otus.pandina.screens.navigation.settings import io.github.kakaocup.kakao.common.views.KView import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KButton import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R object ActionPanel : Screen() { val alertTitle = KTextView { withId(R.id.alertTitle)} val list = KView { withId(R.id.select_dialog_listview) } val cancelButton = KButton { withText("Cancel")} fun getItem(itm : String) = KTextView { withId(R.id.select_dialog_listview) withDescendant { withText(itm) } }.click() } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/SettingsScreen.kt ================================================ package ru.otus.pandina.screens.navigation.settings import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KTextView object SettingsScreen : Screen() { val screenTittle = KTextView { withText("Settings")} val apiKeys = KTextView { withText("API Keys")} val notification = KTextView { withText("Notification")} val interfaceSetting = KTextView { withText("Interface")} val theme = KTextView { withText("Theme")} val gesturesAndButtons = KTextView {withText("Gestures & Buttons")} val video = KTextView {withText("Video")} val lazyModeInterval = KTextView{withText("Lazy Mode Interval")} val summary = KTextView{withText("2.5s")} val downloadLocation = KTextView{withText("Download Location")} val dataSavingMode = KTextView { withText("Data Saving Mode")} val nsfwAndSpoiler = KTextView {withText("NSFW & Spoiler")} val postHistory = KTextView { withText("Post History")} val postFilter = KTextView { withText("Post Filter")} val sortType = KTextView { withText("Sort Type")} val miscellaneous = KTextView { withText("Miscellaneous")} val advanced = KTextView { withText("Advanced")} val about = KTextView { withText("About")} val privacyPolicy = KTextView { withText("Privacy Policy")} val redditUserAgreement = KTextView { withText("Reddit User Agreement")} } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/ThemeScreen.kt ================================================ package ru.otus.pandina.screens.navigation.settings import android.view.View import io.github.kakaocup.kakao.common.views.KView import io.github.kakaocup.kakao.image.KImageView import io.github.kakaocup.kakao.recycler.KRecyclerItem import io.github.kakaocup.kakao.recycler.KRecyclerView import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.switch.KSwitch import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R import org.hamcrest.Matcher object ThemeScreen : Screen() { val screeTitle = KTextView { withParent { withId(R.id.toolbar_settings_activity) } withText("Theme") } val frame = KView { withId(R.id.frame_layout_settings_activity) } val themeRecycler = KRecyclerView( builder = { withId(R.id.recycler_view) }, itemTypeBuilder = { itemType(ThemeScreen::ThemeRecyclerItem) } ) class ThemeRecyclerItem(parent: Matcher) : KRecyclerItem(parent) { val icon = KImageView(parent) { withResourceName("icon_frame") } val title = KTextView(parent) { withResourceName("title") } val summary = KTextView(parent) { withResourceName("summary") } val switcher = KSwitch(parent) { withResourceName("material_switch_switch_preference") } } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/font/FontPreviewScreen.kt ================================================ package ru.otus.pandina.screens.navigation.settings.font import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KTextView object FontPreviewScreen : Screen() { val screenTitle = KTextView { withText("Font Preview") } val someFont = KTextView { withAnyText() } fun selectFont(font : String) { someFont { hasText(font) click() } } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/font/FontScreen.kt ================================================ package ru.otus.pandina.screens.navigation.settings.font import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R object FontScreen : Screen() { val screenTitle = KTextView { withText("Font") withParent { withId(R.id.toolbar_settings_activity) } } val fontPreview = KTextView { withText("Font Preview") } val fontFamily = KTextView { withText("Font Family") } val summaryFontFamily = KTextView { withSibling { withText("Font Family") } } val fontSize = KTextView { withText("Font Size") } val titleFontFamily = KTextView { withText("Title Font Family") } val titleFontSize = KTextView { withText("Title Font Size") } val contentFontFamily = KTextView { withText("Content Font Family") } val contentFontSize = KTextView { withText("Content Font Size") } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/interfaceScreen/CustomizeTabsScreen.kt ================================================ package ru.otus.pandina.screens.navigation.settings.interfaceScreen import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R object CustomizeTabsScreen : Screen() { val screenTitle = KTextView { withText("Customize Tabs in Main Page") } val tabCountTitle = KTextView { withId(R.id.tab_count_title_text_view_customize_main_page_tabs_fragment)} val tabCountSummary = KTextView { withId(R.id.tab_count_text_view_customize_main_page_tabs_fragment)} } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/interfaceScreen/InterfaceScreen.kt ================================================ package ru.otus.pandina.screens.navigation.settings.interfaceScreen import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.switch.KSwitch import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R object InterfaceScreen : Screen() { val screenTitle = KTextView {withText("Interface")} val font = KTextView {withText("Font")} val immersiveInterface = KTextView {withText("Immersive Interface")} val navigationDrawer = KTextView {withText("Navigation Drawer")} val customizeTabs = KTextView {withText("Customize Tabs in Main Page")} val customizeBottom = KTextView{withText("Customize Bottom Navigation Bar")} val hideFab = KTextView{withText("Hide FAB in Post Feed")} val hideFabSwitch = KSwitch {withId(R.id.material_switch_switch_preference)} val enableBottomNav = KTextView{withText("Enable Bottom Navigation")} } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/screens/navigation/settings/notification/NotificationScreen.kt ================================================ package ru.otus.pandina.screens.navigation.settings.notification import io.github.kakaocup.kakao.screen.Screen import io.github.kakaocup.kakao.switch.KSwitch import io.github.kakaocup.kakao.text.KTextView import ml.docilealligator.infinityforreddit.R object NotificationScreen : Screen() { val screenTitle = KTextView { withText("Notification") } val enableNotifications = KTextView { withText("Enable Notifications") } val notificationSwitch = KSwitch { withId(R.id.material_switch_switch_preference) } val notificationInterval = KTextView { withText("Check Notifications Interval")} } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/tests/APIKeysTest.kt ================================================ package ru.otus.pandina.tests import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withClassName import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import ml.docilealligator.infinityforreddit.activities.MainActivity import org.hamcrest.Matchers.endsWith import org.junit.Test import ru.otus.pandina.screens.MainScreen import ru.otus.pandina.screens.navigation.NavigationViewLayout import ru.otus.pandina.screens.navigation.settings.SettingsScreen import ru.otus.pandina.utils.NotificationDialogHelper class APIKeysTest : BaseTest() { fun openSettings() { run { step("Open navigation drawer") { MainScreen { navButton { isVisible() click() } } NavigationViewLayout { navBanner.isVisible() settings.isVisible() nawDrawerRecyclerView { scrollToEnd() } } } step("Open settings") { NavigationViewLayout.settings.click() SettingsScreen { screenTittle { isVisible() hasText("Settings") } } } } } @Test fun addRedditClientIdTest() { val redditClientId = InstrumentationRegistry.getArguments().getString("REDDIT_CLIENT_ID") ?: "test_reddit_client_id_default" // Handle notification dialog immediately after app starts NotificationDialogHelper.handleNotificationDialog() before { openSettings() }.after { }.run { step("Open API Keys screen") { onView(withText("API Keys")).perform(click()) Thread.sleep(1000) // Verify we're on API Keys screen onView(withText("API Keys")).check(matches(isDisplayed())) onView(withText("Reddit API Client ID")).check(matches(isDisplayed())) } step("Add Reddit API Client ID") { onView(withText("Reddit API Client ID")).perform(click()) Thread.sleep(1000) // Type the Reddit Client ID in the dialog onView(withClassName(endsWith("EditText"))).perform(typeText(redditClientId)) // Verify OK button is clickable (test passes without clicking to avoid app restart) onView(withText("OK")).check(matches(isDisplayed())) } } } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/tests/BaseTest.kt ================================================ package ru.otus.pandina.tests import androidx.test.ext.junit.rules.ActivityScenarioRule import com.kaspersky.components.alluresupport.withForcedAllureSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import ml.docilealligator.infinityforreddit.activities.MainActivity import org.junit.Rule open class BaseTest : TestCase( kaspressoBuilder = Kaspresso.Builder.withForcedAllureSupport() ) { @get:Rule val activityRule = ActivityScenarioRule(MainActivity::class.java) } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/tests/LoginTest.kt ================================================ package ru.otus.pandina.tests import androidx.test.espresso.web.webdriver.Locator import org.junit.Test import ru.otus.pandina.screens.MainScreen import ru.otus.pandina.screens.UserAgreementFragment import ru.otus.pandina.screens.navigation.LoginScreen import ru.otus.pandina.screens.navigation.NavigationViewLayout import ru.otus.pandina.utils.NotificationDialogHelper class LoginTest : BaseTest() { @Test fun loginTest() { // Handle notification dialog immediately after app starts NotificationDialogHelper.handleNotificationDialog() run { step("Open navigation") { MainScreen { navButton { isVisible() click() } } NavigationViewLayout { navBanner.isVisible() } } step("Go to login form") { NavigationViewLayout { accountNameTextView { isVisible() hasText("Anonymous") } karmaTextView { isVisible() hasText("Press here to login") } accountSwitcher { isVisible() click() } addAccountTextView { isVisible() hasText("Add an account") } addAccountButton { isVisible() longClick() } } } step("Check if user agreement appears and handle it") { try { UserAgreementFragment { alertTitle { isVisible() hasText("User Agreement") } dontAgreeButton.isVisible() agreeButton { isVisible() click() } } } catch (e: Exception) { // User agreement dialog may not appear if already accepted or not required // Continue with login process } } step("Enter Login and password") { LoginScreen { webView { withElement( Locator.XPATH, "//h1" ) { containsText("Log In") } withElement( Locator.XPATH, "//*[@id='login-username']" ) { containsText("Email or username") keys("*****") } } } } } } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/tests/MainTest.kt ================================================ package ru.otus.pandina.tests import androidx.test.espresso.action.GeneralLocation import org.junit.Test import ru.otus.pandina.screens.CustomizePostFilterScreen import ru.otus.pandina.screens.FilteredPostsScreen import ru.otus.pandina.screens.MainScreen import ru.otus.pandina.utils.NotificationDialogHelper class MainTest : BaseTest() { @Test fun popularPostFilterTest() { // Handle notification dialog immediately after app starts NotificationDialogHelper.handleNotificationDialog() run { step("Main screen popular tab") { MainScreen { tabLayout { isVisible() click(GeneralLocation.CENTER) } button { isVisible() click() } } } step("Check customize filter") { CustomizePostFilterScreen { toolBar { isVisible() hasTitle("Customize Post Filter") } customizeFilterEditText { isEnabled() hasText("New Filter") } textFilterTextView { isVisible() hasText("Text") } textFilterCheckBox { isVisible() isChecked() click() isNotChecked() } linkFilterTextView { isVisible() hasText("Link") } linkFilterCheckBox { isVisible() isChecked() click() isNotChecked() } onlyNsfwTextView { isVisible() hasText("Only NSFW Content") } saveButton.click() } } step("Popular Filtered Posts") { FilteredPostsScreen { postFragmentList { isVisible() firstChild { isVisible() } lastChild { isVisible() // Try different title types since posts can have different layouts try { title.hasAnyText() } catch (e: Exception) { titleGallery.hasAnyText() } } children { isVisible() } } filterButton { isVisible() click() } } } } } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/tests/SettingsTest.kt ================================================ package ru.otus.pandina.tests import android.graphics.Color import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withText import ml.docilealligator.infinityforreddit.activities.MainActivity import org.junit.Test import ru.otus.pandina.screens.navigation.settings.interfaceScreen.CustomizeTabsScreen import ru.otus.pandina.screens.MainScreen import ru.otus.pandina.screens.navigation.NavigationViewLayout import ru.otus.pandina.screens.navigation.settings.ActionPanel import ru.otus.pandina.screens.navigation.settings.SettingsScreen import ru.otus.pandina.screens.navigation.settings.ThemeScreen import ru.otus.pandina.screens.navigation.settings.font.FontPreviewScreen import ru.otus.pandina.screens.navigation.settings.font.FontScreen import ru.otus.pandina.screens.navigation.settings.interfaceScreen.InterfaceScreen import ru.otus.pandina.screens.navigation.settings.notification.NotificationScreen import ru.otus.pandina.utils.NotificationDialogHelper class SettingsTest : BaseTest() { fun openSettings() { run { step("Open navigation drawer") { MainScreen { navButton { isVisible() click() } } NavigationViewLayout { navBanner.isVisible() settings.isVisible() nawDrawerRecyclerView { scrollToEnd() } } } step("Open settings") { NavigationViewLayout.settings.click() SettingsScreen { screenTittle { isVisible() hasText("Settings") } } } } } @Test fun disableNotificationTest() { before { openSettings() }.after { }.run { step("Open notifications screen and disable notifications") { SettingsScreen.notification.click() NotificationScreen { screenTitle { isVisible() hasText("Notification") } enableNotifications.isVisible() notificationInterval.isVisible() notificationSwitch.click() notificationInterval.doesNotExist() notificationSwitch.click() notificationInterval.isVisible() } } } } @Test fun setFontTest() { // Handle notification dialog immediately after app starts NotificationDialogHelper.handleNotificationDialog() before { openSettings() }.after { }.run { step("Open interface screen") { SettingsScreen.interfaceSetting.click() InterfaceScreen { screenTitle { isVisible() hasText("Interface") } font.isVisible() } } step("Open font screen and set font") { InterfaceScreen.font.click() FontScreen { screenTitle { isVisible() hasText("Font") } fontPreview { isVisible() click() } } FontPreviewScreen { screenTitle { isVisible() hasText("Font Preview") } } } } } @Test fun customizeTabsInMainPage() { before { openSettings() }.after { }.run { step("Open interface screen") { SettingsScreen.interfaceSetting.click() InterfaceScreen { screenTitle { isVisible() hasText("Interface") } customizeTabs.isVisible() } } step("Open and Customize Tabs in Main Page") { InterfaceScreen.customizeTabs.click() CustomizeTabsScreen { screenTitle { isVisible() hasText("Customize Tabs in Main Page") } tabCountTitle { isVisible() hasText("Tab Count") click() } } // Skip ActionPanel verification and go directly to selection Thread.sleep(500) onView(withText("2")).perform(click()) // Wait for UI to update Thread.sleep(500) CustomizeTabsScreen { tabCountSummary.hasText("2") } } step("Restart app") { activityRule.scenario.close() ActivityScenario.launch(MainActivity::class.java, null) } step("Check tabs") { MainScreen { tabLayout { hasDescendant { withText("Home") } hasDescendant { withText("Popular") } hasNotDescendant { withText("All") } } } } } } @Test fun setDarkThemeTest() { before { openSettings() }.after { }.run { step("Open Theme screen and set dark theme") { SettingsScreen.theme.click() ThemeScreen { themeRecycler { // Click on the "Theme" preference item childWith { withDescendant { withText("Theme") } }.click() } } // Wait for dialog and select Dark Theme Thread.sleep(1000) onView(withText("Dark Theme")).perform(click()) // Give the UI time to update Thread.sleep(500) ThemeScreen { themeRecycler { // Verify the Theme item now shows "Dark Theme" in summary childWith { withDescendant { withText("Theme") } }.summary.hasText("Dark Theme") } } } } } @Test fun setLightThemeTest() { before { openSettings() }.after { }.run { step("Open Theme screen and set light theme") { SettingsScreen.theme.click() ThemeScreen { themeRecycler { // Click on the "Theme" preference item childWith { withDescendant { withText("Theme") } }.click() } } // Wait for dialog and select Light Theme Thread.sleep(1000) onView(withText("Light Theme")).perform(click()) // Remove this line - no OK button needed: // onView(withText("OK")).perform(click()) // Give the UI time to update Thread.sleep(500) ThemeScreen { themeRecycler { // Verify the Theme item now shows "Light Theme" in summary childWith { withDescendant { withText("Theme") } }.summary.hasText("Light Theme") } frame.hasBackgroundColor(Color.WHITE) } } } } } ================================================ FILE: app/src/androidTest/kotlin/ru/otus/pandina/utils/NotificationDialogHelper.kt ================================================ package ru.otus.pandina.utils import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector object NotificationDialogHelper { fun handleNotificationDialog() { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) Thread.sleep(3000) // Wait for app and dialog to fully load // Try to find and click "Don't allow" or similar buttons first val dontAllowButton = device.findObject(UiSelector().text("Don't allow")) val allowButton = device.findObject(UiSelector().text("Allow")) when { dontAllowButton.exists() -> { dontAllowButton.click() Thread.sleep(1000) } allowButton.exists() -> { // If only "Allow" is visible, click it to proceed (we can test without notifications) allowButton.click() Thread.sleep(1000) } else -> { // If no dialog buttons found, the dialog might have been dismissed already Thread.sleep(1000) } } } } ================================================ FILE: app/src/debug/res/values/strings.xml ================================================ Continuum (Debug) ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/APIResult.kt ================================================ package ml.docilealligator.infinityforreddit import androidx.annotation.StringRes sealed class APIResult { data class Success(val data: T): APIResult() data class Error(val error: APIError): APIResult() } sealed class APIError { data class Message(val message: String) : APIError() data class MessageRes(@StringRes val resId: Int) : APIError() } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/ActionState.kt ================================================ package ml.docilealligator.infinityforreddit import androidx.annotation.StringRes sealed interface ActionState { object Idle: ActionState object Running: ActionState data class Success(val data: T): ActionState data class Error(val error: ActionStateError): ActionState } sealed class ActionStateError { data class Message(val message: String) : ActionStateError() data class MessageRes(@StringRes val resId: Int) : ActionStateError() } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java ================================================ package ml.docilealligator.infinityforreddit; import android.app.Application; import javax.inject.Singleton; import dagger.BindsInstance; import dagger.Component; import ml.docilealligator.infinityforreddit.activities.AccountPostsActivity; import ml.docilealligator.infinityforreddit.activities.AccountSavedThingActivity; import ml.docilealligator.infinityforreddit.activities.CommentActivity; import ml.docilealligator.infinityforreddit.activities.CommentFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.activities.CommentFilterUsageListingActivity; import ml.docilealligator.infinityforreddit.activities.CopyMultiRedditActivity; import ml.docilealligator.infinityforreddit.activities.CreateMultiRedditActivity; import ml.docilealligator.infinityforreddit.activities.CustomThemeListingActivity; import ml.docilealligator.infinityforreddit.activities.CustomThemePreviewActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeCommentFilterActivity; import ml.docilealligator.infinityforreddit.activities.CustomizePostFilterActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity; import ml.docilealligator.infinityforreddit.activities.EditCommentActivity; import ml.docilealligator.infinityforreddit.activities.EditMultiRedditActivity; import ml.docilealligator.infinityforreddit.activities.EditPostActivity; import ml.docilealligator.infinityforreddit.activities.EditProfileActivity; import ml.docilealligator.infinityforreddit.activities.FilteredPostsActivity; import ml.docilealligator.infinityforreddit.activities.FullMarkdownActivity; import ml.docilealligator.infinityforreddit.activities.HistoryActivity; import ml.docilealligator.infinityforreddit.activities.InboxActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.LockScreenActivity; import ml.docilealligator.infinityforreddit.activities.LoginActivity; import ml.docilealligator.infinityforreddit.activities.LoginChromeCustomTabActivity; import ml.docilealligator.infinityforreddit.activities.MainActivity; import ml.docilealligator.infinityforreddit.activities.PostFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.activities.PostFilterUsageListingActivity; import ml.docilealligator.infinityforreddit.activities.PostGalleryActivity; import ml.docilealligator.infinityforreddit.activities.PostImageActivity; import ml.docilealligator.infinityforreddit.activities.PostLinkActivity; import ml.docilealligator.infinityforreddit.activities.PostPollActivity; import ml.docilealligator.infinityforreddit.activities.PostTextActivity; import ml.docilealligator.infinityforreddit.activities.PostVideoActivity; import ml.docilealligator.infinityforreddit.activities.ReportActivity; import ml.docilealligator.infinityforreddit.activities.RulesActivity; import ml.docilealligator.infinityforreddit.activities.SearchActivity; import ml.docilealligator.infinityforreddit.activities.SearchHistoryActivity; import ml.docilealligator.infinityforreddit.activities.SearchResultActivity; import ml.docilealligator.infinityforreddit.activities.SearchSubredditsResultActivity; import ml.docilealligator.infinityforreddit.activities.SearchUsersResultActivity; import ml.docilealligator.infinityforreddit.activities.SelectUserFlairActivity; import ml.docilealligator.infinityforreddit.activities.SelectedSubredditsAndUsersActivity; import ml.docilealligator.infinityforreddit.activities.SendPrivateMessageActivity; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.activities.SubmitCrosspostActivity; import ml.docilealligator.infinityforreddit.activities.SubredditMultiselectionActivity; import ml.docilealligator.infinityforreddit.activities.UserMultiselectionActivity; import ml.docilealligator.infinityforreddit.activities.SubscribedThingListingActivity; import ml.docilealligator.infinityforreddit.activities.SuicidePreventionActivity; import ml.docilealligator.infinityforreddit.activities.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.activities.ViewImgurMediaActivity; import ml.docilealligator.infinityforreddit.activities.ViewMultiRedditDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewPrivateMessagesActivity; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity; import ml.docilealligator.infinityforreddit.activities.WebViewActivity; import ml.docilealligator.infinityforreddit.activities.WikiActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.ShareBottomSheetFragment; import ml.docilealligator.infinityforreddit.fragments.CommentsListingFragment; import ml.docilealligator.infinityforreddit.fragments.CustomThemeListingFragment; import ml.docilealligator.infinityforreddit.fragments.FollowedUsersListingFragment; import ml.docilealligator.infinityforreddit.fragments.HistoryPostFragment; import ml.docilealligator.infinityforreddit.fragments.InboxFragment; import ml.docilealligator.infinityforreddit.fragments.MorePostsInfoFragment; import ml.docilealligator.infinityforreddit.fragments.MultiRedditListingFragment; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.fragments.SidebarFragment; import ml.docilealligator.infinityforreddit.fragments.SubredditListingFragment; import ml.docilealligator.infinityforreddit.fragments.SubscribedSubredditsListingFragment; import ml.docilealligator.infinityforreddit.fragments.UserListingFragment; import ml.docilealligator.infinityforreddit.fragments.ViewImgurImageFragment; import ml.docilealligator.infinityforreddit.fragments.ViewImgurVideoFragment; import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment; import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryImageOrGifFragment; import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryVideoFragment; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.services.DownloadRedditVideoService; import ml.docilealligator.infinityforreddit.services.EditProfileService; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.settings.AdvancedPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.APIKeysPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.CommentPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.CrashReportsFragment; import ml.docilealligator.infinityforreddit.settings.CustomizeBottomAppBarFragment; import ml.docilealligator.infinityforreddit.settings.CustomizeMainPageTabsFragment; import ml.docilealligator.infinityforreddit.settings.DownloadLocationPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.FontPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.GesturesAndButtonsPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.MainPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.MiscellaneousPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.NotificationPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.NsfwAndSpoilerFragment; import ml.docilealligator.infinityforreddit.settings.PostHistoryFragment; import ml.docilealligator.infinityforreddit.settings.ProxyPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.SecurityPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.ThemePreferenceFragment; import ml.docilealligator.infinityforreddit.settings.TranslationFragment; import ml.docilealligator.infinityforreddit.settings.VideoPreferenceFragment; import ml.docilealligator.infinityforreddit.worker.MaterialYouWorker; import ml.docilealligator.infinityforreddit.worker.PullNotificationWorker; @Singleton @Component(modules = {AppModule.class, NetworkModule.class}) public interface AppComponent { void inject(MainActivity mainActivity); void inject(LoginActivity loginActivity); void inject(PostFragment postFragment); void inject(SubredditListingFragment subredditListingFragment); void inject(UserListingFragment userListingFragment); void inject(ViewPostDetailActivity viewPostDetailActivity); void inject(ViewSubredditDetailActivity viewSubredditDetailActivity); void inject(ViewUserDetailActivity viewUserDetailActivity); void inject(CommentActivity commentActivity); void inject(SubscribedThingListingActivity subscribedThingListingActivity); void inject(PostTextActivity postTextActivity); void inject(SubscribedSubredditsListingFragment subscribedSubredditsListingFragment); void inject(PostLinkActivity postLinkActivity); void inject(PostImageActivity postImageActivity); void inject(PostVideoActivity postVideoActivity); void inject(FlairBottomSheetFragment flairBottomSheetFragment); void inject(RulesActivity rulesActivity); void inject(CommentsListingFragment commentsListingFragment); void inject(SubmitPostService submitPostService); void inject(FilteredPostsActivity filteredPostsActivity); void inject(SearchResultActivity searchResultActivity); void inject(SearchSubredditsResultActivity searchSubredditsResultActivity); void inject(FollowedUsersListingFragment followedUsersListingFragment); void inject(EditPostActivity editPostActivity); void inject(EditCommentActivity editCommentActivity); void inject(AccountPostsActivity accountPostsActivity); void inject(PullNotificationWorker pullNotificationWorker); void inject(InboxActivity inboxActivity); void inject(NotificationPreferenceFragment notificationPreferenceFragment); void inject(LinkResolverActivity linkResolverActivity); void inject(SearchActivity searchActivity); void inject(SearchHistoryActivity searchHistoryActivity); void inject(SettingsActivity settingsActivity); void inject(MainPreferenceFragment mainPreferenceFragment); void inject(AccountSavedThingActivity accountSavedThingActivity); void inject(ViewImageOrGifActivity viewGIFActivity); void inject(ViewMultiRedditDetailActivity viewMultiRedditDetailActivity); void inject(ViewVideoActivity viewVideoActivity); void inject(GesturesAndButtonsPreferenceFragment gesturesAndButtonsPreferenceFragment); void inject(CreateMultiRedditActivity createMultiRedditActivity); void inject(SubredditMultiselectionActivity subredditMultiselectionActivity); void inject(UserMultiselectionActivity userMultiselectionActivity); void inject(ThemePreferenceFragment themePreferenceFragment); void inject(CustomizeThemeActivity customizeThemeActivity); void inject(CustomThemeListingActivity customThemeListingActivity); void inject(SidebarFragment sidebarFragment); void inject(AdvancedPreferenceFragment advancedPreferenceFragment); void inject(APIKeysPreferenceFragment apiKeysPreferenceFragment); void inject(CustomThemePreviewActivity customThemePreviewActivity); void inject(EditMultiRedditActivity editMultiRedditActivity); void inject(SelectedSubredditsAndUsersActivity selectedSubredditsAndUsersActivity); void inject(ReportActivity reportActivity); void inject(ViewImgurMediaActivity viewImgurMediaActivity); void inject(ViewImgurVideoFragment viewImgurVideoFragment); void inject(DownloadRedditVideoService downloadRedditVideoService); void inject(MultiRedditListingFragment multiRedditListingFragment); void inject(InboxFragment inboxFragment); void inject(ViewPrivateMessagesActivity viewPrivateMessagesActivity); void inject(SendPrivateMessageActivity sendPrivateMessageActivity); void inject(VideoPreferenceFragment videoPreferenceFragment); void inject(ViewRedditGalleryActivity viewRedditGalleryActivity); void inject(ViewRedditGalleryVideoFragment viewRedditGalleryVideoFragment); void inject(CustomizeMainPageTabsFragment customizeMainPageTabsFragment); void inject(DownloadMediaService downloadMediaService); void inject(DownloadLocationPreferenceFragment downloadLocationPreferenceFragment); void inject(SubmitCrosspostActivity submitCrosspostActivity); void inject(FullMarkdownActivity fullMarkdownActivity); void inject(SelectUserFlairActivity selectUserFlairActivity); void inject(SecurityPreferenceFragment securityPreferenceFragment); void inject(NsfwAndSpoilerFragment nsfwAndSpoilerFragment); void inject(CustomizeBottomAppBarFragment customizeBottomAppBarFragment); void inject(TranslationFragment translationFragment); void inject(MiscellaneousPreferenceFragment miscellaneousPreferenceFragment); void inject(CustomizePostFilterActivity customizePostFilterActivity); void inject(PostHistoryFragment postHistoryFragment); void inject(PostFilterPreferenceActivity postFilterPreferenceActivity); void inject(PostFilterUsageListingActivity postFilterUsageListingActivity); void inject(SearchUsersResultActivity searchUsersResultActivity); void inject(ViewImgurImageFragment viewImgurImageFragment); void inject(ViewRedditGalleryImageOrGifFragment viewRedditGalleryImageOrGifFragment); void inject(ViewPostDetailFragment viewPostDetailFragment); void inject(SuicidePreventionActivity suicidePreventionActivity); void inject(WebViewActivity webViewActivity); void inject(CrashReportsFragment crashReportsFragment); void inject(LockScreenActivity lockScreenActivity); void inject(PostGalleryActivity postGalleryActivity); void inject(WikiActivity wikiActivity); void inject(Infinity infinity); void inject(EditProfileService editProfileService); void inject(EditProfileActivity editProfileActivity); void inject(FontPreferenceFragment fontPreferenceFragment); void inject(CommentPreferenceFragment commentPreferenceFragment); void inject(PostPollActivity postPollActivity); void inject(AccountChooserBottomSheetFragment accountChooserBottomSheetFragment); void inject(MaterialYouWorker materialYouWorker); void inject(HistoryPostFragment historyPostFragment); void inject(HistoryActivity historyActivity); void inject(MorePostsInfoFragment morePostsInfoFragment); void inject(CommentFilterPreferenceActivity commentFilterPreferenceActivity); void inject(CustomizeCommentFilterActivity customizeCommentFilterActivity); void inject(CommentFilterUsageListingActivity commentFilterUsageListingActivity); void inject(CustomThemeListingFragment customThemeListingFragment); void inject(LoginChromeCustomTabActivity loginChromeCustomTabActivity); void inject(PostOptionsBottomSheetFragment postOptionsBottomSheetFragment); void inject(ShareBottomSheetFragment shareBottomSheetFragment); void inject(ProxyPreferenceFragment proxyPreferenceFragment); void inject(CopyMultiRedditActivity copyMultiRedditActivity); @Component.Factory interface Factory { AppComponent create(@BindsInstance Application application); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java ================================================ package ml.docilealligator.infinityforreddit; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; import androidx.media3.database.StandaloneDatabaseProvider; import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor; import androidx.media3.datasource.cache.SimpleCache; import androidx.media3.datasource.okhttp.OkHttpDataSource; import androidx.preference.PreferenceManager; import java.io.File; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import javax.inject.Named; import javax.inject.Singleton; import dagger.Binds; import dagger.Module; import dagger.Provides; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LoopAvailableExoCreator; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.videoautoplay.Config; import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator; import ml.docilealligator.infinityforreddit.videoautoplay.MediaSourceBuilder; import ml.docilealligator.infinityforreddit.videoautoplay.ToroExo; import okhttp3.OkHttpClient; @Module abstract class AppModule { @Binds abstract Context providesContext(Application application); @Provides @Singleton static RedditDataRoomDatabase provideRedditDataRoomDatabase(Application application) { return RedditDataRoomDatabase.create(application); } @Provides @Named("default") @Singleton static SharedPreferences provideSharedPreferences(Application application) { return PreferenceManager.getDefaultSharedPreferences(application); } @Provides @Named("light_theme") @Singleton static SharedPreferences provideLightThemeSharedPreferences(Application application) { return application.getSharedPreferences(CustomThemeSharedPreferencesUtils.LIGHT_THEME_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("dark_theme") @Singleton static SharedPreferences provideDarkThemeSharedPreferences(Application application) { return application.getSharedPreferences(CustomThemeSharedPreferencesUtils.DARK_THEME_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("amoled_theme") @Singleton static SharedPreferences provideAmoledThemeSharedPreferences(Application application) { return application.getSharedPreferences(CustomThemeSharedPreferencesUtils.AMOLED_THEME_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("sort_type") static SharedPreferences provideSortTypeSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.SORT_TYPE_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("post_layout") static SharedPreferences providePostLayoutSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.POST_LAYOUT_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("post_feed_scrolled_position_cache") static SharedPreferences providePostFeedScrolledPositionSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("main_activity_tabs") static SharedPreferences provideMainActivityTabsSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.MAIN_PAGE_TABS_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("nsfw_and_spoiler") static SharedPreferences provideNsfwAndSpoilerSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.NSFW_AND_SPOILER_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("bottom_app_bar") static SharedPreferences provideBottoappBarSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.BOTTOM_APP_BAR_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("post_history") static SharedPreferences providePostHistorySharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.POST_HISTORY_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("current_account") static SharedPreferences provideCurrentAccountSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.CURRENT_ACCOUNT_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("navigation_drawer") static SharedPreferences provideNavigationDrawerSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.NAVIGATION_DRAWER_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("post_details") static SharedPreferences providePostDetailsSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.POST_DETAILS_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("security") @Singleton static SharedPreferences provideSecuritySharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.SECURITY_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("internal") @Singleton static SharedPreferences provideInternalSharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.INTERNAL_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Named("proxy") @Singleton static SharedPreferences provideProxySharedPreferences(Application application) { return application.getSharedPreferences(SharedPreferencesUtils.PROXY_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); } @Provides @Singleton static CustomThemeWrapper provideCustomThemeWrapper(@Named("light_theme") SharedPreferences lightThemeSharedPreferences, @Named("dark_theme") SharedPreferences darkThemeSharedPreferences, @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences) { return new CustomThemeWrapper(lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences); } @Provides @Named("app_cache_dir") static File providesAppCache(Application application) { return application.getCacheDir(); } @Provides @Named("exo_player_cache") static File providesExoPlayerCache(@Named("app_cache_dir") File appCache) { return new File(appCache, "/exoplayer"); } @OptIn(markerClass = UnstableApi.class) @Provides static StandaloneDatabaseProvider provideExoDatabaseProvider(Application application) { return new StandaloneDatabaseProvider(application); } @OptIn(markerClass = UnstableApi.class) @Provides @Singleton static SimpleCache provideSimpleCache(StandaloneDatabaseProvider standaloneDatabaseProvider, @Named("exo_player_cache") File exoPlayerCache) { return new SimpleCache(exoPlayerCache, new LeastRecentlyUsedCacheEvictor(200 * 1024 * 1024), standaloneDatabaseProvider); } @OptIn(markerClass = UnstableApi.class) @Provides static Config providesMediaConfig(Application application, SimpleCache simpleCache, @Named("media3")OkHttpClient okHttpClient) { return new Config.Builder(application) .setDataSourceFactory(new OkHttpDataSource.Factory(okHttpClient).setUserAgent(APIUtils.USER_AGENT)) .setMediaSourceBuilder(MediaSourceBuilder.DEFAULT) .setCache(simpleCache) .build(); } @OptIn(markerClass = UnstableApi.class) @Provides static ToroExo providesToroExo(Application application) { return ToroExo.with(application); } @OptIn(markerClass = UnstableApi.class) @Provides @Singleton static ExoCreator provideExoCreator(Config config, ToroExo toroExo, @Named("default") SharedPreferences sharedPreferences) { return new LoopAvailableExoCreator(toroExo, config, sharedPreferences); } @Provides @Singleton static Executor provideExecutor() { return Executors.newFixedThreadPool(4); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/CommentModerationActionHandler.kt ================================================ package ml.docilealligator.infinityforreddit import ml.docilealligator.infinityforreddit.comment.Comment interface CommentModerationActionHandler { fun approveComment(comment: Comment, position: Int) fun removeComment(comment: Comment, position: Int, isSpam: Boolean) fun toggleLock(comment: Comment, position: Int) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/Constants.java ================================================ package ml.docilealligator.infinityforreddit; public class Constants { public static final int DEFAULT_TAB_COUNT = 3; public static final int MAX_TAB_COUNT = 6; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/Converters.kt ================================================ package ml.docilealligator.infinityforreddit import androidx.room.TypeConverter import ml.docilealligator.infinityforreddit.comment.DraftType class Converters { @TypeConverter fun fromDraftType(value: DraftType): String { return value.name } @TypeConverter fun toDraftType(value: String): DraftType { return DraftType.valueOf(value) } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/CustomFontReceiver.java ================================================ package ml.docilealligator.infinityforreddit; import android.graphics.Typeface; public interface CustomFontReceiver { void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/DataLoadState.kt ================================================ package ml.docilealligator.infinityforreddit sealed class DataLoadState { object Loading: DataLoadState() data class Success(val data: T): DataLoadState() data class Error(val message: String): DataLoadState() } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/DownloadProgressResponseBody.java ================================================ package ml.docilealligator.infinityforreddit; import java.io.IOException; import okhttp3.MediaType; import okhttp3.ResponseBody; import okio.Buffer; import okio.BufferedSource; import okio.ForwardingSource; import okio.Okio; import okio.Source; public class DownloadProgressResponseBody extends ResponseBody { private final ResponseBody responseBody; private final ProgressListener progressListener; private BufferedSource bufferedSource; public DownloadProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { this.responseBody = responseBody; this.progressListener = progressListener; } @Override public MediaType contentType() { return responseBody.contentType(); } @Override public long contentLength() { return responseBody.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(source(responseBody.source())); } return bufferedSource; } private Source source(Source source) { return new ForwardingSource(source) { long totalBytesRead = 0L; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); // read() returns the number of bytes read, or -1 if this source is exhausted. totalBytesRead += bytesRead != -1 ? bytesRead : 0; progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1); return bytesRead; } }; } public interface ProgressListener { void update(long bytesRead, long contentLength, boolean done); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/FetchPostFilterAndConcatenatedSubredditNames.java ================================================ package ml.docilealligator.infinityforreddit; import android.os.Handler; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubreddit; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; public class FetchPostFilterAndConcatenatedSubredditNames { public static void fetchPostFilter(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, Handler handler, int postFilterUsage, String nameOfUsage, FetchPostFilterListerner fetchPostFilterListerner) { executor.execute(() -> { List postFilters = redditDataRoomDatabase.postFilterDao().getValidPostFilters(postFilterUsage, nameOfUsage); PostFilter mergedPostFilter = PostFilter.mergePostFilter(postFilters); handler.post(() -> fetchPostFilterListerner.success(mergedPostFilter)); }); } public static void fetchPostFilterAndConcatenatedSubredditNames(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, Handler handler, int postFilterUsage, String nameOfUsage, FetchPostFilterAndConcatenatecSubredditNamesListener fetchPostFilterAndConcatenatecSubredditNamesListener) { executor.execute(() -> { List postFilters = redditDataRoomDatabase.postFilterDao().getValidPostFilters(postFilterUsage, nameOfUsage); PostFilter mergedPostFilter = PostFilter.mergePostFilter(postFilters); List anonymousSubscribedSubreddits = redditDataRoomDatabase.subscribedSubredditDao().getAllSubscribedSubredditsList(Account.ANONYMOUS_ACCOUNT); if (anonymousSubscribedSubreddits != null && !anonymousSubscribedSubreddits.isEmpty()) { StringBuilder stringBuilder = new StringBuilder(); for (SubscribedSubredditData s : anonymousSubscribedSubreddits) { stringBuilder.append(s.getName()).append("+"); } if (stringBuilder.length() > 0) { stringBuilder.deleteCharAt(stringBuilder.length() - 1); } handler.post(() -> fetchPostFilterAndConcatenatecSubredditNamesListener.success(mergedPostFilter, stringBuilder.toString())); } else { handler.post(() -> fetchPostFilterAndConcatenatecSubredditNamesListener.success(mergedPostFilter, null)); } }); } public static void fetchPostFilterAndConcatenatedSubredditNames(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, Handler handler, String multipath, int postFilterUsage, String nameOfUsage, FetchPostFilterAndConcatenatecSubredditNamesListener fetchPostFilterAndConcatenatecSubredditNamesListener) { executor.execute(() -> { List postFilters = redditDataRoomDatabase.postFilterDao().getValidPostFilters(postFilterUsage, nameOfUsage); PostFilter mergedPostFilter = PostFilter.mergePostFilter(postFilters); List anonymousMultiredditSubreddits = redditDataRoomDatabase.anonymousMultiredditSubredditDao().getAllAnonymousMultiRedditSubreddits(multipath); if (anonymousMultiredditSubreddits != null && !anonymousMultiredditSubreddits.isEmpty()) { StringBuilder stringBuilder = new StringBuilder(); for (AnonymousMultiredditSubreddit s : anonymousMultiredditSubreddits) { stringBuilder.append(s.getSubredditName()).append("+"); } if (stringBuilder.length() > 0) { stringBuilder.deleteCharAt(stringBuilder.length() - 1); } handler.post(() -> fetchPostFilterAndConcatenatecSubredditNamesListener.success(mergedPostFilter, stringBuilder.toString())); } else { handler.post(() -> fetchPostFilterAndConcatenatecSubredditNamesListener.success(mergedPostFilter, null)); } }); } public interface FetchPostFilterListerner { void success(PostFilter postFilter); } public interface FetchPostFilterAndConcatenatecSubredditNamesListener { void success(PostFilter postFilter, String concatenatedSubredditNames); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/FetchVideoLinkListener.java ================================================ package ml.docilealligator.infinityforreddit; import androidx.annotation.Nullable; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.thing.StreamableVideo; public interface FetchVideoLinkListener { default void onFetchRedditVideoLinkSuccess(Post post, String fileName) {} default void onFetchImgurVideoLinkSuccess(String videoUrl, String videoDownloadUrl, String fileName) {} default void onFetchRedgifsVideoLinkSuccess(String webm, String mp4) {} default void onFetchStreamableVideoLinkSuccess(StreamableVideo streamableVideo) {} default void onChangeFileName(String fileName) {} default void onFetchVideoFallbackDirectUrlSuccess(String videoFallbackDirectUrl) {} default void failed(@Nullable Integer messageRes) {} } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/Infinity.java ================================================ package ml.docilealligator.infinityforreddit; import android.app.Activity; import android.app.Application; import android.app.WallpaperColors; import android.app.WallpaperManager; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Typeface; import android.net.ConnectivityManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.WindowManager; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.ProcessLifecycleOwner; import com.evernote.android.state.StateSaver; import com.livefront.bridge.Bridge; import com.livefront.bridge.SavedStateHandler; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.activities.LockScreenActivity; import ml.docilealligator.infinityforreddit.broadcastreceivers.NetworkWifiStatusReceiver; import ml.docilealligator.infinityforreddit.broadcastreceivers.WallpaperChangeReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.events.ChangeAppLockEvent; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.events.ToggleSecureModeEvent; import ml.docilealligator.infinityforreddit.font.ContentFontFamily; import ml.docilealligator.infinityforreddit.font.FontFamily; import ml.docilealligator.infinityforreddit.font.TitleFontFamily; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.MaterialYouUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class Infinity extends Application implements LifecycleObserver { public Typeface typeface; public Typeface titleTypeface; public Typeface contentTypeface; private AppComponent mAppComponent; private NetworkWifiStatusReceiver mNetworkWifiStatusReceiver; private boolean appLock; private long appLockTimeout; private boolean canStartLockScreenActivity = false; private boolean isSecureMode; @Inject public RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("security") SharedPreferences mSecuritySharedPreferences; @Inject @Named("internal") SharedPreferences mInternalSharedPreferences; @Inject @Named("light_theme") SharedPreferences lightThemeSharedPreferences; @Inject @Named("dark_theme") SharedPreferences darkThemeSharedPreferences; @Inject @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor executor; @Override public void onCreate() { super.onCreate(); mAppComponent = DaggerAppComponent.factory() .create(this); mAppComponent.inject(this); APIUtils.initConfigurableFields(this); appLock = mSecuritySharedPreferences.getBoolean(SharedPreferencesUtils.APP_LOCK, false); appLockTimeout = Long.parseLong(mSecuritySharedPreferences.getString(SharedPreferencesUtils.APP_LOCK_TIMEOUT, "600000")); isSecureMode = mSecuritySharedPreferences.getBoolean(SharedPreferencesUtils.SECURE_MODE, false); try { if (mSharedPreferences.getString(SharedPreferencesUtils.FONT_FAMILY_KEY, FontFamily.Default.name()).equals(FontFamily.Custom.name())) { typeface = Typeface.createFromFile(getExternalFilesDir("fonts") + "/font_family.ttf"); } if (mSharedPreferences.getString(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY, TitleFontFamily.Default.name()).equals(TitleFontFamily.Custom.name())) { titleTypeface = Typeface.createFromFile(getExternalFilesDir("fonts") + "/title_font_family.ttf"); } if (mSharedPreferences.getString(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY, ContentFontFamily.Default.name()).equals(ContentFontFamily.Custom.name())) { contentTypeface = Typeface.createFromFile(getExternalFilesDir("fonts") + "/content_font_family.ttf"); } } catch (RuntimeException e) { e.printStackTrace(); Toast.makeText(this, R.string.unable_to_load_font, Toast.LENGTH_SHORT).show(); } registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityPreCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { if (activity instanceof CustomFontReceiver) { ((CustomFontReceiver) activity).setCustomFont(typeface, titleTypeface, contentTypeface); } } @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) { if (isSecureMode) { activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); } } @Override public void onActivityStarted(@NonNull Activity activity) { } @Override public void onActivityResumed(@NonNull Activity activity) { if (canStartLockScreenActivity && appLock && System.currentTimeMillis() - mSecuritySharedPreferences.getLong(SharedPreferencesUtils.LAST_FOREGROUND_TIME, 0) >= appLockTimeout && !(activity instanceof LockScreenActivity)) { Intent intent = new Intent(activity, LockScreenActivity.class); activity.startActivity(intent); } canStartLockScreenActivity = false; } @Override public void onActivityPaused(@NonNull Activity activity) { } @Override public void onActivityStopped(@NonNull Activity activity) { } @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) { } @Override public void onActivityDestroyed(@NonNull Activity activity) { } }); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); Bridge.initialize(getApplicationContext(), new SavedStateHandler() { @Override public void saveInstanceState(@NonNull Object target, @NonNull Bundle state) { StateSaver.saveInstanceState(target, state); } @Override public void restoreInstanceState(@NonNull Object target, @Nullable Bundle state) { StateSaver.restoreInstanceState(target, state); } }); EventBus.builder().addIndex(new EventBusIndex()).installDefaultEventBus(); EventBus.getDefault().register(this); mNetworkWifiStatusReceiver = new NetworkWifiStatusReceiver(() -> EventBus.getDefault().post(new ChangeNetworkStatusEvent(Utils.getConnectedNetwork(getApplicationContext())))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(mNetworkWifiStatusReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION), RECEIVER_NOT_EXPORTED); registerReceiver(new WallpaperChangeReceiver(mSharedPreferences), new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED), RECEIVER_NOT_EXPORTED); } else { registerReceiver(mNetworkWifiStatusReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); registerReceiver(new WallpaperChangeReceiver(mSharedPreferences), new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)); } if (mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_MATERIAL_YOU, false)) { int sentryColor = mInternalSharedPreferences.getInt(SharedPreferencesUtils.MATERIAL_YOU_SENTRY_COLOR, 0); boolean sentryColorHasChanged = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (sentryColor != getColor(android.R.color.system_accent1_100)) { sentryColorHasChanged = true; } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { WallpaperManager wallpaperManager = WallpaperManager.getInstance(this); WallpaperColors wallpaperColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM); if (wallpaperColors != null) { int colorPrimaryInt = wallpaperColors.getPrimaryColor().toArgb(); if (sentryColor != colorPrimaryInt) { sentryColorHasChanged = true; } } } if (sentryColorHasChanged) { MaterialYouUtils.changeThemeASync(this, executor, new Handler(Looper.getMainLooper()), redditDataRoomDatabase, customThemeWrapper, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, mInternalSharedPreferences, null); } } } @OnLifecycleEvent(Lifecycle.Event.ON_START) public void appInForeground() { canStartLockScreenActivity = true; } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void appInBackground() { if (appLock) { mSecuritySharedPreferences.edit().putLong(SharedPreferencesUtils.LAST_FOREGROUND_TIME, System.currentTimeMillis()).apply(); } } public AppComponent getAppComponent() { return mAppComponent; } public CustomThemeWrapper getCustomThemeWrapper() { return customThemeWrapper; } @Subscribe public void onToggleSecureModeEvent(ToggleSecureModeEvent secureModeEvent) { isSecureMode = secureModeEvent.isSecureMode; } @Subscribe public void onChangeAppLockEvent(ChangeAppLockEvent changeAppLockEvent) { appLock = changeAppLockEvent.appLock; appLockTimeout = changeAppLockEvent.appLockTimeout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/NetworkModule.java ================================================ package ml.docilealligator.infinityforreddit; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.concurrent.TimeUnit; import javax.inject.Named; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.network.AccessTokenAuthenticator; import ml.docilealligator.infinityforreddit.network.RedgifsAccessTokenAuthenticator; import ml.docilealligator.infinityforreddit.network.ServerAccessTokenAuthenticator; import ml.docilealligator.infinityforreddit.network.SortTypeConverterFactory; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.ConnectionPool; import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import retrofit2.Retrofit; import retrofit2.adapter.guava.GuavaCallAdapterFactory; import retrofit2.converter.scalars.ScalarsConverterFactory; @Module(includes = AppModule.class) abstract class NetworkModule { @Provides @Named("base") @Singleton static OkHttpClient provideBaseOkhttp(@Named("proxy") SharedPreferences mProxySharedPreferences) { boolean proxyEnabled = mProxySharedPreferences.getBoolean(SharedPreferencesUtils.PROXY_ENABLED, false); var builder = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .addInterceptor(chain -> { if (chain.request().header("User-Agent") == null) { return chain.proceed( chain.request() .newBuilder() .header("User-Agent", APIUtils.USER_AGENT) .build() ); } else { return chain.proceed(chain.request()); } }); if (proxyEnabled) { Proxy.Type proxyType = Proxy.Type.valueOf(mProxySharedPreferences.getString(SharedPreferencesUtils.PROXY_TYPE, "HTTP")); if (proxyType != Proxy.Type.DIRECT) { String proxyHost = mProxySharedPreferences.getString(SharedPreferencesUtils.PROXY_HOSTNAME, "127.0.0.1"); int proxyPort = Integer.parseInt(mProxySharedPreferences.getString(SharedPreferencesUtils.PROXY_PORT, "1080")); InetSocketAddress proxyAddr = new InetSocketAddress(proxyHost, proxyPort); Proxy proxy = new Proxy(proxyType, proxyAddr); builder.proxy(proxy); } } return builder.build(); } @Provides @Named("base") @Singleton static Retrofit provideBaseRetrofit(@Named("base") OkHttpClient okHttpClient) { return new Retrofit.Builder() .baseUrl(APIUtils.API_BASE_URI) .client(okHttpClient) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(SortTypeConverterFactory.create()) .addCallAdapterFactory(GuavaCallAdapterFactory.create()) .build(); } @Provides static ConnectionPool provideConnectionPool() { return new ConnectionPool(0, 1, TimeUnit.NANOSECONDS); } @Provides @Named("no_oauth") static Retrofit provideRetrofit(@Named("base") Retrofit retrofit) { return retrofit; } @Provides @Named("oauth") static Retrofit provideOAuthRetrofit(@Named("base") Retrofit retrofit, @Named("default") OkHttpClient okHttpClient) { return retrofit.newBuilder().baseUrl(APIUtils.OAUTH_API_BASE_URI).client(okHttpClient).build(); } @Provides @Named("default") @Singleton static OkHttpClient provideOkHttpClient(Context context, @Named("base") OkHttpClient httpClient, @Named("base") Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Named("current_account") SharedPreferences currentAccountSharedPreferences, ConnectionPool connectionPool) { return httpClient.newBuilder() // Fetch clientId once and pass it to the authenticator .authenticator(new AccessTokenAuthenticator(APIUtils.getClientId(context), retrofit, redditDataRoomDatabase, currentAccountSharedPreferences)) .connectionPool(connectionPool) .build(); } @Provides @Named("server") @Singleton static OkHttpClient provideServerOkHttpClient(@Named("base") OkHttpClient httpClient, RedditDataRoomDatabase redditDataRoomDatabase, @Named("current_account") SharedPreferences currentAccountSharedPreferences, ConnectionPool connectionPool) { return httpClient.newBuilder() .authenticator(new ServerAccessTokenAuthenticator(redditDataRoomDatabase, currentAccountSharedPreferences)) .connectionPool(connectionPool) .build(); } @Provides @Named("media3") @Singleton static OkHttpClient provideMedia3OkHttpClient(@Named("base") OkHttpClient httpClient, ConnectionPool connectionPool) { return httpClient.newBuilder() .connectionPool(connectionPool) .followRedirects(false) .addInterceptor(new Interceptor() { @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); int redirectCount = 0; while (isRedirect(response.code()) && redirectCount < 5) { String location = response.header("Location"); if (location == null) break; HttpUrl newUrl = response.request().url().resolve(location); if (newUrl == null) break; request = request.newBuilder() .url(newUrl) .build(); response.close(); // Close the previous response before continuing response = chain.proceed(request); redirectCount++; } return response; } private boolean isRedirect(int code) { return code == 301 || code == 302 || code == 303 || code == 307 || code == 308; } }) .build(); } @Provides @Named("oauth_without_authenticator") @Singleton static Retrofit provideOauthWithoutAuthenticatorRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder().baseUrl(APIUtils.OAUTH_API_BASE_URI).build(); } @Provides @Named("upload_media") @Singleton static Retrofit provideUploadMediaRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder().baseUrl(APIUtils.API_UPLOAD_MEDIA_URI).build(); } @Provides @Named("upload_video") @Singleton static Retrofit provideUploadVideoRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder().baseUrl(APIUtils.API_UPLOAD_VIDEO_URI).build(); } @Provides @Named("download_media") @Singleton static Retrofit provideDownloadRedditVideoRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder().baseUrl("http://localhost/").build(); } @Provides @Named("RedgifsAccessTokenAuthenticator") static Interceptor redgifsAccessTokenAuthenticator(@Named("current_account") SharedPreferences currentAccountSharedPreferences) { return new RedgifsAccessTokenAuthenticator(currentAccountSharedPreferences); } @Provides @Named("redgifs") @Singleton static Retrofit provideRedgifsRetrofit(@Named("RedgifsAccessTokenAuthenticator") Interceptor accessTokenAuthenticator, @Named("base") OkHttpClient httpClient, @Named("base") Retrofit retrofit, ConnectionPool connectionPool) { OkHttpClient.Builder okHttpClientBuilder = httpClient.newBuilder() .addInterceptor(chain -> chain.proceed( chain.request() .newBuilder() .header("User-Agent", APIUtils.USER_AGENT) .build() )) .addInterceptor(accessTokenAuthenticator) .connectionPool(connectionPool); return retrofit.newBuilder() .baseUrl(APIUtils.REDGIFS_API_BASE_URI) .client(okHttpClientBuilder.build()) .build(); } /*@Provides @Named("redgifs") @Singleton static Retrofit provideRedgifsRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder() .baseUrl(APIUtils.OH_MY_DL_BASE_URI) .build(); }*/ @Provides @Named("imgur") @Singleton static Retrofit provideImgurRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder().baseUrl(APIUtils.IMGUR_API_BASE_URI).build(); } @Provides @Named("vReddIt") @Singleton static Retrofit provideVReddItRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder().baseUrl("http://localhost/").build(); } @Provides @Named("streamable") @Singleton static Retrofit provideStreamableRetrofit(@Named("base") Retrofit retrofit) { return retrofit.newBuilder().baseUrl(APIUtils.STREAMABLE_API_BASE_URI).build(); } @Provides @Named("online_custom_themes") @Singleton static Retrofit provideOnlineCustomThemesRetrofit(@Named("base") Retrofit retrofit, @Named("server") OkHttpClient httpClient) { return retrofit.newBuilder().baseUrl(APIUtils.SERVER_API_BASE_URI).client(httpClient).build(); } @Provides @Singleton static StreamableAPI provideStreamableApi(@Named("streamable") Retrofit streamableRetrofit) { return streamableRetrofit.create(StreamableAPI.class); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/NetworkState.java ================================================ package ml.docilealligator.infinityforreddit; public class NetworkState { public static final NetworkState LOADED; public static final NetworkState LOADING; static { LOADED = new NetworkState(Status.SUCCESS, "Success"); LOADING = new NetworkState(Status.LOADING, "Loading"); } private final Status status; private final String msg; public NetworkState(Status status, String msg) { this.status = status; this.msg = msg; } public Status getStatus() { return status; } public String getMsg() { return msg; } public enum Status { LOADING, SUCCESS, FAILED } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/PostModerationActionHandler.kt ================================================ package ml.docilealligator.infinityforreddit import ml.docilealligator.infinityforreddit.post.Post interface PostModerationActionHandler { fun approvePost(post: Post, position: Int) fun removePost(post: Post, position: Int, isSpam: Boolean) fun toggleSticky(post: Post, position: Int) fun toggleLock(post: Post, position: Int) fun toggleNSFW(post: Post, position: Int) fun toggleSpoiler(post: Post, position: Int) fun toggleMod(post: Post, position: Int) fun toggleNotification(post: Post, position: Int) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/ProxyEnabledGlideModule.java ================================================ package ml.docilealligator.infinityforreddit; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.AppGlideModule; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.concurrent.TimeUnit; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.OkHttpClient; @GlideModule public class ProxyEnabledGlideModule extends AppGlideModule { @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { OkHttpClient.Builder builder = new OkHttpClient.Builder() .readTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS); SharedPreferences mProxySharedPreferences = context.getSharedPreferences(SharedPreferencesUtils.PROXY_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE); boolean proxyEnabled = mProxySharedPreferences.getBoolean(SharedPreferencesUtils.PROXY_ENABLED, false); if (proxyEnabled) { Proxy.Type proxyType = Proxy.Type.valueOf(mProxySharedPreferences.getString(SharedPreferencesUtils.PROXY_TYPE, "HTTP")); if (proxyType != Proxy.Type.DIRECT) { String proxyHost = mProxySharedPreferences.getString(SharedPreferencesUtils.PROXY_HOSTNAME, "127.0.0.1"); int proxyPort = Integer.parseInt(mProxySharedPreferences.getString(SharedPreferencesUtils.PROXY_PORT, "1080")); InetSocketAddress proxyAddr = new InetSocketAddress(proxyHost, proxyPort); Proxy proxy = new Proxy(proxyType, proxyAddr); builder.proxy(proxy); } } OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(builder.build()); registry.replace(GlideUrl.class, InputStream.class, factory); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/RecyclerViewContentScrollingInterface.java ================================================ package ml.docilealligator.infinityforreddit; public interface RecyclerViewContentScrollingInterface { void contentScrollUp(); void contentScrollDown(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/RedditDataRoomDatabase.java ================================================ package ml.docilealligator.infinityforreddit; import android.content.Context; import android.database.Cursor; import android.graphics.Color; import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.account.AccountDao; import ml.docilealligator.infinityforreddit.account.AccountDaoKt; import ml.docilealligator.infinityforreddit.comment.CommentDraft; import ml.docilealligator.infinityforreddit.comment.CommentDraftDao; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterDao; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsageDao; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeDao; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeDaoKt; import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubreddit; import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubredditDao; import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubredditDaoKt; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.multireddit.MultiRedditDao; import ml.docilealligator.infinityforreddit.multireddit.MultiRedditDaoKt; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterDao; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsageDao; import ml.docilealligator.infinityforreddit.readpost.ReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostDao; import ml.docilealligator.infinityforreddit.recentsearchquery.RecentSearchQuery; import ml.docilealligator.infinityforreddit.recentsearchquery.RecentSearchQueryDao; import ml.docilealligator.infinityforreddit.subreddit.SubredditDao; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditDao; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserDao; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.user.UserDao; import ml.docilealligator.infinityforreddit.user.UserData; @Database(entities = {Account.class, SubredditData.class, SubscribedSubredditData.class, UserData.class, SubscribedUserData.class, MultiReddit.class, CustomTheme.class, RecentSearchQuery.class, ReadPost.class, PostFilter.class, PostFilterUsage.class, AnonymousMultiredditSubreddit.class, CommentFilter.class, CommentFilterUsage.class, CommentDraft.class}, version = 31, exportSchema = false) @TypeConverters(Converters.class) public abstract class RedditDataRoomDatabase extends RoomDatabase { public static RedditDataRoomDatabase create(final Context context) { return Room.databaseBuilder(context.getApplicationContext(), RedditDataRoomDatabase.class, "reddit_data") .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, MIGRATION_20_21, MIGRATION_21_22, MIGRATION_22_23, MIGRATION_23_24, MIGRATION_24_25, MIGRATION_25_26, MIGRATION_26_27, MIGRATION_27_28, MIGRATION_28_29, MIGRATION_29_30, MIGRATION_30_31) .build(); } public abstract AccountDao accountDao(); public abstract AccountDaoKt accountDaoKt(); public abstract SubredditDao subredditDao(); public abstract SubscribedSubredditDao subscribedSubredditDao(); public abstract UserDao userDao(); public abstract SubscribedUserDao subscribedUserDao(); public abstract MultiRedditDao multiRedditDao(); public abstract MultiRedditDaoKt multiRedditDaoKt(); public abstract CustomThemeDao customThemeDao(); public abstract CustomThemeDaoKt customThemeDaoKt(); public abstract RecentSearchQueryDao recentSearchQueryDao(); public abstract ReadPostDao readPostDao(); public abstract PostFilterDao postFilterDao(); public abstract PostFilterUsageDao postFilterUsageDao(); public abstract AnonymousMultiredditSubredditDao anonymousMultiredditSubredditDao(); public abstract AnonymousMultiredditSubredditDaoKt anonymousMultiredditSubredditDaoKt(); public abstract CommentFilterDao commentFilterDao(); public abstract CommentFilterUsageDao commentFilterUsageDao(); public abstract CommentDraftDao commentDraftDao(); private static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE subscribed_subreddits" + " ADD COLUMN is_favorite INTEGER DEFAULT 0 NOT NULL"); database.execSQL("ALTER TABLE subscribed_users" + " ADD COLUMN is_favorite INTEGER DEFAULT 0 NOT NULL"); } }; private static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE subscribed_subreddits_temp " + "(id TEXT NOT NULL, name TEXT, icon TEXT, username TEXT NOT NULL, " + "is_favorite INTEGER NOT NULL, PRIMARY KEY(id, username), " + "FOREIGN KEY(username) REFERENCES accounts(username) ON DELETE CASCADE)"); database.execSQL( "INSERT INTO subscribed_subreddits_temp SELECT * FROM subscribed_subreddits"); database.execSQL("DROP TABLE subscribed_subreddits"); database.execSQL("ALTER TABLE subscribed_subreddits_temp RENAME TO subscribed_subreddits"); database.execSQL("CREATE TABLE subscribed_users_temp " + "(name TEXT NOT NULL, icon TEXT, username TEXT NOT NULL, " + "is_favorite INTEGER NOT NULL, PRIMARY KEY(name, username), " + "FOREIGN KEY(username) REFERENCES accounts(username) ON DELETE CASCADE)"); database.execSQL( "INSERT INTO subscribed_users_temp SELECT * FROM subscribed_users"); database.execSQL("DROP TABLE subscribed_users"); database.execSQL("ALTER TABLE subscribed_users_temp RENAME TO subscribed_users"); } }; private static final Migration MIGRATION_3_4 = new Migration(3, 4) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE multi_reddits" + "(path TEXT NOT NULL, username TEXT NOT NULL, name TEXT NOT NULL, " + "display_name TEXT NOT NULL, description TEXT, copied_from TEXT, " + "n_subscribers INTEGER NOT NULL, icon_url TEXT, created_UTC INTEGER NOT NULL, " + "visibility TEXT, over_18 INTEGER NOT NULL, is_subscriber INTEGER NOT NULL, " + "is_favorite INTEGER NOT NULL, PRIMARY KEY(path, username), " + "FOREIGN KEY(username) REFERENCES accounts(username) ON DELETE CASCADE)"); } }; private static final Migration MIGRATION_4_5 = new Migration(4, 5) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE subreddits" + " ADD COLUMN sidebar_description TEXT"); } }; private static final Migration MIGRATION_5_6 = new Migration(5, 6) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE custom_themes" + "(name TEXT NOT NULL PRIMARY KEY, is_light_theme INTEGER NOT NULL," + "is_dark_theme INTEGER NOT NULL, is_amoled_theme INTEGER NOT NULL, color_primary INTEGER NOT NULL," + "color_primary_dark INTEGER NOT NULL, color_accent INTEGER NOT NULL," + "color_primary_light_theme INTEGER NOT NULL, primary_text_color INTEGER NOT NULL," + "secondary_text_color INTEGER NOT NULL, post_title_color INTEGER NOT NULL," + "post_content_color INTEGER NOT NULL, comment_color INTEGER NOT NULL," + "button_text_color INTEGER NOT NULL, background_color INTEGER NOT NULL," + "card_view_background_color INTEGER NOT NULL, comment_background_color INTEGER NOT NULL," + "bottom_app_bar_background_color INTEGER NOT NULL, primary_icon_color INTEGER NOT NULL," + "post_icon_and_info_color INTEGER NOT NULL," + "comment_icon_and_info_color INTEGER NOT NULL, toolbar_primary_text_and_icon_color INTEGER NOT NULL," + "toolbar_secondary_text_color INTEGER NOT NULL, circular_progress_bar_background INTEGER NOT NULL," + "tab_layout_with_expanded_collapsing_toolbar_tab_background INTEGER NOT NULL," + "tab_layout_with_expanded_collapsing_toolbar_text_color INTEGER NOT NULL," + "tab_layout_with_expanded_collapsing_toolbar_tab_indicator INTEGER NOT NULL," + "tab_layout_with_collapsed_collapsing_toolbar_tab_background INTEGER NOT NULL," + "tab_layout_with_collapsed_collapsing_toolbar_text_color INTEGER NOT NULL," + "tab_layout_with_collapsed_collapsing_toolbar_tab_indicator INTEGER NOT NULL," + "nav_bar_color INTEGER NOT NULL, upvoted INTEGER NOT NULL, downvoted INTEGER NOT NULL," + "post_type_background_color INTEGER NOT NULL, post_type_text_color INTEGER NOT NULL," + "spoiler_background_color INTEGER NOT NULL, spoiler_text_color INTEGER NOT NULL," + "nsfw_background_color INTEGER NOT NULL, nsfw_text_color INTEGER NOT NULL," + "flair_background_color INTEGER NOT NULL, flair_text_color INTEGER NOT NULL," + "archived_tint INTEGER NOT NULL, locked_icon_tint INTEGER NOT NULL," + "crosspost_icon_tint INTEGER NOT NULL, stickied_post_icon_tint INTEGER NOT NULL, subscribed INTEGER NOT NULL," + "unsubscribed INTEGER NOT NULL, username INTEGER NOT NULL, subreddit INTEGER NOT NULL," + "author_flair_text_color INTEGER NOT NULL, submitter INTEGER NOT NULL," + "moderator INTEGER NOT NULL, single_comment_thread_background_color INTEGER NOT NULL," + "unread_message_background_color INTEGER NOT NULL, divider_color INTEGER NOT NULL," + "no_preview_link_background_color INTEGER NOT NULL," + "vote_and_reply_unavailable_button_color INTEGER NOT NULL," + "comment_vertical_bar_color_1 INTEGER NOT NULL, comment_vertical_bar_color_2 INTEGER NOT NULL," + "comment_vertical_bar_color_3 INTEGER NOT NULL, comment_vertical_bar_color_4 INTEGER NOT NULL," + "comment_vertical_bar_color_5 INTEGER NOT NULL, comment_vertical_bar_color_6 INTEGER NOT NULL," + "comment_vertical_bar_color_7 INTEGER NOT NULL, fab_icon_color INTEGER NOT NULL," + "chip_text_color INTEGER NOT NULL, is_light_status_bar INTEGER NOT NULL," + "is_light_nav_bar INTEGER NOT NULL," + "is_change_status_bar_icon_color_after_toolbar_collapsed_in_immersive_interface INTEGER NOT NULL)"); } }; private static final Migration MIGRATION_6_7 = new Migration(6, 7) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE custom_themes ADD COLUMN awards_background_color INTEGER DEFAULT " + Color.parseColor("#EEAB02") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN awards_text_color INTEGER DEFAULT " + Color.parseColor("#FFFFFF") + " NOT NULL"); } }; private static final Migration MIGRATION_7_8 = new Migration(7, 8) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE users_temp " + "(name TEXT NOT NULL PRIMARY KEY, icon TEXT, banner TEXT, " + "link_karma INTEGER NOT NULL, comment_karma INTEGER DEFAULT 0 NOT NULL, created_utc INTEGER DEFAULT 0 NOT NULL," + "is_gold INTEGER NOT NULL, is_friend INTEGER NOT NULL, can_be_followed INTEGER NOT NULL," + "description TEXT)"); database.execSQL( "INSERT INTO users_temp(name, icon, banner, link_karma, is_gold, is_friend, can_be_followed) SELECT * FROM users"); database.execSQL("DROP TABLE users"); database.execSQL("ALTER TABLE users_temp RENAME TO users"); database.execSQL("ALTER TABLE subreddits" + " ADD COLUMN created_utc INTEGER DEFAULT 0 NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN bottom_app_bar_icon_color INTEGER DEFAULT " + Color.parseColor("#000000") + " NOT NULL"); } }; private static final Migration MIGRATION_8_9 = new Migration(8, 9) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN link_color INTEGER DEFAULT " + Color.parseColor("#FF1868") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN received_message_text_color INTEGER DEFAULT " + Color.parseColor("#FFFFFF") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN sent_message_text_color INTEGER DEFAULT " + Color.parseColor("#FFFFFF") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN received_message_background_color INTEGER DEFAULT " + Color.parseColor("#4185F4") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN sent_message_background_color INTEGER DEFAULT " + Color.parseColor("#31BF7D") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN send_message_icon_color INTEGER DEFAULT " + Color.parseColor("#4185F4") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN fully_collapsed_comment_background_color INTEGER DEFAULT " + Color.parseColor("#8EDFBA") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN awarded_comment_background_color INTEGER DEFAULT " + Color.parseColor("#FFF162") + " NOT NULL"); } }; private static final Migration MIGRATION_9_10 = new Migration(9, 10) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE recent_search_queries" + "(username TEXT NOT NULL, search_query TEXT NOT NULL, PRIMARY KEY(username, search_query), " + "FOREIGN KEY(username) REFERENCES accounts(username) ON DELETE CASCADE)"); database.execSQL("ALTER TABLE subreddits" + " ADD COLUMN suggested_comment_sort TEXT"); database.execSQL("ALTER TABLE subreddits" + " ADD COLUMN over18 INTEGER DEFAULT 0 NOT NULL"); } }; private static final Migration MIGRATION_10_11 = new Migration(10, 11) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE users" + " ADD COLUMN awarder_karma INTEGER DEFAULT 0 NOT NULL"); database.execSQL("ALTER TABLE users" + " ADD COLUMN awardee_karma INTEGER DEFAULT 0 NOT NULL"); database.execSQL("ALTER TABLE users" + " ADD COLUMN total_karma INTEGER DEFAULT 0 NOT NULL"); database.execSQL("ALTER TABLE users" + " ADD COLUMN over_18 INTEGER DEFAULT 0 NOT NULL"); } }; private static final Migration MIGRATION_11_12 = new Migration(11, 12) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE subreddit_filter" + "(subreddit_name TEXT NOT NULL, type INTEGER NOT NULL, PRIMARY KEY(subreddit_name, type))"); } }; private static final Migration MIGRATION_12_13 = new Migration(12, 13) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE custom_themes" + " ADD COLUMN no_preview_post_type_icon_tint INTEGER DEFAULT " + Color.parseColor("#808080") + " NOT NULL"); } }; private static final Migration MIGRATION_13_14 = new Migration(13, 14) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE read_posts" + "(username TEXT NOT NULL, id TEXT NOT NULL, PRIMARY KEY(username, id), " + "FOREIGN KEY(username) REFERENCES accounts(username) ON DELETE CASCADE)"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN read_post_title_color INTEGER DEFAULT " + Color.parseColor("#9D9D9D") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN read_post_content_color INTEGER DEFAULT " + Color.parseColor("#9D9D9D") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN read_post_card_view_background_color INTEGER DEFAULT " + Color.parseColor("#F5F5F5") + " NOT NULL"); } }; private static final Migration MIGRATION_14_15 = new Migration(14, 15) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE post_filter" + "(name TEXT NOT NULL PRIMARY KEY, max_vote INTEGER NOT NULL, min_vote INTEGER NOT NULL, " + "max_comments INTEGER NOT NULL, min_comments INTEGER NOT NULL, max_awards INTEGER NOT NULL, " + "min_awards INTEGER NOT NULL, only_nsfw INTEGER NOT NULL, only_spoiler INTEGER NOT NULL, " + "post_title_excludes_regex TEXT, post_title_excludes_strings TEXT, exclude_subreddits TEXT, " + "exclude_users TEXT, contain_flairs TEXT, exclude_flairs TEXT, contain_text_type INTEGER NOT NULL, " + "contain_link_type INTEGER NOT NULL, contain_image_type INTEGER NOT NULL, " + "contain_gif_type INTEGER NOT NULL, contain_video_type INTEGER NOT NULL, " + "contain_gallery_type INTEGER NOT NULL)"); database.execSQL("CREATE TABLE post_filter_usage (name TEXT NOT NULL, usage INTEGER NOT NULL, " + "name_of_usage TEXT NOT NULL, PRIMARY KEY(name, usage, name_of_usage), FOREIGN KEY(name) REFERENCES post_filter(name) ON DELETE CASCADE)"); } }; private static final Migration MIGRATION_15_16 = new Migration(15, 16) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("DROP TABLE subreddit_filter"); } }; private static final Migration MIGRATION_16_17 = new Migration(16, 17) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("UPDATE accounts SET is_current_user = 0"); } }; private static final Migration MIGRATION_17_18 = new Migration(17, 18) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE custom_themes ADD COLUMN current_user INTEGER DEFAULT " + Color.parseColor("#00D5EA") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN upvote_ratio_icon_tint INTEGER DEFAULT " + Color.parseColor("#0256EE") + " NOT NULL"); } }; private static final Migration MIGRATION_18_19 = new Migration(18, 19) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("INSERT INTO accounts(username, karma, is_current_user) VALUES (\"-\", 0, 0)"); } }; private static final Migration MIGRATION_19_20 = new Migration(19, 20) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE post_filter ADD COLUMN exclude_domains TEXT"); } }; private static final Migration MIGRATION_20_21 = new Migration(20, 21) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE anonymous_multireddit_subreddits (path TEXT NOT NULL, " + "username TEXT NOT NULL, subreddit_name TEXT NOT NULL, " + "PRIMARY KEY(path, username, subreddit_name), FOREIGN KEY(path, username) REFERENCES multi_reddits(path, username) ON DELETE CASCADE ON UPDATE CASCADE)"); database.execSQL("ALTER TABLE recent_search_queries ADD COLUMN time INTEGER DEFAULT 0 NOT NULL"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN media_indicator_icon_color INTEGER DEFAULT " + Color.parseColor("#FFFFFF") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN media_indicator_background_color INTEGER DEFAULT " + Color.parseColor("#000000") + " NOT NULL"); database.execSQL("ALTER TABLE post_filter ADD COLUMN post_title_contains_strings TEXT"); database.execSQL("ALTER TABLE post_filter ADD COLUMN post_title_contains_regex TEXT"); database.execSQL("ALTER TABLE post_filter ADD COLUMN contain_domains TEXT"); } }; private static final Migration MIGRATION_21_22 = new Migration(21, 22) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE users ADD COLUMN title TEXT"); } }; private static final Migration MIGRATION_22_23 = new Migration(22, 23) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE read_posts ADD COLUMN time INTEGER DEFAULT 0 NOT NULL"); Cursor cursor = database.query("SELECT * FROM read_posts"); int row = 0; database.beginTransaction(); try { while (cursor.moveToNext()) { int index; index = cursor.getColumnIndexOrThrow("username"); String username = cursor.getString(index); index = cursor.getColumnIndexOrThrow("id"); String id = cursor.getString(index); database.execSQL("UPDATE read_posts SET time = " + row++ + " WHERE username = '" + username + "' AND id = '" + id + "'"); } database.setTransactionSuccessful(); } finally { database.endTransaction(); } } }; private static final Migration MIGRATION_23_24 = new Migration(23, 24) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE custom_themes ADD COLUMN filled_card_view_background_color INTEGER DEFAULT " + Color.parseColor("#E6F4FF") + " NOT NULL"); database.execSQL("ALTER TABLE custom_themes ADD COLUMN read_post_filled_card_view_background_color INTEGER DEFAULT " + Color.parseColor("#F5F5F5") + " NOT NULL"); } }; private static final Migration MIGRATION_24_25 = new Migration(24, 25) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE comment_filter " + "(name TEXT NOT NULL PRIMARY KEY, max_vote INTEGER NOT NULL, min_vote INTEGER NOT NULL, exclude_strings TEXT, exclude_users TEXT)"); database.execSQL("CREATE TABLE comment_filter_usage (name TEXT NOT NULL, usage INTEGER NOT NULL, " + "name_of_usage TEXT NOT NULL, PRIMARY KEY(name, usage, name_of_usage), FOREIGN KEY(name) REFERENCES comment_filter(name) ON DELETE CASCADE)"); } }; private static final Migration MIGRATION_25_26 = new Migration(25, 26) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE comment_filter ADD COLUMN display_mode INTEGER DEFAULT 0 NOT NULL"); } }; private static final Migration MIGRATION_26_27 = new Migration(26, 27) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE recent_search_queries ADD COLUMN search_in_subreddit_or_user_name TEXT"); database.execSQL("ALTER TABLE recent_search_queries ADD COLUMN search_in_multireddit_path TEXT"); database.execSQL("ALTER TABLE recent_search_queries ADD COLUMN search_in_multireddit_display_name TEXT"); database.execSQL("ALTER TABLE recent_search_queries ADD COLUMN search_in_thing_type INTEGER DEFAULT 0 NOT NULL"); } }; private static final Migration MIGRATION_27_28 = new Migration(27, 28) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE INDEX index_subscribed_subreddits_username ON subscribed_subreddits(username)"); database.execSQL("CREATE INDEX index_subscribed_users_username ON subscribed_users(username)"); database.execSQL("CREATE INDEX index_multi_reddits_username ON multi_reddits(username)"); } }; private static final Migration MIGRATION_28_29 = new Migration(28, 29) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE post_filter ADD COLUMN contain_users TEXT"); database.execSQL("ALTER TABLE post_filter ADD COLUMN contain_subreddits TEXT"); } }; private static final Migration MIGRATION_29_30 = new Migration(29, 30) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE accounts ADD COLUMN is_mod INTEGER DEFAULT 0 NOT NULL"); database.execSQL("CREATE TABLE comment_draft(" + "full_name TEXT NOT NULL, " + "content TEXT NOT NULL, " + "last_updated INTEGER NOT NULL," + "draft_type TEXT NOT NULL," + "PRIMARY KEY (full_name, draft_type))"); } }; private static final Migration MIGRATION_30_31 = new Migration(30, 31) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE anonymous_multireddit_subreddits ADD COLUMN icon_url TEXT"); } }; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/SaveMemoryCenterInisdeDownsampleStrategy.java ================================================ package ml.docilealligator.infinityforreddit; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; public class SaveMemoryCenterInisdeDownsampleStrategy extends DownsampleStrategy { private int threshold; public SaveMemoryCenterInisdeDownsampleStrategy(int threshold) { this.threshold = threshold; } @Override public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { int originalSourceWidth = sourceWidth; int originalSourceHeight = sourceHeight; if (sourceWidth * sourceHeight > threshold) { int divisor = 2; do { sourceWidth /= divisor; sourceHeight /= divisor; } while (sourceWidth * sourceHeight > threshold); } float widthPercentage = (float) requestedWidth / (float) sourceWidth; float heightPercentage = (float) requestedHeight / (float) sourceHeight; return Math.min((float) sourceWidth / (float) originalSourceWidth, (float) sourceHeight / (float) originalSourceHeight) * Math.min(1.f, Math.min(widthPercentage, heightPercentage)); } @Override public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return SampleSizeRounding.MEMORY; } public void setThreshold(int threshold) { this.threshold = threshold; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/SetAsWallpaperCallback.java ================================================ package ml.docilealligator.infinityforreddit; public interface SetAsWallpaperCallback { void setToHomeScreen(int viewPagerPosition); void setToLockScreen(int viewPagerPosition); void setToBoth(int viewPagerPosition); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/SingleLiveEvent.java ================================================ package ml.docilealligator.infinityforreddit; import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import java.util.concurrent.atomic.AtomicBoolean; public class SingleLiveEvent extends MutableLiveData { private static final String TAG = "SingleLiveEvent"; private final AtomicBoolean mPending = new AtomicBoolean(false); @MainThread public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer observer) { if (hasActiveObservers()) { Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); } // Observe the internal MutableLiveData super.observe(owner, new Observer() { @Override public void onChanged(@Nullable T t) { if (mPending.compareAndSet(true, false)) { observer.onChanged(t); } } }); } @MainThread public void setValue(@Nullable T t) { mPending.set(true); super.setValue(t); } /** * Used for cases where T is Void, to make calls cleaner. */ @MainThread public void call() { setValue(null); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/VideoLinkFetcher.java ================================================ package ml.docilealligator.infinityforreddit; import android.content.SharedPreferences; import android.net.Uri; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.annotation.WorkerThread; import androidx.media3.common.util.UnstableApi; import org.apache.commons.io.FilenameUtils; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Provider; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.apis.VReddIt; import ml.docilealligator.infinityforreddit.post.FetchPost; import ml.docilealligator.infinityforreddit.post.FetchStreamableVideo; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.thing.FetchRedgifsVideoLinks; import ml.docilealligator.infinityforreddit.thing.StreamableVideo; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class VideoLinkFetcher { public static void fetchVideoLink(Executor executor, Handler handler, Retrofit retrofit, Retrofit vReddItRetrofit, Retrofit redgifsRetrofit, Provider streamableApiProvider, SharedPreferences currentAccountSharedPreferences, int videoType, @Nullable String redgifsId, @Nullable String vRedditItUrl, @Nullable String shortCode, FetchVideoLinkListener fetchVideoLinkListener) { switch (videoType) { case ViewVideoActivity.VIDEO_TYPE_STREAMABLE: FetchStreamableVideo.fetchStreamableVideo(executor, handler, streamableApiProvider, shortCode, fetchVideoLinkListener); break; case ViewVideoActivity.VIDEO_TYPE_REDGIFS: FetchRedgifsVideoLinks.fetchRedgifsVideoLinks(executor, handler, redgifsRetrofit, currentAccountSharedPreferences, redgifsId, fetchVideoLinkListener); break; case ViewVideoActivity.VIDEO_TYPE_V_REDD_IT: loadVReddItVideo(executor, handler, retrofit, vReddItRetrofit, redgifsRetrofit, streamableApiProvider, currentAccountSharedPreferences, vRedditItUrl, fetchVideoLinkListener); break; } } @WorkerThread @Nullable public static String fetchVideoLinkSync(Retrofit redgifsRetrofit, Provider streamableApiProvider, SharedPreferences currentAccountSharedPreferences, int videoType, @Nullable String redgifsId, @Nullable String shortCode) { if (videoType == ViewVideoActivity.VIDEO_TYPE_STREAMABLE) { StreamableVideo streamableVideo = FetchStreamableVideo.fetchStreamableVideoSync(streamableApiProvider, shortCode); return streamableVideo == null ? null : (streamableVideo.mp4 == null ? null : streamableVideo.mp4.url); } else if (videoType == ViewVideoActivity.VIDEO_TYPE_REDGIFS) { return FetchRedgifsVideoLinks.fetchRedgifsVideoLinkSync(redgifsRetrofit, currentAccountSharedPreferences, redgifsId); } return null; } public static void loadVReddItVideo(Executor executor, Handler handler, Retrofit retrofit, Retrofit mVReddItRetrofit, Retrofit redgifsRetrofit, Provider streamableApiProvider, SharedPreferences currentAccountSharedPreferences, String vRedditItUrl, FetchVideoLinkListener fetchVideoLinkListener) { mVReddItRetrofit.create(VReddIt.class).getRedirectUrl(vRedditItUrl).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { Uri redirectUri = Uri.parse(response.raw().request().url().toString()); String redirectPath = redirectUri.getPath(); if (redirectPath != null && (redirectPath.matches("/r/\\w+/comments/\\w+/?\\w+/?") || redirectPath.matches("/user/\\w+/comments/\\w+/?\\w+/?"))) { List segments = redirectUri.getPathSegments(); int commentsIndex = segments.lastIndexOf("comments"); String postId = segments.get(commentsIndex + 1); FetchPost.fetchPost(executor, handler, retrofit, postId, null, Account.ANONYMOUS_ACCOUNT, new FetchPost.FetchPostListener() { @OptIn(markerClass = UnstableApi.class) @Override public void fetchPostSuccess(Post post) { fetchVideoLinkListener.onFetchVideoFallbackDirectUrlSuccess(post.getVideoFallBackDirectUrl()); if (post.isRedgifs()) { String redgifsId = post.getRedgifsId(); if (redgifsId != null && redgifsId.contains("-")) { redgifsId = redgifsId.substring(0, redgifsId.indexOf('-')); } fetchVideoLinkListener.onChangeFileName("Redgifs-" + redgifsId + ".mp4"); FetchRedgifsVideoLinks.fetchRedgifsVideoLinks(executor, handler, redgifsRetrofit, currentAccountSharedPreferences, redgifsId, fetchVideoLinkListener); } else if (post.isStreamable()) { String shortCode = post.getStreamableShortCode(); fetchVideoLinkListener.onChangeFileName("Streamable-" + shortCode + ".mp4"); FetchStreamableVideo.fetchStreamableVideo(executor, handler, streamableApiProvider, shortCode, fetchVideoLinkListener); } else if (post.isImgur()) { String videoDownloadUrl = post.getVideoDownloadUrl(); String videoFileName = "Imgur-" + FilenameUtils.getName(videoDownloadUrl); fetchVideoLinkListener.onFetchImgurVideoLinkSuccess(post.getVideoUrl(), post.getVideoDownloadUrl(), videoFileName); } else { if (post.getVideoUrl() != null) { String videoFileName = post.getSubredditName() + "-" + post.getId() + ".mp4"; fetchVideoLinkListener.onFetchRedditVideoLinkSuccess(post, videoFileName); } else { fetchVideoLinkListener.failed(R.string.error_fetching_v_redd_it_video_cannot_get_video_url); } } } @Override public void fetchPostFailed() { fetchVideoLinkListener.failed(R.string.error_fetching_v_redd_it_video_cannot_get_post); } }); } else { fetchVideoLinkListener.failed(R.string.error_fetching_v_redd_it_video_cannot_get_post_id); } } else { fetchVideoLinkListener.failed(R.string.error_fetching_v_redd_it_video_cannot_get_redirect_url); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchVideoLinkListener.failed(R.string.error_fetching_v_redd_it_video_cannot_get_redirect_url); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/WallpaperSetter.java ================================================ package ml.docilealligator.infinityforreddit; import android.app.WallpaperManager; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Handler; import android.view.WindowManager; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.asynctasks.SetAsWallpaper; public class WallpaperSetter { public static final int HOME_SCREEN = 0; public static final int LOCK_SCREEN = 1; public static final int BOTH_SCREENS = 2; public static void set(Executor executor, Handler handler, String url, int setTo, Context context, SetWallpaperListener setWallpaperListener) { Toast.makeText(context, R.string.save_image_first, Toast.LENGTH_SHORT).show(); WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Glide.with(context).asBitmap().load(url).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { SetAsWallpaper.setAsWallpaper(executor, handler, resource, setTo, wallpaperManager, windowManager, setWallpaperListener); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); } public interface SetWallpaperListener { void success(); void failed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/account/Account.java ================================================ package ml.docilealligator.infinityforreddit.account; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import com.google.gson.Gson; import com.google.gson.JsonParseException; @Entity(tableName = "accounts") public class Account implements Parcelable { public static final String ANONYMOUS_ACCOUNT = "-"; @PrimaryKey @NonNull @ColumnInfo(name = "username") private final String accountName; @ColumnInfo(name = "profile_image_url") private final String profileImageUrl; @ColumnInfo(name = "banner_image_url") private final String bannerImageUrl; @ColumnInfo(name = "karma") private final int karma; @ColumnInfo(name = "access_token") private String accessToken; @ColumnInfo(name = "refresh_token") private final String refreshToken; @ColumnInfo(name = "code") private final String code; @ColumnInfo(name = "is_current_user") private final boolean isCurrentUser; @ColumnInfo(name = "is_mod") private final boolean isMod; @Ignore protected Account(Parcel in) { accountName = in.readString(); profileImageUrl = in.readString(); bannerImageUrl = in.readString(); karma = in.readInt(); accessToken = in.readString(); refreshToken = in.readString(); code = in.readString(); isCurrentUser = in.readByte() != 0; isMod = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public Account createFromParcel(Parcel in) { return new Account(in); } @Override public Account[] newArray(int size) { return new Account[size]; } }; @Ignore public static Account getAnonymousAccount() { return new Account(Account.ANONYMOUS_ACCOUNT, null, null, null, null, null, 0, false, false); } public Account(@NonNull String accountName, String accessToken, String refreshToken, String code, String profileImageUrl, String bannerImageUrl, int karma, boolean isCurrentUser, boolean isMod) { this.accountName = accountName; this.accessToken = accessToken; this.refreshToken = refreshToken; this.code = code; this.profileImageUrl = profileImageUrl; this.bannerImageUrl = bannerImageUrl; this.karma = karma; this.isCurrentUser = isCurrentUser; this.isMod = isMod; } @NonNull public String getAccountName() { return accountName; } public String getProfileImageUrl() { return profileImageUrl; } public String getBannerImageUrl() { return bannerImageUrl; } public int getKarma() { return karma; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getRefreshToken() { return refreshToken; } public String getCode() { return code; } public boolean isCurrentUser() { return isCurrentUser; } public boolean isMod() { return isMod; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(accountName); dest.writeString(profileImageUrl); dest.writeString(bannerImageUrl); dest.writeInt(karma); dest.writeString(accessToken); dest.writeString(refreshToken); dest.writeString(code); dest.writeByte((byte) (isCurrentUser ? 1 : 0)); dest.writeByte((byte) (isMod ? 1 : 0)); } public String getJSONModel() { return new Gson().toJson(this); } public static Account fromJson(String json) throws JsonParseException { return new Gson().fromJson(json, Account.class); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/account/AccountDao.java ================================================ package ml.docilealligator.infinityforreddit.account; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; @Dao public interface AccountDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(Account account); @Query("SELECT EXISTS (SELECT 1 FROM accounts WHERE username = '-')") boolean isAnonymousAccountInserted(); @Query("SELECT * FROM accounts WHERE username != '-'") LiveData> getAllAccountsLiveData(); @Query("SELECT * FROM accounts WHERE username != '-'") List getAllAccounts(); @Query("SELECT * FROM accounts WHERE is_current_user = 0 AND username != '-'") List getAllNonCurrentAccounts(); @Query("UPDATE accounts SET is_current_user = 0 WHERE is_current_user = 1 AND username != '-'") void markAllAccountsNonCurrent(); @Query("DELETE FROM accounts WHERE is_current_user = 1 AND username != '-'") void deleteCurrentAccount(); @Query("DELETE FROM accounts WHERE username = :accountName") void deleteAccount(String accountName); @Query("DELETE FROM accounts WHERE username != '-'") void deleteAllAccounts(); @Query("SELECT * FROM accounts WHERE username = :username COLLATE NOCASE LIMIT 1") LiveData getAccountLiveData(String username); @Query("SELECT * FROM accounts WHERE username = :username COLLATE NOCASE LIMIT 1") Account getAccountData(String username); @Query("SELECT * FROM accounts WHERE is_current_user = 1 AND username != '-' LIMIT 1") Account getCurrentAccount(); @Query("SELECT * FROM accounts WHERE is_current_user = 1 AND username != '-' LIMIT 1") LiveData getCurrentAccountLiveData(); @Query("UPDATE accounts SET profile_image_url = :profileImageUrl, banner_image_url = :bannerImageUrl, " + "karma = :karma, is_mod = :isMod WHERE username = :username") void updateAccountInfo(String username, String profileImageUrl, String bannerImageUrl, int karma, boolean isMod); @Query("SELECT * FROM accounts WHERE is_current_user = 0 AND username != '-' ORDER BY username COLLATE NOCASE ASC") LiveData> getAccountsExceptCurrentAccountLiveData(); @Query("UPDATE accounts SET is_current_user = 1 WHERE username = :username") void markAccountCurrent(String username); @Query("UPDATE accounts SET access_token = :accessToken, refresh_token = :refreshToken WHERE username = :username") void updateAccessTokenAndRefreshToken(String username, String accessToken, String refreshToken); @Query("UPDATE accounts SET access_token = :accessToken WHERE username = :username") void updateAccessToken(String username, String accessToken); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/account/AccountDaoKt.kt ================================================ package ml.docilealligator.infinityforreddit.account import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @Dao interface AccountDaoKt { @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) suspend fun insert(account: Account) @Query("SELECT EXISTS (SELECT 1 FROM accounts WHERE username = '-')") suspend fun isAnonymousAccountInserted(): Boolean } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/account/AccountRepository.java ================================================ package ml.docilealligator.infinityforreddit.account; import androidx.lifecycle.LiveData; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class AccountRepository { private final Executor mExecutor; private final AccountDao mAccountDao; private final LiveData> mAccountsExceptCurrentAccountLiveData; private final LiveData mCurrentAccountLiveData; private final LiveData> mAllAccountsLiveData; AccountRepository(Executor executor, RedditDataRoomDatabase redditDataRoomDatabase) { mExecutor = executor; mAccountDao = redditDataRoomDatabase.accountDao(); mAccountsExceptCurrentAccountLiveData = mAccountDao.getAccountsExceptCurrentAccountLiveData(); mCurrentAccountLiveData = mAccountDao.getCurrentAccountLiveData(); mAllAccountsLiveData = mAccountDao.getAllAccountsLiveData(); } public LiveData> getAccountsExceptCurrentAccountLiveData() { return mAccountsExceptCurrentAccountLiveData; } public LiveData getCurrentAccountLiveData() { return mCurrentAccountLiveData; } public LiveData> getAllAccountsLiveData() { return mAllAccountsLiveData; } public void insert(Account account) { mExecutor.execute(() -> mAccountDao.insert(account)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/account/AccountViewModel.java ================================================ package ml.docilealligator.infinityforreddit.account; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class AccountViewModel extends ViewModel { private final AccountRepository mAccountRepository; private final LiveData> mAccountsExceptCurrentAccountLiveData; private final LiveData mCurrentAccountLiveData; private final LiveData> mAllAccountsLiveData; public AccountViewModel(Executor executor, RedditDataRoomDatabase redditDataRoomDatabase) { mAccountRepository = new AccountRepository(executor, redditDataRoomDatabase); mAccountsExceptCurrentAccountLiveData = mAccountRepository.getAccountsExceptCurrentAccountLiveData(); mCurrentAccountLiveData = mAccountRepository.getCurrentAccountLiveData(); mAllAccountsLiveData = mAccountRepository.getAllAccountsLiveData(); } public LiveData> getAccountsExceptCurrentAccountLiveData() { return mAccountsExceptCurrentAccountLiveData; } public LiveData getCurrentAccountLiveData() { return mCurrentAccountLiveData; } public LiveData> getAllAccountsLiveData() { return mAllAccountsLiveData; } public void insert(Account userData) { mAccountRepository.insert(userData); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Executor mExecutor; private final RedditDataRoomDatabase mRedditDataRoomDatabase; public Factory(Executor executor, RedditDataRoomDatabase redditDataRoomDatabase) { mExecutor = executor; mRedditDataRoomDatabase = redditDataRoomDatabase; } @Override public T create(Class modelClass) { //noinspection unchecked return (T) new AccountViewModel(mExecutor, mRedditDataRoomDatabase); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/account/FetchMyInfo.java ================================================ package ml.docilealligator.infinityforreddit.account; import android.os.Handler; import android.text.Html; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchMyInfo { public static void fetchAccountInfo(final Executor executor, final Handler handler, final Retrofit retrofit, final RedditDataRoomDatabase redditDataRoomDatabase, final String accessToken, final FetchMyInfoListener fetchMyInfoListener) { retrofit.create(RedditAPI.class).getMyInfo(APIUtils.getOAuthHeader(accessToken)).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { try { JSONObject jsonResponse = new JSONObject(response.body()); String name = jsonResponse.getString(JSONUtils.NAME_KEY); String profileImageUrl = Html.fromHtml(jsonResponse.getString(JSONUtils.ICON_IMG_KEY)).toString(); String bannerImageUrl = !jsonResponse.isNull(JSONUtils.SUBREDDIT_KEY) ? Html.fromHtml(jsonResponse.getJSONObject(JSONUtils.SUBREDDIT_KEY).getString(JSONUtils.BANNER_IMG_KEY)).toString() : null; int karma = jsonResponse.getInt(JSONUtils.TOTAL_KARMA_KEY); boolean isMod = jsonResponse.getBoolean(JSONUtils.IS_MOD_KEY); redditDataRoomDatabase.accountDao().updateAccountInfo(name, profileImageUrl, bannerImageUrl, karma, isMod); handler.post(() -> fetchMyInfoListener.onFetchMyInfoSuccess(name, profileImageUrl, bannerImageUrl, karma, isMod)); } catch (JSONException e) { handler.post(() -> fetchMyInfoListener.onFetchMyInfoFailed(true)); } }); } else { fetchMyInfoListener.onFetchMyInfoFailed(false); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { fetchMyInfoListener.onFetchMyInfoFailed(false); } }); } public interface FetchMyInfoListener { void onFetchMyInfoSuccess(String name, String profileImageUrl, String bannerImageUrl, int karma, boolean isMod); void onFetchMyInfoFailed(boolean parseFailed); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/AccountPostsActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.Objects; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityAccountPostsBinding; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.FragmentCommunicator; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.fragments.PostFragmentBase; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class AccountPostsActivity extends BaseActivity implements SortTypeSelectionCallback, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, ActivityToolbarInterface { static final String EXTRA_USER_WHERE = "EUW"; private static final String FRAGMENT_OUT_STATE = "FOS"; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private String mUserWhere; private Fragment mFragment; private PostLayoutBottomSheetFragment postLayoutBottomSheetFragment; private ActivityAccountPostsBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityAccountPostsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.accountPostsAppbarLayout); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.accountPostsToolbar, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.accountPostsFrameLayout.setPadding(allInsets.left, 0, allInsets.right, 0); return insets; } }); //adjustToolbar(binding.accountPostsToolbar); } } mUserWhere = getIntent().getExtras().getString(EXTRA_USER_WHERE); switch (Objects.requireNonNull(mUserWhere)) { case PostPagingSource.USER_WHERE_UPVOTED -> binding.accountPostsToolbar.setTitle(R.string.upvoted); case PostPagingSource.USER_WHERE_DOWNVOTED -> binding.accountPostsToolbar.setTitle(R.string.downvoted); case PostPagingSource.USER_WHERE_HIDDEN -> binding.accountPostsToolbar.setTitle(R.string.hidden); } setSupportActionBar(binding.accountPostsToolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.accountPostsToolbar); postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); if (savedInstanceState != null) { mFragment = getSupportFragmentManager().getFragment(savedInstanceState, FRAGMENT_OUT_STATE); getSupportFragmentManager().beginTransaction() .replace(binding.accountPostsFrameLayout.getId(), mFragment) .commit(); } else { initializeFragment(); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mFragment != null) { return ((PostFragmentBase) mFragment).handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.accountPostsCoordinatorLayout.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme( binding.accountPostsAppbarLayout, binding.accountPostsCollapsingToolbarLayout, binding.accountPostsToolbar); applyAppBarScrollFlagsIfApplicable(binding.accountPostsCollapsingToolbarLayout); } private void initializeFragment() { mFragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_USER); bundle.putString(PostFragment.EXTRA_USER_NAME, accountName); bundle.putString(PostFragment.EXTRA_USER_WHERE, mUserWhere); bundle.putBoolean(PostFragment.EXTRA_DISABLE_READ_POSTS, true); mFragment.setArguments(bundle); getSupportFragmentManager().beginTransaction() .replace(binding.accountPostsFrameLayout.getId(), mFragment) .commit(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.account_posts_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_refresh_account_posts_activity) { if (mFragment != null) { ((PostFragment) mFragment).refresh(); } return true; } else if (itemId == R.id.action_change_post_layout_account_posts_activity) { postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } else if (itemId == android.R.id.home) { finish(); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); getSupportFragmentManager().putFragment(outState, FRAGMENT_OUT_STATE, mFragment); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Override public void sortTypeSelected(SortType sortType) { if (mFragment != null) { ((PostFragment) mFragment).changeSortType(sortType); } } @Override public void sortTypeSelected(String sortType) { } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onChangeNSFWEvent(ChangeNSFWEvent changeNSFWEvent) { ((FragmentCommunicator) mFragment).changeNSFW(changeNSFWEvent.nsfw); } @Override public void postLayoutSelected(int postLayout) { if (mFragment != null) { mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE + accountName, postLayout).apply(); ((PostFragmentBase) mFragment).changePostLayout(postLayout); } } @Override public void onLongPress() { if (mFragment != null) { ((PostFragment) mFragment).goBackToTop(); } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/AccountSavedThingActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayoutMediator; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityAccountSavedThingBinding; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.CommentsListingFragment; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class AccountSavedThingActivity extends BaseActivity implements ActivityToolbarInterface, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, MarkPostAsReadInterface { @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private FragmentManager fragmentManager; private SectionsPagerAdapter sectionsPagerAdapter; private PostLayoutBottomSheetFragment postLayoutBottomSheetFragment; private ActivityAccountSavedThingBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityAccountSavedThingBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); mViewPager2 = binding.accountSavedThingViewPager2; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.accountSavedThingAppbarLayout); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.accountSavedThingToolbar, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.accountSavedThingViewPager2.setPadding(allInsets.left, 0, allInsets.right, 0); return insets; } }); //adjustToolbar(binding.accountSavedThingToolbar); } } setSupportActionBar(binding.accountSavedThingToolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.accountSavedThingToolbar); postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); fragmentManager = getSupportFragmentManager(); initializeViewPager(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (sectionsPagerAdapter != null) { return sectionsPagerAdapter.handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.accountSavedThingCoordinatorLayout.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme( binding.accountSavedThingAppbarLayout, binding.accountSavedThingCollapsingToolbarLayout, binding.accountSavedThingToolbar); applyAppBarScrollFlagsIfApplicable(binding.accountSavedThingCollapsingToolbarLayout); applyTabLayoutTheme(binding.accountSavedThingTabLayout); } private void initializeViewPager() { sectionsPagerAdapter = new SectionsPagerAdapter(this); binding.accountSavedThingViewPager2.setAdapter(sectionsPagerAdapter); binding.accountSavedThingViewPager2.setUserInputEnabled(!mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false)); new TabLayoutMediator(binding.accountSavedThingTabLayout, binding.accountSavedThingViewPager2, (tab, position) -> { switch (position) { case 0: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.posts)); break; case 1: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.comments)); break; } }).attach(); binding.accountSavedThingViewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } } }); fixViewPager2Sensitivity(binding.accountSavedThingViewPager2); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.account_saved_thing_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_refresh_account_saved_thing_activity) { sectionsPagerAdapter.refresh(); return true; } else if (itemId == R.id.action_change_post_layout_account_saved_thing_activity) { postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } return false; } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onChangeNSFWEvent(ChangeNSFWEvent changeNSFWEvent) { sectionsPagerAdapter.changeNSFW(changeNSFWEvent.nsfw); } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } @Override public void postLayoutSelected(int postLayout) { if (sectionsPagerAdapter != null) { mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE + accountName, postLayout).apply(); sectionsPagerAdapter.changePostLayout(postLayout); } } @Override public void markPostAsRead(Post post) { int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, accountName, post.getId(), readPostsLimit); } private class SectionsPagerAdapter extends FragmentStateAdapter { SectionsPagerAdapter(FragmentActivity fa) { super(fa); } @NonNull @Override public Fragment createFragment(int position) { if (position == 0) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_USER); bundle.putString(PostFragment.EXTRA_USER_NAME, accountName); bundle.putString(PostFragment.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_SAVED); bundle.putBoolean(PostFragment.EXTRA_DISABLE_READ_POSTS, true); fragment.setArguments(bundle); return fragment; } CommentsListingFragment fragment = new CommentsListingFragment(); Bundle bundle = new Bundle(); bundle.putString(CommentsListingFragment.EXTRA_USERNAME, accountName); bundle.putBoolean(CommentsListingFragment.EXTRA_ARE_SAVED_COMMENTS, true); fragment.setArguments(bundle); return fragment; } @Nullable private Fragment getCurrentFragment() { if (fragmentManager == null) { return null; } return fragmentManager.findFragmentByTag("f" + binding.accountSavedThingViewPager2.getCurrentItem()); } public boolean handleKeyDown(int keyCode) { if (binding.accountSavedThingViewPager2.getCurrentItem() == 0) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { return ((PostFragment) fragment).handleKeyDown(keyCode); } } return false; } public void refresh() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).refresh(); } else if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).refresh(); } } public void changeNSFW(boolean nsfw) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeNSFW(nsfw); } } public void changePostLayout(int postLayout) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changePostLayout(postLayout); } } public void goBackToTop() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).goBackToTop(); } else if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).goBackToTop(); } } @Override public int getItemCount() { return 2; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ActivityToolbarInterface.java ================================================ package ml.docilealligator.infinityforreddit.activities; public interface ActivityToolbarInterface { void onLongPress(); default void displaySortType() {} } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/AppBarStateChangeListener.java ================================================ package ml.docilealligator.infinityforreddit.activities; import com.google.android.material.appbar.AppBarLayout; public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener { private AppBarStateChangeListener.State mCurrentState = AppBarStateChangeListener.State.IDLE; private int lastOffset = -1; @Override public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { if (lastOffset == verticalOffset) { return; } lastOffset = verticalOffset; if (verticalOffset == 0) { if (mCurrentState != AppBarStateChangeListener.State.EXPANDED) { onStateChanged(appBarLayout, AppBarStateChangeListener.State.EXPANDED); } mCurrentState = AppBarStateChangeListener.State.EXPANDED; } else if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) { if (mCurrentState != AppBarStateChangeListener.State.COLLAPSED) { onStateChanged(appBarLayout, AppBarStateChangeListener.State.COLLAPSED); } mCurrentState = AppBarStateChangeListener.State.COLLAPSED; } else { if (mCurrentState != AppBarStateChangeListener.State.IDLE) { onStateChanged(appBarLayout, AppBarStateChangeListener.State.IDLE); } mCurrentState = AppBarStateChangeListener.State.IDLE; } } /** * Notifies on state change * * @param appBarLayout Layout * @param state Collapse state */ public abstract void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state); // State public enum State { EXPANDED, COLLAPSED, IDLE } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/BaseActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL; import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.view.menu.MenuItemImpl; import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; import androidx.core.view.MenuItemCompat; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; 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 org.greenrobot.eventbus.EventBus; import java.lang.reflect.Field; import java.util.Locale; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.slidr.Slidr; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; import ml.docilealligator.infinityforreddit.events.FinishViewMediaActivityEvent; import ml.docilealligator.infinityforreddit.font.ContentFontFamily; import ml.docilealligator.infinityforreddit.font.ContentFontStyle; import ml.docilealligator.infinityforreddit.font.FontFamily; import ml.docilealligator.infinityforreddit.font.FontStyle; import ml.docilealligator.infinityforreddit.font.TitleFontFamily; import ml.docilealligator.infinityforreddit.font.TitleFontStyle; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public abstract class BaseActivity extends AppCompatActivity implements CustomFontReceiver { public static final int IGNORE_MARGIN = -1; private boolean immersiveInterface; private boolean changeStatusBarIconColor; private boolean transparentStatusBarAfterToolbarCollapsed; private boolean hasDrawerLayout = false; private boolean isImmersiveInterfaceApplicable = true; private int systemVisibilityToolbarExpanded = 0; private int systemVisibilityToolbarCollapsed = 0; private boolean shouldTrackFullscreenMediaPeekTouchEvent; public CustomThemeWrapper customThemeWrapper; public Typeface typeface; public Typeface titleTypeface; public Typeface contentTypeface; @Nullable public SliderPanel mSliderPanel; @Nullable public ViewPager2 mViewPager2; @Nullable public String accessToken; @NonNull public String accountName = Account.ANONYMOUS_ACCOUNT; public Handler mHandler; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); customThemeWrapper = getCustomThemeWrapper(); SharedPreferences mSharedPreferences = getDefaultSharedPreferences(); String language = mSharedPreferences.getString(SharedPreferencesUtils.LANGUAGE, SharedPreferencesUtils.LANGUAGE_DEFAULT_VALUE); Locale systemLocale = Resources.getSystem().getConfiguration().locale; Locale locale; if (language.equals(SharedPreferencesUtils.LANGUAGE_DEFAULT_VALUE)) { language = systemLocale.getLanguage(); locale = new Locale(language, systemLocale.getCountry()); } else { if (language.contains("-")) { locale = new Locale(language.substring(0, 2), language.substring(4)); } else { locale = new Locale(language); } } Locale.setDefault(locale); Resources resources = getResources(); Configuration config = resources.getConfiguration(); config.setLocale(locale); resources.updateConfiguration(config, resources.getDisplayMetrics()); boolean systemDefault = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; int systemThemeType = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.THEME_KEY, "2")); immersiveInterface = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true); if (immersiveInterface && config.orientation == Configuration.ORIENTATION_LANDSCAPE) { immersiveInterface = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_IMMERSIVE_INTERFACE_IN_LANDSCAPE_MODE, false); } switch (systemThemeType) { case 0: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO); getTheme().applyStyle(R.style.Theme_Normal, true); customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.LIGHT); break; case 1: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES); if(mSharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.AMOLED); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.DARK); } break; case 2: if (systemDefault) { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM); } else { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY); } if((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO) { getTheme().applyStyle(R.style.Theme_Normal, true); customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.LIGHT); } else { if(mSharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.AMOLED); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.DARK); } } } boolean userDefinedChangeStatusBarIconColorInImmersiveInterface = customThemeWrapper.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface(); if (isImmersiveInterface()) { changeStatusBarIconColor = userDefinedChangeStatusBarIconColorInImmersiveInterface; } else { changeStatusBarIconColor = false; } getTheme().applyStyle(FontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.FONT_SIZE_KEY, FontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(TitleFontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_SIZE_KEY, TitleFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(ContentFontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_SIZE_KEY, ContentFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(FontFamily.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.FONT_FAMILY_KEY, FontFamily.Default.name())).getResId(), true); getTheme().applyStyle(TitleFontFamily.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY, TitleFontFamily.Default.name())).getResId(), true); getTheme().applyStyle(ContentFontFamily.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY, ContentFontFamily.Default.name())).getResId(), true); Window window = getWindow(); View decorView = window.getDecorView(); boolean isLightStatusbar = customThemeWrapper.isLightStatusBar(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { boolean isLightNavBar = customThemeWrapper.isLightNavBar(); if (isLightStatusbar) { if (isLightNavBar) { systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; if (changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } else { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } } else { systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; if (!changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } } } else { if (isLightNavBar) { systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; if (changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } } else { if (changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } } } decorView.setSystemUiVisibility(systemVisibilityToolbarExpanded); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { ViewCompat.setOnApplyWindowInsetsListener(window.getDecorView(), new OnApplyWindowInsetsListener() { @Override public @NonNull WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { if (!isImmersiveInterface()) { Insets inset = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); v.setBackgroundColor(customThemeWrapper.getColorPrimary()); v.setPadding(inset.left, inset.top, inset.right, 0); } return insets; } }); } if (!isImmersiveInterface()) { window.setNavigationBarColor(customThemeWrapper.getNavBarColor()); if (!hasDrawerLayout) { window.setStatusBarColor(customThemeWrapper.getColorPrimaryDark()); } } else { window.setNavigationBarColor(Color.TRANSPARENT); window.setStatusBarColor(Color.TRANSPARENT); } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isLightStatusbar) { decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; if (!changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } } else if (changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } } accessToken = getCurrentAccountSharedPreferences().getString(SharedPreferencesUtils.ACCESS_TOKEN, null); accountName = getCurrentAccountSharedPreferences().getString(SharedPreferencesUtils.ACCOUNT_NAME, Account.ANONYMOUS_ACCOUNT); mHandler = new Handler(Looper.getMainLooper()); } @Override protected void onResume() { super.onResume(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && mSliderPanel != null) { setTranslucent(true); } } @Override protected void onPause() { super.onPause(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && mSliderPanel != null && !isFinishing()) { mHandler.postDelayed(() -> setTranslucent(false), 500); } } @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (shouldTrackFullscreenMediaPeekTouchEvent) { if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) { shouldTrackFullscreenMediaPeekTouchEvent = false; EventBus.getDefault().post(new FinishViewMediaActivityEvent()); } return true; } return super.dispatchTouchEvent(ev); } public abstract SharedPreferences getDefaultSharedPreferences(); public abstract SharedPreferences getCurrentAccountSharedPreferences(); public abstract CustomThemeWrapper getCustomThemeWrapper(); protected abstract void applyCustomTheme(); protected boolean isChangeStatusBarIconColor() { return changeStatusBarIconColor; } protected int getSystemVisibilityToolbarExpanded() { return systemVisibilityToolbarExpanded; } protected int getSystemVisibilityToolbarCollapsed() { return systemVisibilityToolbarCollapsed; } public boolean isImmersiveInterfaceRespectForcedEdgeToEdge() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { return true; } return immersiveInterface && isImmersiveInterfaceApplicable; } private boolean isImmersiveInterface() { return immersiveInterface && isImmersiveInterfaceApplicable; } public boolean isForcedImmersiveInterface() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && !immersiveInterface; } public boolean isImmersiveInterfaceEnabled() { return immersiveInterface; } protected void setToolbarGoToTop(Toolbar toolbar) { toolbar.setOnLongClickListener(view -> { if (BaseActivity.this instanceof ActivityToolbarInterface) { ((ActivityToolbarInterface) BaseActivity.this).onLongPress(); } return true; }); } /*protected void adjustToolbar(Toolbar toolbar) { int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); if (statusBarResourceId > 0) { ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) toolbar.getLayoutParams(); params.topMargin = getResources().getDimensionPixelSize(statusBarResourceId); toolbar.setLayoutParams(params); } }*/ protected void addOnOffsetChangedListener(AppBarLayout appBarLayout) { View decorView = getWindow().getDecorView(); appBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state) { if (state == State.COLLAPSED) { decorView.setSystemUiVisibility(getSystemVisibilityToolbarCollapsed()); } else if (state == State.EXPANDED) { decorView.setSystemUiVisibility(getSystemVisibilityToolbarExpanded()); } } }); } public static void setMargins(T view, int left, int top, int right, int bottom) { ViewGroup.LayoutParams lp = view.getLayoutParams(); if (lp instanceof ViewGroup.MarginLayoutParams) { ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) lp; if (top >= 0) { marginParams.topMargin = top; } if (bottom >= 0) { marginParams.bottomMargin = bottom; } if (left >= 0) { marginParams.setMarginStart(left); } if (right >= 0) { marginParams.setMarginEnd(right); } view.setLayoutParams(marginParams); } } protected void setTransparentStatusBarAfterToolbarCollapsed() { this.transparentStatusBarAfterToolbarCollapsed = true; } protected void setHasDrawerLayout() { hasDrawerLayout = true; } public void setImmersiveModeNotApplicableBelowAndroid16() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { return; } isImmersiveInterfaceApplicable = false; } protected void applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(AppBarLayout appBarLayout, @Nullable CollapsingToolbarLayout collapsingToolbarLayout, Toolbar toolbar) { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(appBarLayout, collapsingToolbarLayout, toolbar, true); } protected void applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(AppBarLayout appBarLayout, @Nullable CollapsingToolbarLayout collapsingToolbarLayout, Toolbar toolbar, boolean setToolbarBackgroundColor) { appBarLayout.setBackgroundColor(customThemeWrapper.getColorPrimary()); if (collapsingToolbarLayout != null) { collapsingToolbarLayout.setContentScrimColor(customThemeWrapper.getColorPrimary()); } if (setToolbarBackgroundColor) { toolbar.setBackgroundColor(customThemeWrapper.getColorPrimary()); } else if (!isImmersiveInterface()) { int[] colors = {customThemeWrapper.getColorPrimary(), Color.TRANSPARENT}; GradientDrawable gradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, colors); toolbar.setBackground(gradientDrawable); } toolbar.setTitleTextColor(customThemeWrapper.getToolbarPrimaryTextAndIconColor()); toolbar.setSubtitleTextColor(customThemeWrapper.getToolbarSecondaryTextColor()); if (toolbar.getNavigationIcon() != null) { toolbar.getNavigationIcon().setColorFilter(customThemeWrapper.getToolbarPrimaryTextAndIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } if (toolbar.getOverflowIcon() != null) { toolbar.getOverflowIcon().setColorFilter(customThemeWrapper.getToolbarPrimaryTextAndIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } if (typeface != null) { toolbar.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> { for (int j = 0; j < toolbar.getChildCount(); j++) { if (toolbar.getChildAt(j) instanceof TextView) { ((TextView) toolbar.getChildAt(j)).setTypeface(typeface); } } }); } } protected void applyAppBarScrollFlagsIfApplicable(CollapsingToolbarLayout collapsingToolbarLayout) { applyAppBarScrollFlagsIfApplicable(collapsingToolbarLayout, null); } protected void applyAppBarScrollFlagsIfApplicable(@NonNull CollapsingToolbarLayout collapsingToolbarLayout, @Nullable TabLayout tabLayout) { if (getDefaultSharedPreferences().getBoolean(SharedPreferencesUtils.LOCK_TOOLBAR, false)) { AppBarLayout.LayoutParams p = (AppBarLayout.LayoutParams) collapsingToolbarLayout.getLayoutParams(); p.setScrollFlags(SCROLL_FLAG_SCROLL | SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); collapsingToolbarLayout.setLayoutParams(p); if (tabLayout != null) { AppBarLayout.LayoutParams p1 = (AppBarLayout.LayoutParams) tabLayout.getLayoutParams(); p1.setScrollFlags(SCROLL_FLAG_SCROLL | SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); tabLayout.setLayoutParams(p1); } } } @SuppressLint("RestrictedApi") protected boolean applyMenuItemTheme(Menu menu) { if (customThemeWrapper != null) { for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); if (((MenuItemImpl) item).requestsActionButton()) { MenuItemCompat.setIconTintList(item, ColorStateList .valueOf(customThemeWrapper.getToolbarPrimaryTextAndIconColor())); } Utils.setTitleWithCustomFontToMenuItem(typeface, item, null); } } return true; } protected void applyTabLayoutTheme(TabLayout tabLayout) { int toolbarAndTabBackgroundColor = customThemeWrapper.getColorPrimary(); tabLayout.setBackgroundColor(toolbarAndTabBackgroundColor); tabLayout.setSelectedTabIndicatorColor(customThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTabIndicator()); tabLayout.setTabTextColors(customThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTextColor(), customThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTextColor()); } protected void applyFABTheme(FloatingActionButton fab) { fab.setBackgroundTintList(ColorStateList.valueOf(customThemeWrapper.getColorAccent())); fab.setImageTintList(ColorStateList.valueOf(customThemeWrapper.getFABIconColor())); } protected void fixViewPager2Sensitivity(ViewPager2 viewPager2) { try { Field recyclerViewField = ViewPager2.class.getDeclaredField("mRecyclerView"); recyclerViewField.setAccessible(true); RecyclerView recyclerView = (RecyclerView) recyclerViewField.get(viewPager2); Field touchSlopField = RecyclerView.class.getDeclaredField("mTouchSlop"); touchSlopField.setAccessible(true); Object touchSlopBox = touchSlopField.get(recyclerView); if (touchSlopBox != null) { int touchSlop = (int) touchSlopBox; touchSlopField.set(recyclerView, touchSlop * Integer.parseInt(getDefaultSharedPreferences().getString(SharedPreferencesUtils.TAB_SWITCHING_SENSITIVITY, "4"))); } } catch (NoSuchFieldException | IllegalAccessException ignore) {} } protected void setOtherActivitiesFabContentDescription(FloatingActionButton fab, int fabOption) { switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS: fab.setContentDescription(getString(R.string.content_description_submit_post)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: fab.setContentDescription(getString(R.string.content_description_refresh)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: fab.setContentDescription(getString(R.string.content_description_change_sort_type)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: fab.setContentDescription(getString(R.string.content_description_change_post_layout)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: fab.setContentDescription(getString(R.string.content_description_search)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: fab.setContentDescription(getString(R.string.content_description_go_to_subreddit)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: fab.setContentDescription(getString(R.string.content_description_go_to_user)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: fab.setContentDescription(getString(R.string.content_description_hide_read_posts)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: fab.setContentDescription(getString(R.string.content_description_filter_posts)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: fab.setContentDescription(getString(R.string.content_description_go_to_top)); break; } } protected void attachSliderPanelIfApplicable() { if (getDefaultSharedPreferences().getBoolean(SharedPreferencesUtils.SWIPE_RIGHT_TO_GO_BACK, true)) { mSliderPanel = Slidr.attach(this, Float.parseFloat(getDefaultSharedPreferences().getString(SharedPreferencesUtils.SWIPE_RIGHT_TO_GO_BACK_SENSITIVITY, "0.1")) ); } } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; this.titleTypeface = titleTypeface; this.contentTypeface = contentTypeface; } public void lockSwipeRightToGoBack() { } public void unlockSwipeRightToGoBack() { } public void copyLink(String link) { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", link); clipboard.setPrimaryClip(clip); if (android.os.Build.VERSION.SDK_INT < 33) { Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(this, R.string.copy_link_failed, Toast.LENGTH_SHORT).show(); } } public void triggerBackPress() { getOnBackPressedDispatcher().onBackPressed(); } public void setShouldTrackFullscreenMediaPeekTouchEvent(boolean value) { shouldTrackFullscreenMediaPeekTouchEvent = value; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CommentActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.provider.MediaStore; import android.text.Spanned; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.giphy.sdk.core.models.Media; import com.giphy.sdk.ui.GPHContentType; import com.giphy.sdk.ui.Giphy; import com.giphy.sdk.ui.views.GiphyDialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import kotlin.Unit; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CopyTextBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.GiphyGifInfoBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UploadedImagesBottomSheetFragment; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.comment.SendComment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityCommentBinding; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.markdown.CustomMarkwonAdapter; import ml.docilealligator.infinityforreddit.markdown.EmoteCloseBracketInlineProcessor; import ml.docilealligator.infinityforreddit.markdown.EmotePlugin; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.network.AnyAccountAccessTokenAuthenticator; import ml.docilealligator.infinityforreddit.repositories.CommentActivityRepository; import ml.docilealligator.infinityforreddit.thing.GiphyGif; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.viewmodels.CommentActivityViewModel; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import retrofit2.Retrofit; public class CommentActivity extends BaseActivity implements UploadImageEnabledActivity, AccountChooserBottomSheetFragment.AccountChooserListener, GiphyDialogFragment.GifSelectionListener { public static final String EXTRA_COMMENT_PARENT_TITLE_KEY = "ECPTK"; public static final String EXTRA_COMMENT_PARENT_BODY_KEY = "ECPBK"; public static final String EXTRA_COMMENT_PARENT_BODY_MARKDOWN_KEY = "ECPBMK"; public static final String EXTRA_PARENT_FULLNAME_KEY = "EPFK"; public static final String EXTRA_PARENT_DEPTH_KEY = "EPDK"; public static final String EXTRA_PARENT_POSITION_KEY = "EPPK"; public static final String EXTRA_SUBREDDIT_NAME_KEY = "ESNK"; public static final String EXTRA_IS_REPLYING_KEY = "EIRK"; public static final String RETURN_EXTRA_COMMENT_DATA_KEY = "RECDK"; public static final int WRITE_COMMENT_REQUEST_CODE = 1; private static final int PICK_IMAGE_REQUEST_CODE = 100; private static final int CAPTURE_IMAGE_REQUEST_CODE = 200; private static final int MARKDOWN_PREVIEW_REQUEST_CODE = 300; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String UPLOADED_IMAGES_STATE = "UIS"; private static final String GIPHY_GIF_STATE = "GGS"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private RequestManager mGlide; private Account selectedAccount; private String parentFullname; private int parentDepth; private int parentPosition; private boolean isSubmitting = false; private boolean isReplying; private Uri capturedImageUri; private ArrayList uploadedImages = new ArrayList<>(); private GiphyGif giphyGif; private Menu mMenu; public CommentActivityViewModel commentActivityViewModel; /** * Post or comment body text color */ @ColorInt private int parentTextColor; @ColorInt private int parentSpoilerBackgroundColor; private ActivityCommentBinding binding; private EmotePlugin emotePlugin; private ImageAndGifEntry imageAndGifEntry; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCommentBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); Intent intent = getIntent(); isReplying = intent.getExtras().getBoolean(EXTRA_IS_REPLYING_KEY); applyCustomTheme(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.commentAppbarLayout); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.commentToolbar, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutCommentActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } } mGlide = Glide.with(this); if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { finish(); return; } String parentTitle = intent.getStringExtra(EXTRA_COMMENT_PARENT_TITLE_KEY); if (!TextUtils.isEmpty(parentTitle)) { binding.commentParentTitleTextView.setVisibility(View.VISIBLE); binding.commentParentTitleTextView.setText(parentTitle); binding.commentParentTitleTextView.setOnLongClickListener(view -> { Utils.hideKeyboard(CommentActivity.this); CopyTextBottomSheetFragment.show(getSupportFragmentManager(), parentTitle, null); return true; }); } String parentBodyMarkdown = intent.getStringExtra(EXTRA_COMMENT_PARENT_BODY_MARKDOWN_KEY); String parentBody = intent.getStringExtra(EXTRA_COMMENT_PARENT_BODY_KEY); if (parentBodyMarkdown != null && !parentBodyMarkdown.equals("")) { binding.commentContentMarkdownView.setVisibility(View.VISIBLE); binding.commentContentMarkdownView.setNestedScrollingEnabled(false); int linkColor = mCustomThemeWrapper.getLinkColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (contentTypeface != null) { textView.setTypeface(contentTypeface); } textView.setTextColor(parentTextColor); textView.setOnLongClickListener(view -> { if (textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { Utils.hideKeyboard(CommentActivity.this); CopyTextBottomSheetFragment.show(getSupportFragmentManager(), parentBody, parentBodyMarkdown); } return true; }); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(CommentActivity.this, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(linkColor); } }; EmoteCloseBracketInlineProcessor emoteCloseBracketInlineProcessor = new EmoteCloseBracketInlineProcessor(); emotePlugin = EmotePlugin.create(this, SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, mediaMetadata -> { Intent imageIntent = new Intent(this, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, intent.getStringExtra(EXTRA_SUBREDDIT_NAME_KEY)); imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); ImageAndGifPlugin imageAndGifPlugin = new ImageAndGifPlugin(); imageAndGifEntry = new ImageAndGifEntry(this, mGlide, SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, (mediaMetadata, commentId, postId) -> { Intent imageIntent = new Intent(this, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, intent.getStringExtra(EXTRA_SUBREDDIT_NAME_KEY)); imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); Markwon postBodyMarkwon = MarkdownUtils.createFullRedditMarkwon(this, miscPlugin, emoteCloseBracketInlineProcessor, emotePlugin, imageAndGifPlugin, parentTextColor, parentSpoilerBackgroundColor, null); CustomMarkwonAdapter markwonAdapter = MarkdownUtils.createCustomTablesAndImagesAdapter(this, imageAndGifEntry); markwonAdapter.setOnLongClickListener(view -> { Utils.hideKeyboard(CommentActivity.this); CopyTextBottomSheetFragment.show(getSupportFragmentManager(), parentBody, parentBodyMarkdown); return true; }); binding.commentContentMarkdownView.setLayoutManager(new LinearLayoutManagerBugFixed(this)); binding.commentContentMarkdownView.setAdapter(markwonAdapter); markwonAdapter.setMarkdown(postBodyMarkwon, parentBodyMarkdown); // noinspection NotifyDataSetChanged markwonAdapter.notifyDataSetChanged(); } parentFullname = intent.getStringExtra(EXTRA_PARENT_FULLNAME_KEY); parentDepth = intent.getExtras().getInt(EXTRA_PARENT_DEPTH_KEY); parentPosition = intent.getExtras().getInt(EXTRA_PARENT_POSITION_KEY); if (isReplying) { binding.commentToolbar.setTitle(getString(R.string.comment_activity_label_is_replying)); } setSupportActionBar(binding.commentToolbar); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); uploadedImages = savedInstanceState.getParcelableArrayList(UPLOADED_IMAGES_STATE); giphyGif = savedInstanceState.getParcelable(GIPHY_GIF_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.commentAccountIconGifImageView); binding.commentAccountNameTextView.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } } else { loadCurrentAccount(); } MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, true, true, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener(CommentActivity.this, binding.commentCommentEditText, item); } @Override public void onUploadImage() { Utils.hideKeyboard(CommentActivity.this); UploadedImagesBottomSheetFragment fragment = new UploadedImagesBottomSheetFragment(); Bundle arguments = new Bundle(); arguments.putParcelableArrayList(UploadedImagesBottomSheetFragment.EXTRA_UPLOADED_IMAGES, uploadedImages); fragment.setArguments(arguments); fragment.show(getSupportFragmentManager(), fragment.getTag()); } @Override public void onSelectGiphyGif() { GiphyGifInfoBottomSheetFragment fragment = new GiphyGifInfoBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); } }); binding.commentMarkdownBottomBarRecyclerView.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManagerBugFixed.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.commentMarkdownBottomBarRecyclerView.setAdapter(adapter); binding.commentAccountLinearLayout.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.commentCommentEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), binding.commentCommentEditText); Giphy.INSTANCE.configure(this, APIUtils.getGiphyApiKey(this)); commentActivityViewModel = new ViewModelProvider( this, CommentActivityViewModel.Companion.provideFactory(new CommentActivityRepository(mRedditDataRoomDatabase.commentDraftDao())) ).get(CommentActivityViewModel.class); if (savedInstanceState == null) { commentActivityViewModel.getCommentDraft(parentFullname).observe(this, commentDraft -> { if (commentDraft != null) { binding.commentCommentEditText.setText(commentDraft.getContent()); } }); } getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isSubmitting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_edit_comment_detail, false); } else { if (binding.commentCommentEditText.getText().toString().isEmpty()) { commentActivityViewModel.deleteCommentDraft(parentFullname, () -> { setEnabled(false); triggerBackPress(); return Unit.INSTANCE; }); } else { promptAlertDialog(R.string.save_comment_draft, R.string.save_comment_draft_detail, true); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon).transform(new RoundedCornersTransformation(72, 0))) .into(binding.commentAccountIconGifImageView); binding.commentAccountNameTextView.setText(account.getAccountName()); } }); }); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putParcelableArrayList(UPLOADED_IMAGES_STATE, uploadedImages); outState.putParcelable(GIPHY_GIF_STATE, giphyGif); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.commentCoordinatorLayout.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.commentAppbarLayout, null, binding.commentToolbar); binding.commentParentTitleTextView.setTextColor(customThemeWrapper.getPostTitleColor()); binding.commentDivider.setBackgroundColor(mCustomThemeWrapper.getDividerColor()); binding.commentCommentEditText.setTextColor(mCustomThemeWrapper.getCommentColor()); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.commentCommentEditText.setHintTextColor(secondaryTextColor); if (isReplying) { parentTextColor = mCustomThemeWrapper.getCommentColor(); } else { parentTextColor = mCustomThemeWrapper.getPostContentColor(); } parentSpoilerBackgroundColor = parentTextColor | 0xFF000000; binding.commentAccountNameTextView.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); if (typeface != null) { binding.commentCommentEditText.setTypeface(typeface); } if (titleTypeface != null) { binding.commentParentTitleTextView.setTypeface(titleTypeface); } } @Override protected void onPause() { super.onPause(); Utils.hideKeyboard(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.comment_activity, menu); mMenu = menu; applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_preview_comment_activity) { Intent intent = new Intent(this, FullMarkdownActivity.class); intent.putExtra(FullMarkdownActivity.EXTRA_MARKDOWN, binding.commentCommentEditText.getText().toString()); intent.putExtra(FullMarkdownActivity.EXTRA_SUBMIT_POST, true); startActivityForResult(intent, MARKDOWN_PREVIEW_REQUEST_CODE); } else if (itemId == R.id.action_send_comment_activity) { sendComment(item); return true; } return false; } public void sendComment(@Nullable MenuItem item) { if (!isSubmitting) { isSubmitting = true; if (binding.commentCommentEditText.getText() == null || binding.commentCommentEditText.getText().toString().equals("")) { isSubmitting = false; Snackbar.make(binding.commentCoordinatorLayout, R.string.comment_content_required, Snackbar.LENGTH_SHORT).show(); return; } if (item != null) { item.setEnabled(false); item.getIcon().setAlpha(130); } Snackbar sendingSnackbar = Snackbar.make(binding.commentCoordinatorLayout, R.string.sending_comment, Snackbar.LENGTH_INDEFINITE); sendingSnackbar.show(); Retrofit newAuthenticatorOauthRetrofit = mOauthRetrofit.newBuilder() .client(new OkHttpClient.Builder().authenticator(new AnyAccountAccessTokenAuthenticator(APIUtils.getClientId(getApplicationContext()), mRetrofit, mRedditDataRoomDatabase, selectedAccount, mCurrentAccountSharedPreferences)) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build()) .build(); SendComment.sendComment(this, mExecutor, new Handler(), binding.commentCommentEditText.getText().toString(), parentFullname, parentDepth, uploadedImages, giphyGif, newAuthenticatorOauthRetrofit, selectedAccount, new SendComment.SendCommentListener() { @Override public void sendCommentSuccess(Comment comment) { isSubmitting = false; if (item != null) { item.setEnabled(true); item.getIcon().setAlpha(255); } Toast.makeText(CommentActivity.this, R.string.send_comment_success, Toast.LENGTH_SHORT).show(); Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_COMMENT_DATA_KEY, comment); returnIntent.putExtra(EXTRA_PARENT_FULLNAME_KEY, parentFullname); if (isReplying) { returnIntent.putExtra(EXTRA_PARENT_POSITION_KEY, parentPosition); } setResult(RESULT_OK, returnIntent); commentActivityViewModel.deleteCommentDraft(parentFullname, () -> { finish(); return Unit.INSTANCE; }); } @Override public void sendCommentFailed(@Nullable String errorMessage) { isSubmitting = false; sendingSnackbar.dismiss(); if (item != null) { item.setEnabled(true); item.getIcon().setAlpha(255); } if (errorMessage == null || errorMessage.isEmpty()) { Snackbar.make(binding.commentCoordinatorLayout, R.string.send_comment_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.commentCoordinatorLayout, errorMessage, Snackbar.LENGTH_SHORT).show(); } } }); } } private void promptAlertDialog(int titleResId, int messageResId, boolean canSaveDraft) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.yes, (dialogInterface, i) -> { if (canSaveDraft) { commentActivityViewModel.saveCommentDraft(parentFullname, binding.commentCommentEditText.getText().toString(), () -> { finish(); return Unit.INSTANCE; }); } else { finish(); } }) .setNegativeButton(R.string.no, (dialog, which) -> { if (canSaveDraft) { finish(); } }) .setNeutralButton(R.string.cancel, null) .show(); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == PICK_IMAGE_REQUEST_CODE) { if (data == null) { Toast.makeText(CommentActivity.this, R.string.error_getting_image, Toast.LENGTH_LONG).show(); return; } Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, accessToken, binding.commentCommentEditText, binding.commentCoordinatorLayout, data.getData(), uploadedImages); } else if (requestCode == CAPTURE_IMAGE_REQUEST_CODE) { Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, accessToken, binding.commentCommentEditText, binding.commentCoordinatorLayout, capturedImageUri, uploadedImages); } else if (requestCode == MARKDOWN_PREVIEW_REQUEST_CODE) { sendComment(mMenu == null ? null : mMenu.findItem(R.id.action_send_comment_activity)); } } } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { String dataSavingMode = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { if (emotePlugin != null) { emotePlugin.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } if (imageAndGifEntry != null) { imageAndGifEntry.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } } } @Override public void uploadImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getResources().getString(R.string.select_from_gallery)), PICK_IMAGE_REQUEST_CODE); } @Override public void captureImage() { Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { capturedImageUri = FileProvider.getUriForFile(this, getPackageName() + ".provider", File.createTempFile("captured_image", ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES))); pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); startActivityForResult(pictureIntent, CAPTURE_IMAGE_REQUEST_CODE); } catch (IOException ex) { Toast.makeText(this, R.string.error_creating_temp_file, Toast.LENGTH_SHORT).show(); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_camera_available, Toast.LENGTH_SHORT).show(); } } @Override public void insertImageUrl(UploadedImage uploadedImage) { int start = Math.max(binding.commentCommentEditText.getSelectionStart(), 0); int end = Math.max(binding.commentCommentEditText.getSelectionEnd(), 0); int realStart = Math.min(start, end); if (realStart > 0 && binding.commentCommentEditText.getText().toString().charAt(realStart - 1) != '\n') { binding.commentCommentEditText.getText().replace(realStart, Math.max(start, end), "\n![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "\n![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } else { binding.commentCommentEditText.getText().replace(realStart, Math.max(start, end), "![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.commentAccountIconGifImageView); binding.commentAccountNameTextView.setText(selectedAccount.getAccountName()); } } @Override public void didSearchTerm(@NonNull String s) { } @Override public void onGifSelected(@NonNull Media media, @Nullable String s, @NonNull GPHContentType gphContentType) { this.giphyGif = new GiphyGif(media.getId(), true); int start = Math.max(binding.commentCommentEditText.getSelectionStart(), 0); int end = Math.max(binding.commentCommentEditText.getSelectionEnd(), 0); int realStart = Math.min(start, end); if (realStart > 0 && binding.commentCommentEditText.getText().toString().charAt(realStart - 1) != '\n') { binding.commentCommentEditText.getText().replace(realStart, Math.max(start, end), "\n![gif](" + giphyGif.id + ")\n", 0, "\n![gif]()\n".length() + giphyGif.id.length()); } else { binding.commentCommentEditText.getText().replace(realStart, Math.max(start, end), "![gif](" + giphyGif.id + ")\n", 0, "![gif]()\n".length() + giphyGif.id.length()); } } @Override public void onDismissed(@NonNull GPHContentType gphContentType) { } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CommentFilterPreferenceActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.CommentFilterWithUsageRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CommentFilterOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterWithUsageViewModel; import ml.docilealligator.infinityforreddit.commentfilter.DeleteCommentFilter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityCommentFilterPreferenceBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class CommentFilterPreferenceActivity extends BaseActivity { public static final String EXTRA_COMMENT = "EC"; private ActivityCommentFilterPreferenceBinding binding; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor executor; public CommentFilterWithUsageViewModel commentFilterWithUsageViewModel; private CommentFilterWithUsageRecyclerViewAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCommentFilterPreferenceBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCommentFilterPreferenceActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarCommentFilterPreferenceActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewCommentFilterPreferenceActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); setMargins(binding.fabCommentFilterPreferenceActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, CommentFilterPreferenceActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, CommentFilterPreferenceActivity.this) + allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarCommentFilterPreferenceActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); Comment comment = getIntent().getParcelableExtra(EXTRA_COMMENT); binding.fabCommentFilterPreferenceActivity.setOnClickListener(view -> { if (comment != null) { showCommentFilterOptions(comment, null); } else { Intent intent = new Intent(this, CustomizeCommentFilterActivity.class); intent.putExtra(CustomizeCommentFilterActivity.EXTRA_FROM_SETTINGS, true); startActivity(intent); } }); adapter = new CommentFilterWithUsageRecyclerViewAdapter(this, commentFilter -> { if (comment != null) { showCommentFilterOptions(comment, commentFilter); } else { CommentFilterOptionsBottomSheetFragment commentFilterOptionsBottomSheetFragment = new CommentFilterOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(CommentFilterOptionsBottomSheetFragment.EXTRA_COMMENT_FILTER, commentFilter); commentFilterOptionsBottomSheetFragment.setArguments(bundle); commentFilterOptionsBottomSheetFragment.show(getSupportFragmentManager(), commentFilterOptionsBottomSheetFragment.getTag()); } }); binding.recyclerViewCommentFilterPreferenceActivity.setAdapter(adapter); commentFilterWithUsageViewModel = new ViewModelProvider(this, new CommentFilterWithUsageViewModel.Factory(redditDataRoomDatabase)).get(CommentFilterWithUsageViewModel.class); commentFilterWithUsageViewModel.getCommentFilterWithUsageListLiveData().observe(this, commentFilterWithUsages -> adapter.setCommentFilterWithUsageList(commentFilterWithUsages)); } public void editCommentFilter(CommentFilter commentFilter) { Intent intent = new Intent(this, CustomizeCommentFilterActivity.class); intent.putExtra(CustomizeCommentFilterActivity.EXTRA_COMMENT_FILTER, commentFilter); intent.putExtra(CustomizeCommentFilterActivity.EXTRA_FROM_SETTINGS, true); startActivity(intent); } public void applyCommentFilterTo(CommentFilter commentFilter) { Intent intent = new Intent(this, CommentFilterUsageListingActivity.class); intent.putExtra(CommentFilterUsageListingActivity.EXTRA_COMMENT_FILTER, commentFilter); startActivity(intent); } public void deleteCommentFilter(CommentFilter commentFilter) { DeleteCommentFilter.deleteCommentFilter(redditDataRoomDatabase, executor, commentFilter); } public void showCommentFilterOptions(Comment comment, @Nullable CommentFilter commentFilter) { String[] options = getResources().getStringArray(R.array.add_to_comment_filter_options); boolean[] selectedOptions = new boolean[]{false}; new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.select) .setMultiChoiceItems(options, selectedOptions, (dialogInterface, i, b) -> selectedOptions[i] = b) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Intent intent = new Intent(CommentFilterPreferenceActivity.this, CustomizeCommentFilterActivity.class); if (commentFilter != null) { intent.putExtra(CustomizeCommentFilterActivity.EXTRA_COMMENT_FILTER, commentFilter); } intent.putExtra(CustomizeCommentFilterActivity.EXTRA_FROM_SETTINGS, true); for (int j = 0; j < selectedOptions.length; j++) { if (selectedOptions[j]) { if (j == 0) { intent.putExtra(CustomizeCommentFilterActivity.EXTRA_EXCLUDE_USER, comment.getAuthor()); } } } startActivity(intent); }) .show(); } @Override public SharedPreferences getDefaultSharedPreferences() { return sharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return customThemeWrapper; } @Override protected void applyCustomTheme() { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCommentFilterPreferenceActivity, binding.collapsingToolbarLayoutCommentFilterPreferenceActivity, binding.toolbarCommentFilterPreferenceActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCommentFilterPreferenceActivity); applyFABTheme(binding.fabCommentFilterPreferenceActivity); binding.getRoot().setBackgroundColor(customThemeWrapper.getBackgroundColor()); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CommentFilterUsageListingActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.os.Bundle; import android.os.Handler; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.CommentFilterUsageRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CommentFilterUsageOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.NewCommentFilterUsageBottomSheetFragment; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsageViewModel; import ml.docilealligator.infinityforreddit.commentfilter.DeleteCommentFilterUsage; import ml.docilealligator.infinityforreddit.commentfilter.SaveCommentFilterUsage; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityCommentFilterUsageListingBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class CommentFilterUsageListingActivity extends BaseActivity { public static final String EXTRA_COMMENT_FILTER = "ECF"; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor executor; private ActivityCommentFilterUsageListingBinding binding; public CommentFilterUsageViewModel commentFilterUsageViewModel; private CommentFilterUsageRecyclerViewAdapter adapter; private CommentFilter commentFilter; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCommentFilterUsageListingBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCommentFilterUsageListingActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarCommentFilterUsageListingActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewCommentFilterUsageListingActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); setMargins(binding.fabCommentFilterUsageListingActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, CommentFilterUsageListingActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, CommentFilterUsageListingActivity.this) + allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarCommentFilterUsageListingActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); commentFilter = getIntent().getParcelableExtra(EXTRA_COMMENT_FILTER); setTitle(commentFilter.name); binding.fabCommentFilterUsageListingActivity.setOnClickListener(view -> { NewCommentFilterUsageBottomSheetFragment newCommentFilterUsageBottomSheetFragment = new NewCommentFilterUsageBottomSheetFragment(); newCommentFilterUsageBottomSheetFragment.show(getSupportFragmentManager(), newCommentFilterUsageBottomSheetFragment.getTag()); }); adapter = new CommentFilterUsageRecyclerViewAdapter(this, customThemeWrapper, commentFilterUsage -> { CommentFilterUsageOptionsBottomSheetFragment commentFilterUsageOptionsBottomSheetFragment = new CommentFilterUsageOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(CommentFilterUsageOptionsBottomSheetFragment.EXTRA_COMMENT_FILTER_USAGE, commentFilterUsage); commentFilterUsageOptionsBottomSheetFragment.setArguments(bundle); commentFilterUsageOptionsBottomSheetFragment.show(getSupportFragmentManager(), commentFilterUsageOptionsBottomSheetFragment.getTag()); }); binding.recyclerViewCommentFilterUsageListingActivity.setAdapter(adapter); commentFilterUsageViewModel = new ViewModelProvider(this, new CommentFilterUsageViewModel.Factory(redditDataRoomDatabase, commentFilter.name)).get(CommentFilterUsageViewModel.class); commentFilterUsageViewModel.getCommentFilterUsageListLiveData().observe(this, commentFilterUsages -> adapter.setCommentFilterUsages(commentFilterUsages)); } public void newCommentFilterUsage(int type) { if (type == CommentFilterUsage.SUBREDDIT_TYPE) { editAndCommentFilterUsageNameOfUsage(type, null); } } private void editAndCommentFilterUsageNameOfUsage(int type, String nameOfUsage) { View dialogView = getLayoutInflater().inflate(R.layout.dialog_edit_post_or_comment_filter_name_of_usage, null); TextView messageTextView = dialogView.findViewById(R.id.message_text_view_edit_post_or_comment_filter_name_of_usage_dialog); messageTextView.setVisibility(View.GONE); TextInputLayout textInputLayout = dialogView.findViewById(R.id.text_input_layout_edit_post_or_comment_filter_name_of_usage_dialog); TextInputEditText textInputEditText = dialogView.findViewById(R.id.text_input_edit_text_edit_post_or_comment_filter_name_of_usage_dialog); int primaryTextColor = customThemeWrapper.getPrimaryTextColor(); textInputLayout.setBoxStrokeColor(primaryTextColor); textInputLayout.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); textInputEditText.setTextColor(primaryTextColor); if (nameOfUsage != null) { textInputEditText.setText(nameOfUsage); } textInputEditText.requestFocus(); int titleStringId = R.string.subreddit; if (type == CommentFilterUsage.SUBREDDIT_TYPE) { textInputEditText.setHint(R.string.settings_tab_subreddit_name); } Utils.showKeyboard(this, new Handler(), textInputEditText); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleStringId) .setView(dialogView) .setPositiveButton(R.string.ok, (editTextDialogInterface, i1) -> { Utils.hideKeyboard(this); CommentFilterUsage commentFilterUsage; if (!textInputEditText.getText().toString().equals("")) { commentFilterUsage = new CommentFilterUsage(commentFilter.name, type, textInputEditText.getText().toString()); SaveCommentFilterUsage.saveCommentFilterUsage(redditDataRoomDatabase, executor, commentFilterUsage); } }) .setNegativeButton(R.string.cancel, null) .setOnDismissListener(editTextDialogInterface -> { Utils.hideKeyboard(this); }) .show(); } public void editCommentFilterUsage(CommentFilterUsage commentFilterUsage) { editAndCommentFilterUsageNameOfUsage(commentFilterUsage.usage, commentFilterUsage.nameOfUsage); } public void deleteCommentFilterUsage(CommentFilterUsage commentFilterUsage) { DeleteCommentFilterUsage.deleteCommentFilterUsage(redditDataRoomDatabase, executor, commentFilterUsage); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override public SharedPreferences getDefaultSharedPreferences() { return sharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return customThemeWrapper; } @Override protected void applyCustomTheme() { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCommentFilterUsageListingActivity, binding.collapsingToolbarLayoutCommentFilterUsageListingActivity, binding.toolbarCommentFilterUsageListingActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCommentFilterUsageListingActivity); applyFABTheme(binding.fabCommentFilterUsageListingActivity); binding.getRoot().setBackgroundColor(customThemeWrapper.getBackgroundColor()); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CopyMultiRedditActivity.kt ================================================ package ml.docilealligator.infinityforreddit.activities import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.bumptech.glide.integration.compose.placeholder import com.bumptech.glide.request.RequestOptions import jp.wasabeef.glide.transformations.RoundedCornersTransformation import kotlinx.coroutines.launch import ml.docilealligator.infinityforreddit.ActionState import ml.docilealligator.infinityforreddit.ActionStateError import ml.docilealligator.infinityforreddit.DataLoadState import ml.docilealligator.infinityforreddit.Infinity import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper import ml.docilealligator.infinityforreddit.customviews.compose.AppTheme import ml.docilealligator.infinityforreddit.customviews.compose.CustomLoadingIndicator import ml.docilealligator.infinityforreddit.customviews.compose.CustomTextField import ml.docilealligator.infinityforreddit.customviews.compose.LocalAppTheme import ml.docilealligator.infinityforreddit.customviews.compose.PrimaryIcon import ml.docilealligator.infinityforreddit.customviews.compose.PrimaryText import ml.docilealligator.infinityforreddit.customviews.compose.ThemedTopAppBar import ml.docilealligator.infinityforreddit.customviews.compose.ToolbarIcon import ml.docilealligator.infinityforreddit.multireddit.ExpandedSubredditInMultiReddit import ml.docilealligator.infinityforreddit.multireddit.MultiReddit import ml.docilealligator.infinityforreddit.repositories.CopyMultiRedditActivityRepositoryImpl import ml.docilealligator.infinityforreddit.viewmodels.CopyMultiRedditActivityViewModel import ml.docilealligator.infinityforreddit.viewmodels.CopyMultiRedditActivityViewModel.Companion.provideFactory import retrofit2.Retrofit import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Named @OptIn(ExperimentalMaterial3Api::class) class CopyMultiRedditActivity : BaseActivity() { @Inject @Named("oauth") lateinit var mOauthRetrofit: Retrofit @Inject lateinit var mRedditDataRoomDatabase: RedditDataRoomDatabase @Inject @Named("default") lateinit var mSharedPreferences: SharedPreferences @Inject @Named("current_account") lateinit var mCurrentAccountSharedPreferences: SharedPreferences @Inject lateinit var mCustomThemeWrapper: CustomThemeWrapper @Inject lateinit var mExecutor: Executor lateinit var copyMultiRedditActivityViewModel: CopyMultiRedditActivityViewModel companion object { private const val EXTRA_MULTIPATH = "EM" fun start(context: Context, multipath: String) { val intent = Intent(context, CopyMultiRedditActivity::class.java).apply { putExtra(EXTRA_MULTIPATH, multipath) } context.startActivity(intent) } } override fun onCreate(savedInstanceState: Bundle?) { ((application) as Infinity).appComponent.inject(this) super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { enableEdgeToEdge() } } val multipath = intent.getStringExtra(EXTRA_MULTIPATH) ?: "" copyMultiRedditActivityViewModel = ViewModelProvider.create( this, provideFactory(multipath, CopyMultiRedditActivityRepositoryImpl(mOauthRetrofit, mRedditDataRoomDatabase, accessToken ?: "")) )[CopyMultiRedditActivityViewModel::class.java] copyMultiRedditActivityViewModel.fetchMultiRedditInfo() val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView) windowInsetsController.isAppearanceLightStatusBars = customThemeWrapper.isLightStatusBar setContent { AppTheme(customThemeWrapper.themeType) { val context = LocalContext.current val scrollBehavior = enterAlwaysScrollBehavior() val multiRedditState by copyMultiRedditActivityViewModel.multiRedditState.collectAsStateWithLifecycle() val copyMultiRedditState by copyMultiRedditActivityViewModel.copyMultiRedditState.collectAsStateWithLifecycle() val name by copyMultiRedditActivityViewModel.name.collectAsStateWithLifecycle() val description by copyMultiRedditActivityViewModel.description.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val copyingMultiRedditMessage = stringResource(R.string.copying_multi_reddit) LaunchedEffect(copyMultiRedditState) { when (copyMultiRedditState) { is ActionState.Error -> { val error = (copyMultiRedditState as ActionState.Error).error scope.launch { when (error) { is ActionStateError.Message -> snackbarHostState.showSnackbar(error.message) is ActionStateError.MessageRes -> snackbarHostState.showSnackbar(context.getString(error.resId)) } } } is ActionState.Idle -> { } is ActionState.Running -> { scope.launch { snackbarHostState.showSnackbar(copyingMultiRedditMessage) } } is ActionState.Success<*> -> { startActivity(Intent(this@CopyMultiRedditActivity, ViewMultiRedditDetailActivity::class.java).apply { val data = (copyMultiRedditState as ActionState.Success<*>).data if (data is MultiReddit) { putExtra(ViewMultiRedditDetailActivity.EXTRA_MULTIREDDIT_PATH, data.path) } }) finish() } } } Scaffold( topBar = { ThemedTopAppBar( titleStringResId = R.string.copy_multireddit_activity_label, isImmersiveInterfaceEnabled = isImmersiveInterfaceEnabled, scrollBehavior = scrollBehavior, windowInsetsController = windowInsetsController, actions = { IconButton(onClick = { if (multiRedditState is DataLoadState.Success) { copyMultiRedditActivityViewModel.copyMultiRedditInfo() } }) { ToolbarIcon( drawableId = R.drawable.ic_check_circle_toolbar_24dp, contentDescription = stringResource(R.string.action_copy_multi_reddit) ) } } ) { finish() } }, modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection) .imePadding(), snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, contentWindowInsets = if (isImmersiveInterfaceEnabled) WindowInsets.safeDrawing else WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) ) { innerPadding -> when(multiRedditState) { is DataLoadState.Loading -> { Box(modifier = Modifier .fillMaxSize() .background(Color(LocalAppTheme.current.backgroundColor)) .padding(innerPadding)) { CustomLoadingIndicator( modifier = Modifier.align(Alignment.Center) ) } } is DataLoadState.Error -> { val interactionSource = remember { MutableInteractionSource() } Column( modifier = Modifier .fillMaxSize() .background(Color(LocalAppTheme.current.backgroundColor)) .padding(innerPadding) .clickable( interactionSource = interactionSource, indication = null ) { copyMultiRedditActivityViewModel.fetchMultiRedditInfo() } .padding(16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { PrimaryIcon( drawableId = R.drawable.ic_error_outline_black_day_night_24dp, contentDescription = stringResource(R.string.cannot_fetch_multireddit_tap_to_retry) ) PrimaryText( R.string.cannot_fetch_multireddit_tap_to_retry, textAlign = TextAlign.Center ) } } is DataLoadState.Success -> { LazyColumn( modifier = Modifier.fillMaxSize().background(Color(LocalAppTheme.current.backgroundColor)), contentPadding = innerPadding ) { item { CustomTextField( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) .padding(top = 16.dp, bottom = 8.dp), value = name, placeholder = stringResource(R.string.multi_reddit_name_hint) ) { copyMultiRedditActivityViewModel.setName(it) } } item { CustomTextField( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) .padding(top = 8.dp, bottom = 8.dp), value = description, placeholder = stringResource(R.string.multi_reddit_description_hint) ) { copyMultiRedditActivityViewModel.setDescription(it) } } items((multiRedditState as DataLoadState.Success).data.subreddits) { subreddit -> SubredditRow(subreddit) } } } } } } } } @OptIn(ExperimentalGlideComposeApi::class) @Composable fun SubredditRow(expandedSubredditInMultiReddit: ExpandedSubredditInMultiReddit) { Row( modifier = Modifier .fillMaxWidth() .clickable { startActivity(Intent(this, ViewSubredditDetailActivity::class.java).apply { putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, expandedSubredditInMultiReddit.name) }) } .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { GlideImage( modifier = Modifier .padding(end = 32.dp) .size(36.dp), model = expandedSubredditInMultiReddit.iconUrl, failure = placeholder(R.drawable.subreddit_default_icon), contentDescription = expandedSubredditInMultiReddit.name ) { it.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(72, 0))) } PrimaryText(expandedSubredditInMultiReddit.name) } } override fun getDefaultSharedPreferences(): SharedPreferences? { return mSharedPreferences } override fun getCurrentAccountSharedPreferences(): SharedPreferences? { return mCurrentAccountSharedPreferences } override fun getCustomThemeWrapper(): CustomThemeWrapper? { return mCustomThemeWrapper } override fun applyCustomTheme() { } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CreateMultiRedditActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.inputmethod.EditorInfoCompat; import com.google.android.material.snackbar.Snackbar; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityCreateMultiRedditBinding; import ml.docilealligator.infinityforreddit.multireddit.CreateMultiReddit; import ml.docilealligator.infinityforreddit.multireddit.ExpandedSubredditInMultiReddit; import ml.docilealligator.infinityforreddit.multireddit.MultiRedditJSONModel; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class CreateMultiRedditActivity extends BaseActivity { private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 1; private static final String SELECTED_SUBREDDITS_STATE = "SSS"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private ActivityCreateMultiRedditBinding binding; private ArrayList mSubreddits; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCreateMultiRedditBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCreateMultiRedditActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarCreateMultiRedditActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.nestedScrollViewCreateMultiRedditActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarCreateMultiRedditActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.visibilityChipCreateMultiRedditActivity.setVisibility(View.GONE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binding.multiRedditNameEditTextCreateMultiRedditActivity.setImeOptions(binding.multiRedditNameEditTextCreateMultiRedditActivity.getImeOptions() | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING); binding.descriptionEditTextCreateMultiRedditActivity.setImeOptions(binding.descriptionEditTextCreateMultiRedditActivity.getImeOptions() | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING); } } if (savedInstanceState != null) { mSubreddits = savedInstanceState.getParcelableArrayList(SELECTED_SUBREDDITS_STATE); } else { mSubreddits = new ArrayList<>(); } bindView(); } private void bindView() { binding.selectSubredditChipCreateMultiRedditActivity.setOnClickListener(view -> { Intent intent = new Intent(CreateMultiRedditActivity.this, SelectedSubredditsAndUsersActivity.class); intent.putParcelableArrayListExtra(SelectedSubredditsAndUsersActivity.EXTRA_SELECTED_SUBREDDITS, mSubreddits); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.visibilityChipCreateMultiRedditActivity.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { binding.visibilityChipCreateMultiRedditActivity.setChipBackgroundColor(ColorStateList.valueOf(mCustomThemeWrapper.getFilledCardViewBackgroundColor())); } else { //Match the background color binding.visibilityChipCreateMultiRedditActivity.setChipBackgroundColor(ColorStateList.valueOf(mCustomThemeWrapper.getBackgroundColor())); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.create_multi_reddit_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_save_create_multi_reddit_activity) { if (binding.multiRedditNameEditTextCreateMultiRedditActivity.getText() == null || binding.multiRedditNameEditTextCreateMultiRedditActivity.getText().toString().equals("")) { Snackbar.make(binding.coordinatorLayoutCreateMultiRedditActivity, R.string.no_multi_reddit_name, Snackbar.LENGTH_SHORT).show(); return true; } if (!accountName.equals(Account.ANONYMOUS_ACCOUNT)) { String jsonModel = new MultiRedditJSONModel(binding.multiRedditNameEditTextCreateMultiRedditActivity.getText().toString(), binding.descriptionEditTextCreateMultiRedditActivity.getText().toString(), binding.visibilityChipCreateMultiRedditActivity.isChecked(), mSubreddits).createJSONModel(); CreateMultiReddit.createMultiReddit(mExecutor, mHandler, mOauthRetrofit, mRedditDataRoomDatabase, accessToken, "/user/" + accountName + "/m/" + binding.multiRedditNameEditTextCreateMultiRedditActivity.getText().toString(), jsonModel, new CreateMultiReddit.CreateMultiRedditListener() { @Override public void success() { finish(); } @Override public void failed(int errorCode) { if (errorCode == 409) { Snackbar.make(binding.coordinatorLayoutCreateMultiRedditActivity, R.string.duplicate_multi_reddit, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutCreateMultiRedditActivity, R.string.create_multi_reddit_failed, Snackbar.LENGTH_SHORT).show(); } } }); } else { CreateMultiReddit.anonymousCreateMultiReddit(mExecutor, new Handler(), mRedditDataRoomDatabase, "/user/-/m/" + binding.multiRedditNameEditTextCreateMultiRedditActivity.getText().toString(), binding.multiRedditNameEditTextCreateMultiRedditActivity.getText().toString(), binding.descriptionEditTextCreateMultiRedditActivity.getText().toString(), mSubreddits, new CreateMultiReddit.CreateMultiRedditListener() { @Override public void success() { finish(); } @Override public void failed(int errorType) { Snackbar.make(binding.coordinatorLayoutCreateMultiRedditActivity, R.string.duplicate_multi_reddit, Snackbar.LENGTH_SHORT).show(); } }); } } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE && resultCode == RESULT_OK) { if (data != null) { mSubreddits = data.getParcelableArrayListExtra(SelectedSubredditsAndUsersActivity.EXTRA_RETURN_SELECTED_SUBREDDITS); } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(SELECTED_SUBREDDITS_STATE, mSubreddits); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutCreateMultiRedditActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCreateMultiRedditActivity, binding.collapsingToolbarLayoutCreateMultiRedditActivity, binding.toolbarCreateMultiRedditActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCreateMultiRedditActivity); int primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.inputCardViewCreateMultiRedditActivity.setCardBackgroundColor(mCustomThemeWrapper.getFilledCardViewBackgroundColor()); binding.multiRedditNameExplanationTextInputLayoutCreateMultiRedditActivity.setTextColor(primaryTextColor); binding.multiRedditNameTextInputLayoutCreateMultiRedditActivity.setBoxStrokeColor(primaryTextColor); binding.multiRedditNameTextInputLayoutCreateMultiRedditActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.multiRedditNameEditTextCreateMultiRedditActivity.setTextColor(primaryTextColor); binding.descriptionTextInputLayoutCreateMultiRedditActivity.setBoxStrokeColor(primaryTextColor); binding.descriptionTextInputLayoutCreateMultiRedditActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.descriptionEditTextCreateMultiRedditActivity.setTextColor(primaryTextColor); binding.selectSubredditChipCreateMultiRedditActivity.setTextColor(primaryTextColor); binding.selectSubredditChipCreateMultiRedditActivity.setChipBackgroundColor(ColorStateList.valueOf(mCustomThemeWrapper.getFilledCardViewBackgroundColor())); binding.selectSubredditChipCreateMultiRedditActivity.setChipStrokeColor(ColorStateList.valueOf(mCustomThemeWrapper.getFilledCardViewBackgroundColor())); binding.visibilityChipCreateMultiRedditActivity.setTextColor(primaryTextColor); binding.visibilityChipCreateMultiRedditActivity.setChipBackgroundColor(ColorStateList.valueOf(mCustomThemeWrapper.getFilledCardViewBackgroundColor())); binding.visibilityChipCreateMultiRedditActivity.setChipStrokeColor(ColorStateList.valueOf(mCustomThemeWrapper.getFilledCardViewBackgroundColor())); if (typeface != null) { Utils.setFontToAllTextViews(binding.coordinatorLayoutCreateMultiRedditActivity, typeface); binding.selectSubredditChipCreateMultiRedditActivity.setTypeface(typeface); binding.visibilityChipCreateMultiRedditActivity.setTypeface(typeface); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CustomThemeListingActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayoutMediator; import com.google.gson.JsonParseException; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.ServerAPI; import ml.docilealligator.infinityforreddit.asynctasks.ChangeThemeName; import ml.docilealligator.infinityforreddit.asynctasks.DeleteTheme; import ml.docilealligator.infinityforreddit.asynctasks.GetCustomTheme; import ml.docilealligator.infinityforreddit.asynctasks.InsertCustomTheme; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CreateThemeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CustomThemeOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customtheme.OnlineCustomThemeMetadata; import ml.docilealligator.infinityforreddit.databinding.ActivityCustomThemeListingBinding; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.fragments.CustomThemeListingFragment; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class CustomThemeListingActivity extends BaseActivity implements CustomThemeOptionsBottomSheetFragment.CustomThemeOptionsBottomSheetFragmentListener, CreateThemeBottomSheetFragment.SelectBaseThemeBottomSheetFragmentListener, RecyclerViewContentScrollingInterface { @Inject @Named("online_custom_themes") Retrofit onlineCustomThemesRetrofit; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject @Named("light_theme") SharedPreferences lightThemeSharedPreferences; @Inject @Named("dark_theme") SharedPreferences darkThemeSharedPreferences; @Inject @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences; @Inject Executor executor; private FragmentManager fragmentManager; private SectionsPagerAdapter sectionsPagerAdapter; private ActivityCustomThemeListingBinding binding; private ActivityResultLauncher customizeThemeActivityResultLauncher; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCustomThemeListingBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCustomizeThemeListingActivity); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarCustomizeThemeListingActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.viewPager2CustomizeThemeListingActivity.setPadding( allInsets.left, 0, allInsets.right, 0 ); setMargins(binding.fabCustomThemeListingActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, CustomThemeListingActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, CustomThemeListingActivity.this) + allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarCustomizeThemeListingActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); binding.fabCustomThemeListingActivity.setOnClickListener(view -> { CreateThemeBottomSheetFragment createThemeBottomSheetFragment = new CreateThemeBottomSheetFragment(); createThemeBottomSheetFragment.show(getSupportFragmentManager(), createThemeBottomSheetFragment.getTag()); }); fragmentManager = getSupportFragmentManager(); initializeViewPager(); } private void initializeViewPager() { sectionsPagerAdapter = new SectionsPagerAdapter(this); binding.viewPager2CustomizeThemeListingActivity.setAdapter(sectionsPagerAdapter); binding.viewPager2CustomizeThemeListingActivity.setUserInputEnabled(!sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false)); new TabLayoutMediator(binding.tabLayoutCustomizeThemeListingActivity, binding.viewPager2CustomizeThemeListingActivity, (tab, position) -> { switch (position) { case 0: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.local)); break; case 1: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.online)); break; } }).attach(); binding.viewPager2CustomizeThemeListingActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { binding.fabCustomThemeListingActivity.show(); if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } } }); fixViewPager2Sensitivity(binding.viewPager2CustomizeThemeListingActivity); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override public SharedPreferences getDefaultSharedPreferences() { return sharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return customThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutCustomThemeListingActivity.setBackgroundColor(customThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCustomizeThemeListingActivity, binding.collapsingToolbarLayoutCustomizeThemeListingActivity, binding.toolbarCustomizeThemeListingActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCustomizeThemeListingActivity); applyFABTheme(binding.fabCustomThemeListingActivity); applyTabLayoutTheme(binding.tabLayoutCustomizeThemeListingActivity); } @Override public void editTheme(String themeName, @Nullable OnlineCustomThemeMetadata onlineCustomThemeMetadata, int indexInThemeList) { Intent intent = new Intent(this, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_NAME, themeName); intent.putExtra(CustomizeThemeActivity.EXTRA_ONLINE_CUSTOM_THEME_METADATA, onlineCustomThemeMetadata); intent.putExtra(CustomizeThemeActivity.EXTRA_INDEX_IN_THEME_LIST, indexInThemeList); if (indexInThemeList >= 0) { //Online theme Fragment fragment = sectionsPagerAdapter.getOnlineThemeFragment(); if (fragment != null && ((CustomThemeListingFragment) fragment).getCustomizeThemeActivityResultLauncher() != null) { ((CustomThemeListingFragment) fragment).getCustomizeThemeActivityResultLauncher().launch(intent); return; } } startActivity(intent); } @Override public void changeName(String oldThemeName) { View dialogView = getLayoutInflater().inflate(R.layout.dialog_edit_name, null); EditText themeNameEditText = dialogView.findViewById(R.id.name_edit_text_edit_name_dialog); themeNameEditText.setText(oldThemeName); themeNameEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), themeNameEditText); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.edit_theme_name) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); ChangeThemeName.changeThemeName(executor, redditDataRoomDatabase, oldThemeName, themeNameEditText.getText().toString()); }) .setNegativeButton(R.string.cancel, null) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } @Override public void shareTheme(String themeName) { GetCustomTheme.getCustomTheme(executor, new Handler(), redditDataRoomDatabase, themeName, customTheme -> { if (customTheme != null) { String jsonModel = customTheme.getJSONModel(); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", jsonModel); clipboard.setPrimaryClip(clip); Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.theme_copied, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.copy_theme_faied, Snackbar.LENGTH_SHORT).show(); } } else { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.cannot_find_theme, Snackbar.LENGTH_SHORT).show(); } }); } @Override public void delete(String themeName) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.delete_theme) .setMessage(getString(R.string.delete_theme_dialog_message, themeName)) .setPositiveButton(R.string.yes, (dialogInterface, i) -> DeleteTheme.deleteTheme(executor, new Handler(), redditDataRoomDatabase, themeName, (isLightTheme, isDarkTheme, isAmoledTheme) -> { if (isLightTheme) { CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences( CustomThemeWrapper.getIndigo(CustomThemeListingActivity.this), lightThemeSharedPreferences); } if (isDarkTheme) { CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences( CustomThemeWrapper.getIndigoDark(CustomThemeListingActivity.this), darkThemeSharedPreferences); } if (isAmoledTheme) { CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences( CustomThemeWrapper.getIndigoAmoled(CustomThemeListingActivity.this), amoledThemeSharedPreferences); } EventBus.getDefault().post(new RecreateActivityEvent()); })) .setNegativeButton(R.string.no, null) .show(); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } public void shareTheme(CustomTheme customTheme) { if (customTheme != null) { String jsonModel = customTheme.getJSONModel(); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", jsonModel); clipboard.setPrimaryClip(clip); Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.theme_copied, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.copy_theme_faied, Snackbar.LENGTH_SHORT).show(); } } else { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.cannot_find_theme, Snackbar.LENGTH_SHORT).show(); } } @Override public void shareTheme(OnlineCustomThemeMetadata onlineCustomThemeMetadata) { onlineCustomThemesRetrofit.create(ServerAPI.class) .getCustomTheme(onlineCustomThemeMetadata.name, onlineCustomThemeMetadata.username) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", response.body()); clipboard.setPrimaryClip(clip); Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.theme_copied, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.copy_theme_faied, Snackbar.LENGTH_SHORT).show(); } } else { Toast.makeText(CustomThemeListingActivity.this, response.message(), Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { Toast.makeText(CustomThemeListingActivity.this, R.string.cannot_download_theme_data, Toast.LENGTH_SHORT).show(); } }); } @Subscribe public void onRecreateActivityEvent(RecreateActivityEvent recreateActivityEvent) { ActivityCompat.recreate(this); } @Override public void importTheme() { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { // If it does contain data, decide if you can handle the data. if (!clipboard.hasPrimaryClip()) { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.no_data_in_clipboard, Snackbar.LENGTH_SHORT).show(); } else if (clipboard.getPrimaryClipDescription() != null && !clipboard.getPrimaryClipDescription().hasMimeType("text/*")) { // since the clipboard has data but it is not text Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.no_data_in_clipboard, Snackbar.LENGTH_SHORT).show(); } else if (clipboard.getPrimaryClip() != null) { ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); String json = item.coerceToText(this.getApplicationContext()).toString(); if (!TextUtils.isEmpty(json)) { try { CustomTheme customTheme = CustomTheme.fromJson(json); checkDuplicateAndImportTheme(customTheme, true); } catch (JsonParseException e) { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.parse_theme_failed, Snackbar.LENGTH_SHORT).show(); } } else { Snackbar.make(binding.coordinatorLayoutCustomThemeListingActivity, R.string.parse_theme_failed, Snackbar.LENGTH_SHORT).show(); } } } } @Override public void contentScrollUp() { binding.fabCustomThemeListingActivity.show(); } @Override public void contentScrollDown() { binding.fabCustomThemeListingActivity.hide(); } private void checkDuplicateAndImportTheme(CustomTheme customTheme, boolean checkDuplicate) { InsertCustomTheme.insertCustomTheme(executor, new Handler(), redditDataRoomDatabase, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, customTheme, checkDuplicate, new InsertCustomTheme.InsertCustomThemeListener() { @Override public void success() { Toast.makeText(CustomThemeListingActivity.this, R.string.import_theme_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new RecreateActivityEvent()); } @Override public void duplicate() { new MaterialAlertDialogBuilder(CustomThemeListingActivity.this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.duplicate_theme_name_dialog_title) .setMessage(getString(R.string.duplicate_theme_name_dialog_message, customTheme.name)) .setPositiveButton(R.string.rename, (dialogInterface, i) -> { View dialogView = getLayoutInflater().inflate(R.layout.dialog_edit_name, null); EditText themeNameEditText = dialogView.findViewById(R.id.name_edit_text_edit_name_dialog); themeNameEditText.setText(customTheme.name); themeNameEditText.requestFocus(); Utils.showKeyboard(CustomThemeListingActivity.this, new Handler(), themeNameEditText); new MaterialAlertDialogBuilder(CustomThemeListingActivity.this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.edit_theme_name) .setView(dialogView) .setPositiveButton(R.string.ok, (editTextDialogInterface, i1) -> { Utils.hideKeyboard(CustomThemeListingActivity.this); if (!themeNameEditText.getText().toString().equals("")) { customTheme.name = themeNameEditText.getText().toString(); } checkDuplicateAndImportTheme(customTheme, true); }) .setNegativeButton(R.string.cancel, null) .setOnDismissListener(editTextDialogInterface -> { Utils.hideKeyboard(CustomThemeListingActivity.this); }) .show(); }) .setNegativeButton(R.string.override, (dialogInterface, i) -> { checkDuplicateAndImportTheme(customTheme, false); }) .show(); } }); } private class SectionsPagerAdapter extends FragmentStateAdapter { SectionsPagerAdapter(FragmentActivity fa) { super(fa); } @NonNull @Override public Fragment createFragment(int position) { if (position == 0) { return new CustomThemeListingFragment(); } CustomThemeListingFragment fragment = new CustomThemeListingFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(CustomThemeListingFragment.EXTRA_IS_ONLINE, true); fragment.setArguments(bundle); return fragment; } @Nullable private Fragment getOnlineThemeFragment() { if (fragmentManager == null) { return null; } return fragmentManager.findFragmentByTag("f1"); } @Override public int getItemCount() { return 2; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CustomThemePreviewActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.Typeface; import android.os.Build; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; import java.util.ArrayList; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeSettingsItem; import ml.docilealligator.infinityforreddit.customviews.slidr.Slidr; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; import ml.docilealligator.infinityforreddit.databinding.ActivityThemePreviewBinding; import ml.docilealligator.infinityforreddit.font.ContentFontStyle; import ml.docilealligator.infinityforreddit.font.FontStyle; import ml.docilealligator.infinityforreddit.font.TitleFontStyle; import ml.docilealligator.infinityforreddit.fragments.ThemePreviewCommentsFragment; import ml.docilealligator.infinityforreddit.fragments.ThemePreviewPostsFragment; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class CustomThemePreviewActivity extends AppCompatActivity implements CustomFontReceiver { public static final String EXTRA_CUSTOM_THEME_SETTINGS_ITEMS = "ECTSI"; public Typeface typeface; public Typeface titleTypeface; public Typeface contentTypeface; @Inject @Named("default") SharedPreferences mSharedPreferences; private ArrayList customThemeSettingsItems; private CustomTheme customTheme; private int expandedTabTextColor; private int expandedTabBackgroundColor; private int expandedTabIndicatorColor; private int collapsedTabTextColor; private int collapsedTabBackgroundColor; private int collapsedTabIndicatorColor; private int unsubscribedColor; private int subscribedColor; private int systemVisibilityToolbarExpanded = 0; private int systemVisibilityToolbarCollapsed = 0; private int topSystemBarHeight; private SliderPanel mSliderPanel; private ActivityThemePreviewBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((Infinity) getApplication()).getAppComponent().inject(this); customThemeSettingsItems = getIntent().getParcelableArrayListExtra(EXTRA_CUSTOM_THEME_SETTINGS_ITEMS); customTheme = CustomTheme.convertSettingsItemsToCustomTheme(customThemeSettingsItems, "ThemePreview"); boolean systemDefault = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; int systemThemeType = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.THEME_KEY, "2")); switch (systemThemeType) { case 0: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO); getTheme().applyStyle(R.style.Theme_Normal, true); break; case 1: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); } break; case 2: if (systemDefault) { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM); } else { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY); } if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO) { getTheme().applyStyle(R.style.Theme_Normal, true); } else { if (mSharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); } } } boolean immersiveInterface = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) || Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM; boolean changeStatusBarIconColor = false; if (immersiveInterface) { changeStatusBarIconColor = mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true) && customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface; } boolean isLightStatusbar = customTheme.isLightStatusBar; Window window = getWindow(); View decorView = window.getDecorView(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { boolean isLightNavBar = customTheme.isLightNavBar; if (isLightStatusbar) { if (isLightNavBar) { systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; if (changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } else { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } } else { systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; if (!changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } } } else { if (isLightNavBar) { systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; if (changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } } else { if (changeStatusBarIconColor) { systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } } } decorView.setSystemUiVisibility(systemVisibilityToolbarExpanded); window.setNavigationBarColor(customTheme.navBarColor); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isLightStatusbar) { decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); systemVisibilityToolbarExpanded = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; systemVisibilityToolbarCollapsed = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } } getTheme().applyStyle(FontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.FONT_SIZE_KEY, FontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(TitleFontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_SIZE_KEY, TitleFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(ContentFontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_SIZE_KEY, ContentFontStyle.Normal.name())).getResId(), true); binding = ActivityThemePreviewBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_RIGHT_TO_GO_BACK, true)) { mSliderPanel = Slidr.attach(this, Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_RIGHT_TO_GO_BACK_SENSITIVITY, "0.1"))); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (immersiveInterface) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } if (!mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { ViewCompat.setOnApplyWindowInsetsListener(window.getDecorView(), new OnApplyWindowInsetsListener() { @Override public @NonNull WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets inset = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); v.setBackgroundColor(customTheme.colorPrimary); v.setPadding(inset.left, inset.top, inset.right, 0); return insets; } }); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, !mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)); topSystemBarHeight = allInsets.top; int padding16 = (int) Utils.convertDpToPixel(16, CustomThemePreviewActivity.this); setMargins(binding.fabThemePreviewActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, allInsets.bottom); binding.toolbarLinearLayoutThemePreviewActivity.setPadding( padding16 + allInsets.left, binding.toolbar.getPaddingTop(), padding16 + allInsets.right, binding.toolbar.getPaddingBottom()); binding.viewPagerThemePreviewActivity.setPadding(allInsets.left, 0, allInsets.right, 0); binding.linearLayoutBottomAppBarThemePreviewActivity.setPadding( binding.linearLayoutBottomAppBarThemePreviewActivity.getPaddingLeft(), binding.linearLayoutBottomAppBarThemePreviewActivity.getPaddingTop(), binding.linearLayoutBottomAppBarThemePreviewActivity.getPaddingRight(), allInsets.bottom ); setMargins(binding.toolbar, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.tabLayoutThemePreviewActivity.setPadding(allInsets.left, 0, allInsets.right, 0); return WindowInsetsCompat.CONSUMED; } }); /*adjustToolbar(binding.toolbar); Resources resources = getResources(); int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { int navBarHeight = resources.getDimensionPixelSize(navBarResourceId); if (navBarHeight > 0) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.fabThemePreviewActivity.getLayoutParams(); params.bottomMargin = navBarHeight; binding.fabThemePreviewActivity.setLayoutParams(params); binding.linearLayoutBottomAppBarThemePreviewActivity.setPadding(0, (int) (6 * getResources().getDisplayMetrics().density), 0, navBarHeight); } }*/ } if (changeStatusBarIconColor) { binding.appbarLayoutThemePreviewActivity.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state) { if (state == State.COLLAPSED) { decorView.setSystemUiVisibility(systemVisibilityToolbarCollapsed); binding.tabLayoutThemePreviewActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutThemePreviewActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutThemePreviewActivity.setBackgroundColor(collapsedTabBackgroundColor); } else if (state == State.EXPANDED) { decorView.setSystemUiVisibility(systemVisibilityToolbarExpanded); binding.tabLayoutThemePreviewActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutThemePreviewActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutThemePreviewActivity.setBackgroundColor(expandedTabBackgroundColor); } } }); } else { binding.appbarLayoutThemePreviewActivity.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state) { if (state == State.COLLAPSED) { binding.tabLayoutThemePreviewActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutThemePreviewActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutThemePreviewActivity.setBackgroundColor(collapsedTabBackgroundColor); } else if (state == State.EXPANDED) { binding.tabLayoutThemePreviewActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutThemePreviewActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutThemePreviewActivity.setBackgroundColor(expandedTabBackgroundColor); } } }); } } else { binding.appbarLayoutThemePreviewActivity.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, State state) { if (state == State.EXPANDED) { binding.tabLayoutThemePreviewActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutThemePreviewActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutThemePreviewActivity.setBackgroundColor(expandedTabBackgroundColor); } else if (state == State.COLLAPSED) { binding.tabLayoutThemePreviewActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutThemePreviewActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutThemePreviewActivity.setBackgroundColor(collapsedTabBackgroundColor); } } }); } setSupportActionBar(binding.toolbar); binding.subscribeSubredditChipThemePreviewActivity.setOnClickListener(view -> { if (binding.subscribeSubredditChipThemePreviewActivity.getText().equals(getResources().getString(R.string.subscribe))) { binding.subscribeSubredditChipThemePreviewActivity.setText(R.string.unsubscribe); binding.subscribeSubredditChipThemePreviewActivity.setChipBackgroundColor(ColorStateList.valueOf(subscribedColor)); } else { binding.subscribeSubredditChipThemePreviewActivity.setText(R.string.subscribe); binding.subscribeSubredditChipThemePreviewActivity.setChipBackgroundColor(ColorStateList.valueOf(unsubscribedColor)); } }); SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); binding.viewPagerThemePreviewActivity.setAdapter(sectionsPagerAdapter); binding.viewPagerThemePreviewActivity.setOffscreenPageLimit(2); binding.viewPagerThemePreviewActivity.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } } }); binding.tabLayoutThemePreviewActivity.setupWithViewPager(binding.viewPagerThemePreviewActivity); } public static void setMargins(T view, int left, int top, int right, int bottom) { ViewGroup.LayoutParams lp = view.getLayoutParams(); if (lp instanceof ViewGroup.MarginLayoutParams) { ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) lp; if (top >= 0) { marginParams.topMargin = top; } if (bottom >= 0) { marginParams.bottomMargin = bottom; } if (left >= 0) { marginParams.setMarginStart(left); } if (right >= 0) { marginParams.setMarginEnd(right); } view.setLayoutParams(marginParams); } } private void applyCustomTheme() { binding.coordinatorLayoutThemePreviewActivity.setBackgroundColor(customTheme.backgroundColor); binding.appbarLayoutThemePreviewActivity.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { binding.appbarLayoutThemePreviewActivity.getViewTreeObserver().removeOnGlobalLayoutListener(this); binding.collapsingToolbarLayoutThemePreviewActivity.setScrimVisibleHeightTrigger(binding.toolbar.getHeight() + binding.tabLayoutThemePreviewActivity.getHeight() + topSystemBarHeight * 2); } }); binding.collapsingToolbarLayoutThemePreviewActivity.setContentScrimColor(customTheme.colorPrimary); binding.subscribeSubredditChipThemePreviewActivity.setTextColor(customTheme.chipTextColor); binding.subscribeSubredditChipThemePreviewActivity.setChipBackgroundColor(ColorStateList.valueOf(customTheme.unsubscribed)); applyAppBarLayoutAndToolbarTheme(binding.appbarLayoutThemePreviewActivity, binding.toolbar); expandedTabTextColor = customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor; expandedTabIndicatorColor = customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator; expandedTabBackgroundColor = customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground; collapsedTabTextColor = customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor; collapsedTabIndicatorColor = customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator; collapsedTabBackgroundColor = customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground; binding.extraPaddingViewThemePreviewActivity.setBackgroundColor(customTheme.colorPrimary); binding.subredditNameTextViewThemePreviewActivity.setTextColor(customTheme.subreddit); binding.userNameTextViewThemePreviewActivity.setTextColor(customTheme.username); binding.subscribeSubredditChipThemePreviewActivity.setTextColor(customTheme.chipTextColor); binding.primaryTextTextViewThemePreviewActivity.setTextColor(customTheme.primaryTextColor); binding.secondaryTextTextViewThemePreviewActivity.setTextColor(customTheme.secondaryTextColor); binding.bottomNavigationThemePreviewActivity.setBackgroundTint(ColorStateList.valueOf(customTheme.bottomAppBarBackgroundColor)); int bottomAppBarIconColor = customTheme.bottomAppBarIconColor; binding.subscriptionsBottomAppBarThemePreviewActivity.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.multiRedditBottomAppBarThemePreviewActivity.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.messageBottomAppBarThemePreviewActivity.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.profileBottomAppBarThemePreviewActivity.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); applyTabLayoutTheme(binding.tabLayoutThemePreviewActivity); applyFABTheme(binding.fabThemePreviewActivity); unsubscribedColor = customTheme.unsubscribed; subscribedColor = customTheme.subscribed; if (typeface != null) { binding.subredditNameTextViewThemePreviewActivity.setTypeface(typeface); binding.userNameTextViewThemePreviewActivity.setTypeface(typeface); binding.primaryTextTextViewThemePreviewActivity.setTypeface(typeface); binding.secondaryTextTextViewThemePreviewActivity.setTypeface(typeface); binding.subscribeSubredditChipThemePreviewActivity.setTypeface(typeface); } } /*private int getStatusBarHeight() { int result = 0; int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = getResources().getDimensionPixelSize(resourceId); } return result; }*/ protected void applyAppBarLayoutAndToolbarTheme(AppBarLayout appBarLayout, Toolbar toolbar) { appBarLayout.setBackgroundColor(customTheme.colorPrimary); toolbar.setTitleTextColor(customTheme.toolbarPrimaryTextAndIconColor); toolbar.setSubtitleTextColor(customTheme.toolbarSecondaryTextColor); if (toolbar.getNavigationIcon() != null) { toolbar.getNavigationIcon().setColorFilter(customTheme.toolbarPrimaryTextAndIconColor, android.graphics.PorterDuff.Mode.SRC_IN); } if (toolbar.getOverflowIcon() != null) { toolbar.getOverflowIcon().setColorFilter(customTheme.toolbarPrimaryTextAndIconColor, android.graphics.PorterDuff.Mode.SRC_IN); } if (typeface != null) { toolbar.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> { for (int j = 0; j < toolbar.getChildCount(); j++) { if (toolbar.getChildAt(j) instanceof TextView) { ((TextView) toolbar.getChildAt(j)).setTypeface(typeface); } } }); } } /*private void adjustToolbar(Toolbar toolbar) { int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); if (statusBarResourceId > 0) { ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) toolbar.getLayoutParams(); int statusBarHeight = getResources().getDimensionPixelSize(statusBarResourceId); params.topMargin = statusBarHeight; toolbar.setLayoutParams(params); TypedValue tv = new TypedValue(); if (getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { ((ViewGroup.MarginLayoutParams) binding.linearLayoutBottomAppBarThemePreviewActivity.getLayoutParams()).setMargins(0, TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics()) + statusBarHeight, 0, 0); } } }*/ protected void applyTabLayoutTheme(TabLayout tabLayout) { int toolbarAndTabBackgroundColor = customTheme.colorPrimary; binding.tabLayoutThemePreviewActivity.setBackgroundColor(toolbarAndTabBackgroundColor); binding.tabLayoutThemePreviewActivity.setSelectedTabIndicatorColor(customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator); binding.tabLayoutThemePreviewActivity.setTabTextColors(customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor, customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor); } protected void applyFABTheme(FloatingActionButton fab) { fab.setBackgroundTintList(ColorStateList.valueOf(customTheme.colorAccent)); fab.setImageTintList(ColorStateList.valueOf(customTheme.fabIconColor)); } public CustomTheme getCustomTheme() { return customTheme; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } private void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } private void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; this.titleTypeface = titleTypeface; this.contentTypeface = contentTypeface; } private class SectionsPagerAdapter extends FragmentPagerAdapter { private ThemePreviewPostsFragment themePreviewPostsFragment; private ThemePreviewCommentsFragment themePreviewCommentsFragment; SectionsPagerAdapter(FragmentManager fm) { super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } @NonNull @Override public Fragment getItem(int position) { if (position == 0) { return new ThemePreviewPostsFragment(); } return new ThemePreviewCommentsFragment(); } @Override public int getCount() { return 2; } @Override public CharSequence getPageTitle(int position) { switch (position) { case 0: return Utils.getTabTextWithCustomFont(typeface, "Posts"); case 1: return Utils.getTabTextWithCustomFont(typeface, "Comments"); } return null; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { Fragment fragment = (Fragment) super.instantiateItem(container, position); switch (position) { case 0: themePreviewPostsFragment = (ThemePreviewPostsFragment) fragment; break; case 1: themePreviewCommentsFragment = (ThemePreviewCommentsFragment) fragment; } return fragment; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CustomizeCommentFilterActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.regex.PatternSyntaxException; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.SaveCommentFilter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityCustomizeCommentFilterBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class CustomizeCommentFilterActivity extends BaseActivity { public static final String EXTRA_COMMENT_FILTER = "ECF"; public static final String EXTRA_FROM_SETTINGS = "EFS"; public static final String EXTRA_EXCLUDE_USER = "EEU"; public static final String RETURN_EXTRA_COMMENT_FILTER = "RECF"; private static final String COMMENT_FILTER_STATE = "CFS"; private static final String ORIGINAL_NAME_STATE = "ONS"; private static final String DISPLAY_MODE_SELECTED_ITEM_INDEX_STATE = "DMSIIS"; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("current_account") SharedPreferences currentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private CommentFilter commentFilter; private boolean fromSettings; private String originalName; private ActivityCustomizeCommentFilterBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCustomizeCommentFilterBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCustomizeCommentFilterActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets windowInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); setMargins(binding.toolbarCustomizeCommentFilterActivity, windowInsets.left, windowInsets.top, windowInsets.right, BaseActivity.IGNORE_MARGIN); binding.contentWrapperViewCustomizeCommentFilterActivity.setPadding( windowInsets.left, 0, windowInsets.right, windowInsets.bottom ); setMargins(binding.contentWrapperViewCustomizeCommentFilterActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, imeInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarCustomizeCommentFilterActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarCustomizeCommentFilterActivity); ActivityResultLauncher requestAddUsersLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { Intent data = result.getData(); if (data == null) { return; } ArrayList usernames = data.getStringArrayListExtra(SearchActivity.RETURN_EXTRA_SELECTED_USERNAMES); String currentUsers = binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.getText().toString().trim(); if (usernames != null && !usernames.isEmpty()) { if (!currentUsers.isEmpty() && currentUsers.charAt(currentUsers.length() - 1) != ',') { String newString = currentUsers + ","; binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.setText(newString); } StringBuilder stringBuilder = new StringBuilder(); for (String s : usernames) { stringBuilder.append(s).append(","); } stringBuilder.deleteCharAt(stringBuilder.length() - 1); binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.append(stringBuilder.toString()); } }); binding.addUsersImageViewCustomizeCommentFilterActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); intent.putExtra(SearchActivity.EXTRA_IS_MULTI_SELECTION, true); requestAddUsersLauncher.launch(intent); }); fromSettings = getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS, false); if (savedInstanceState != null) { commentFilter = savedInstanceState.getParcelable(COMMENT_FILTER_STATE); originalName = savedInstanceState.getString(ORIGINAL_NAME_STATE); binding.displayModeSpinnerCustomizeCommentFilterActivity.setSelection(savedInstanceState.getInt(DISPLAY_MODE_SELECTED_ITEM_INDEX_STATE), false); } else { commentFilter = getIntent().getParcelableExtra(EXTRA_COMMENT_FILTER); if (commentFilter == null) { commentFilter = new CommentFilter(); originalName = ""; } else { if (!fromSettings) { originalName = ""; } else { originalName = commentFilter.name; } } bindView(); } } private void bindView() { binding.nameTextInputEditTextCustomizeCommentFilterActivity.setText(commentFilter.name); binding.displayModeSpinnerCustomizeCommentFilterActivity.setSelection(commentFilter.displayMode == CommentFilter.DisplayMode.REMOVE_COMMENT ? 0 : 1); binding.excludeStringsTextInputEditTextCustomizeCommentFilterActivity.setText(commentFilter.excludeStrings); binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.setText(commentFilter.excludeUsers); binding.minVoteTextInputEditTextCustomizeCommentFilterActivity.setText(Integer.toString(commentFilter.minVote)); binding.maxVoteTextInputEditTextCustomizeCommentFilterActivity.setText(Integer.toString(commentFilter.maxVote)); Intent intent = getIntent(); String excludeUser = intent.getStringExtra(EXTRA_EXCLUDE_USER); if (excludeUser != null && !excludeUser.equals("")) { if (!binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.getText().toString().equals("")) { binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.append(","); } binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.append(excludeUser); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCustomizeCommentFilterActivity, binding.collapsingToolbarLayoutCustomizeCommentFilterActivity, binding.toolbarCustomizeCommentFilterActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCustomizeCommentFilterActivity); int primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); int primaryIconColor = mCustomThemeWrapper.getPrimaryIconColor(); int filledCardViewBackgroundColor = mCustomThemeWrapper.getFilledCardViewBackgroundColor(); binding.nameCardViewCustomizeCommentFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.nameExplanationTextViewCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.nameTextInputLayoutCustomizeCommentFilterActivity.setBoxStrokeColor(primaryTextColor); binding.nameTextInputLayoutCustomizeCommentFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.nameTextInputEditTextCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.displayModeCardViewCustomizeCommentFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.displayModeExplanationTextViewCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.displayModeTitleTextViewCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.displayModeSpinnerCustomizeCommentFilterActivity.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { View child = parent.getChildAt(0); if (child instanceof TextView) { ((TextView) child).setTextColor(primaryTextColor); } } @Override public void onNothingSelected(AdapterView parent) { } }); binding.excludeStringsCardViewCustomizeCommentFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.excludeStringsExplanationTextViewCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.excludeStringsTextInputLayoutCustomizeCommentFilterActivity.setBoxStrokeColor(primaryTextColor); binding.excludeStringsTextInputLayoutCustomizeCommentFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.excludeStringsTextInputEditTextCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.excludeUsersCardViewCustomizeCommentFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.excludeUsersExplanationTextViewCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.excludeUsersTextInputLayoutCustomizeCommentFilterActivity.setBoxStrokeColor(primaryTextColor); binding.excludeUsersTextInputLayoutCustomizeCommentFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.addUsersImageViewCustomizeCommentFilterActivity.setImageDrawable(Utils.getTintedDrawable(this, R.drawable.ic_add_24dp, primaryIconColor)); binding.voteCardViewCustomizeCommentFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.minVoteExplanationTextViewCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.minVoteTextInputLayoutCustomizeCommentFilterActivity.setBoxStrokeColor(primaryTextColor); binding.minVoteTextInputLayoutCustomizeCommentFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.minVoteTextInputEditTextCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.maxVoteExplanationTextViewCustomizeCommentFilterActivity.setTextColor(primaryTextColor); binding.maxVoteTextInputLayoutCustomizeCommentFilterActivity.setBoxStrokeColor(primaryTextColor); binding.maxVoteTextInputLayoutCustomizeCommentFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.maxVoteTextInputEditTextCustomizeCommentFilterActivity.setTextColor(primaryTextColor); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { binding.nameTextInputLayoutCustomizeCommentFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.excludeStringsTextInputLayoutCustomizeCommentFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.excludeUsersTextInputLayoutCustomizeCommentFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.minVoteTextInputLayoutCustomizeCommentFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.maxVoteTextInputLayoutCustomizeCommentFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); } else { setCursorDrawableColor(binding.nameTextInputEditTextCustomizeCommentFilterActivity, primaryTextColor); setCursorDrawableColor(binding.excludeStringsTextInputEditTextCustomizeCommentFilterActivity, primaryTextColor); setCursorDrawableColor(binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity, primaryTextColor); setCursorDrawableColor(binding.minVoteTextInputEditTextCustomizeCommentFilterActivity, primaryTextColor); setCursorDrawableColor(binding.maxVoteTextInputEditTextCustomizeCommentFilterActivity, primaryTextColor); } if (typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), typeface); } } public void setCursorDrawableColor(EditText editText, int color) { try { Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); fCursorDrawableRes.setAccessible(true); int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); Field fEditor = TextView.class.getDeclaredField("mEditor"); fEditor.setAccessible(true); Object editor = fEditor.get(editText); Class clazz = editor.getClass(); Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); fCursorDrawable.setAccessible(true); Drawable[] drawables = new Drawable[2]; drawables[0] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[1] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); fCursorDrawable.set(editor, drawables); } catch (Throwable ignored) { } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.customize_comment_filter_activity, menu); if (fromSettings) { menu.findItem(R.id.action_save_customize_comment_filter_activity).setVisible(false); } applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_save_customize_comment_filter_activity) { try { constructCommentFilter(); Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_COMMENT_FILTER, commentFilter); setResult(Activity.RESULT_OK, returnIntent); finish(); } catch (PatternSyntaxException e) { Toast.makeText(this, R.string.invalid_regex, Toast.LENGTH_SHORT).show(); } return true; } else if (item.getItemId() == R.id.action_save_to_database_customize_comment_filter_activity) { try { constructCommentFilter(); if (!commentFilter.name.equals("")) { saveCommentFilter(originalName); } else { Toast.makeText(CustomizeCommentFilterActivity.this, R.string.comment_filter_requires_a_name, Toast.LENGTH_LONG).show(); } } catch (PatternSyntaxException e) { Toast.makeText(this, R.string.invalid_regex, Toast.LENGTH_SHORT).show(); } } return false; } private void saveCommentFilter(String originalName) { SaveCommentFilter.saveCommentFilter(mExecutor, new Handler(), mRedditDataRoomDatabase, commentFilter, originalName, new SaveCommentFilter.SaveCommentFilterListener() { @Override public void success() { Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_COMMENT_FILTER, commentFilter); setResult(Activity.RESULT_OK, returnIntent); finish(); } @Override public void duplicate() { new MaterialAlertDialogBuilder(CustomizeCommentFilterActivity.this, R.style.MaterialAlertDialogTheme) .setTitle(getString(R.string.duplicate_comment_filter_dialog_title, commentFilter.name)) .setMessage(R.string.duplicate_comment_filter_dialog_message) .setPositiveButton(R.string.override, (dialogInterface, i) -> saveCommentFilter(commentFilter.name)) .setNegativeButton(R.string.cancel, null) .show(); } }); } private void constructCommentFilter() throws PatternSyntaxException { commentFilter.name = binding.nameTextInputEditTextCustomizeCommentFilterActivity.getText().toString(); commentFilter.displayMode = binding.displayModeSpinnerCustomizeCommentFilterActivity.getSelectedItemPosition() == 0 ? CommentFilter.DisplayMode.REMOVE_COMMENT : CommentFilter.DisplayMode.COLLAPSE_COMMENT; commentFilter.excludeStrings = binding.excludeStringsTextInputEditTextCustomizeCommentFilterActivity.getText().toString(); commentFilter.excludeUsers = binding.excludeUsersTextInputEditTextCustomizeCommentFilterActivity.getText().toString(); commentFilter.maxVote = binding.maxVoteTextInputEditTextCustomizeCommentFilterActivity.getText() == null || binding.maxVoteTextInputEditTextCustomizeCommentFilterActivity.getText().toString().equals("") ? -1 : Integer.parseInt(binding.maxVoteTextInputEditTextCustomizeCommentFilterActivity.getText().toString()); commentFilter.minVote = binding.minVoteTextInputEditTextCustomizeCommentFilterActivity.getText() == null || binding.minVoteTextInputEditTextCustomizeCommentFilterActivity.getText().toString().equals("") ? -1 : Integer.parseInt(binding.minVoteTextInputEditTextCustomizeCommentFilterActivity.getText().toString()); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(COMMENT_FILTER_STATE, commentFilter); outState.putString(ORIGINAL_NAME_STATE, originalName); outState.putInt(DISPLAY_MODE_SELECTED_ITEM_INDEX_STATE, binding.displayModeSpinnerCustomizeCommentFilterActivity.getSelectedItemPosition()); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CustomizePostFilterActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityCustomizePostFilterBinding; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.SavePostFilter; import ml.docilealligator.infinityforreddit.subreddit.SubredditWithSelection; import ml.docilealligator.infinityforreddit.utils.Utils; public class CustomizePostFilterActivity extends BaseActivity { public static final String EXTRA_POST_FILTER = "EPF"; public static final String EXTRA_FROM_SETTINGS = "EFS"; public static final String EXTRA_EXCLUDE_SUBREDDIT = "EES"; public static final String EXTRA_CONTAIN_SUBREDDIT = "ECS"; public static final String EXTRA_EXCLUDE_USER = "EEU"; public static final String EXTRA_CONTAIN_USER = "ECU"; public static final String EXTRA_EXCLUDE_FLAIR = "EEF"; public static final String EXTRA_CONTAIN_FLAIR = "ECF"; public static final String EXTRA_EXCLUDE_DOMAIN = "EED"; public static final String EXTRA_CONTAIN_DOMAIN = "ECD"; public static final String EXTRA_START_FILTERED_POSTS_WHEN_FINISH = "ESFPWF"; public static final String RETURN_EXTRA_POST_FILTER = "REPF"; private static final String POST_FILTER_STATE = "PFS"; private static final String ORIGINAL_NAME_STATE = "ONS"; private static final int ADD_EXCLUDE_SUBREDDITS_REQUEST_CODE = 1; private static final int ADD_CONTAIN_SUBREDDITS_REQUEST_CODE = 11; private static final int ADD_EXCLUDE_USERS_REQUEST_CODE = 3; private static final int ADD_CONTAIN_USERS_REQUEST_CODE = 33; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private ActivityCustomizePostFilterBinding binding; private PostFilter postFilter; private boolean fromSettings; private String originalName; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCustomizePostFilterBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCustomizePostFilterActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets windowInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); setMargins(binding.toolbarCustomizePostFilterActivity, windowInsets.left, windowInsets.top, windowInsets.right, BaseActivity.IGNORE_MARGIN); binding.contentWrapperViewCustomizePostFilterActivity.setPadding( windowInsets.left, 0, windowInsets.right, windowInsets.bottom ); setMargins(binding.contentWrapperViewCustomizePostFilterActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, imeInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarCustomizePostFilterActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarCustomizePostFilterActivity); fromSettings = getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS, false); binding.postTypeTextLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.postTypeTextSwitchCustomizePostFilterActivity.performClick(); }); binding.postTypeLinkLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.postTypeLinkSwitchCustomizePostFilterActivity.performClick(); }); binding.postTypeImageLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.postTypeImageSwitchCustomizePostFilterActivity.performClick(); }); binding.postTypeGifLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.postTypeGifSwitchCustomizePostFilterActivity.performClick(); }); binding.postTypeVideoLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.postTypeVideoSwitchCustomizePostFilterActivity.performClick(); }); binding.postTypeGalleryLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.postTypeGallerySwitchCustomizePostFilterActivity.performClick(); }); binding.onlyNsfwLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.onlyNsfwSwitchCustomizePostFilterActivity.performClick(); }); binding.onlySpoilerLinearLayoutCustomizePostFilterActivity.setOnClickListener(view -> { binding.onlySpoilerSwitchCustomizePostFilterActivity.performClick(); }); binding.excludeAddSubredditsImageViewCustomizePostFilterActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubredditMultiselectionActivity.class); String s = binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString().trim(); intent.putExtra(SubredditMultiselectionActivity.EXTRA_GET_SELECTED_SUBREDDITS, binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString().trim()); startActivityForResult(intent, ADD_EXCLUDE_SUBREDDITS_REQUEST_CODE); }); binding.containAddSubredditsImageViewCustomizePostFilterActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubredditMultiselectionActivity.class); intent.putExtra(SubredditMultiselectionActivity.EXTRA_GET_SELECTED_SUBREDDITS, binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString().trim()); startActivityForResult(intent, ADD_CONTAIN_SUBREDDITS_REQUEST_CODE); }); binding.excludeAddUsersImageViewCustomizePostFilterActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); intent.putExtra(SearchActivity.EXTRA_IS_MULTI_SELECTION, true); startActivityForResult(intent, ADD_EXCLUDE_USERS_REQUEST_CODE); }); binding.containAddUsersImageViewCustomizePostFilterActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); intent.putExtra(SearchActivity.EXTRA_IS_MULTI_SELECTION, true); startActivityForResult(intent, ADD_CONTAIN_USERS_REQUEST_CODE); }); if (savedInstanceState != null) { postFilter = savedInstanceState.getParcelable(POST_FILTER_STATE); originalName = savedInstanceState.getString(ORIGINAL_NAME_STATE); } else { postFilter = getIntent().getParcelableExtra(EXTRA_POST_FILTER); if (postFilter == null) { postFilter = new PostFilter(); originalName = ""; } else { if (!fromSettings) { originalName = ""; } else { originalName = postFilter.name; } } bindView(); } } private void bindView() { binding.nameTextInputEditTextCustomizePostFilterActivity.setText(postFilter.name); binding.postTypeTextSwitchCustomizePostFilterActivity.setChecked(postFilter.containTextType); binding.postTypeLinkSwitchCustomizePostFilterActivity.setChecked(postFilter.containLinkType); binding.postTypeImageSwitchCustomizePostFilterActivity.setChecked(postFilter.containImageType); binding.postTypeGifSwitchCustomizePostFilterActivity.setChecked(postFilter.containGifType); binding.postTypeVideoSwitchCustomizePostFilterActivity.setChecked(postFilter.containVideoType); binding.postTypeGallerySwitchCustomizePostFilterActivity.setChecked(postFilter.containGalleryType); binding.onlyNsfwSwitchCustomizePostFilterActivity.setChecked(postFilter.onlyNSFW); binding.onlySpoilerSwitchCustomizePostFilterActivity.setChecked(postFilter.onlySpoiler); binding.titleExcludesStringsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.postTitleExcludesStrings); binding.titleContainsStringsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.postTitleContainsStrings); binding.titleExcludesRegexTextInputEditTextCustomizePostFilterActivity.setText(postFilter.postTitleExcludesRegex); binding.titleContainsRegexTextInputEditTextCustomizePostFilterActivity.setText(postFilter.postTitleContainsRegex); binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.excludeSubreddits); binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.containSubreddits); binding.excludesUsersTextInputEditTextCustomizePostFilterActivity.setText(postFilter.excludeUsers); binding.containsUsersTextInputEditTextCustomizePostFilterActivity.setText(postFilter.containUsers); binding.excludesFlairsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.excludeFlairs); binding.containsFlairsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.containFlairs); binding.excludeDomainsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.excludeDomains); binding.containDomainsTextInputEditTextCustomizePostFilterActivity.setText(postFilter.containDomains); binding.minVoteTextInputEditTextCustomizePostFilterActivity.setText(Integer.toString(postFilter.minVote)); binding.maxVoteTextInputEditTextCustomizePostFilterActivity.setText(Integer.toString(postFilter.maxVote)); binding.minCommentsTextInputEditTextCustomizePostFilterActivity.setText(Integer.toString(postFilter.minComments)); binding.maxCommentsTextInputEditTextCustomizePostFilterActivity.setText(Integer.toString(postFilter.maxComments)); Intent intent = getIntent(); String excludeSubreddit = intent.getStringExtra(EXTRA_EXCLUDE_SUBREDDIT); String excludeUser = intent.getStringExtra(EXTRA_EXCLUDE_USER); String excludeFlair = intent.getStringExtra(EXTRA_EXCLUDE_FLAIR); String containFlair = intent.getStringExtra(EXTRA_CONTAIN_FLAIR); String excludeDomain = intent.getStringExtra(EXTRA_EXCLUDE_DOMAIN); String containDomain = intent.getStringExtra(EXTRA_CONTAIN_DOMAIN); String containSubreddit = intent.getStringExtra(EXTRA_CONTAIN_SUBREDDIT); String containUser = intent.getStringExtra(EXTRA_CONTAIN_USER); if (excludeSubreddit != null && !excludeSubreddit.equals("")) { if (!binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity.append(","); } binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity.append(excludeSubreddit); } if (containSubreddit != null && !containSubreddit.equals("")) { if (!binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.append(","); } binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.append(containSubreddit); } if (containUser != null && !containUser.equals("")) { if (!binding.containsUsersTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.containsUsersTextInputEditTextCustomizePostFilterActivity.append(","); } binding.containsUsersTextInputEditTextCustomizePostFilterActivity.append(containUser); } if (excludeUser != null && !excludeUser.equals("")) { if (!binding.excludesUsersTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.excludesUsersTextInputEditTextCustomizePostFilterActivity.append(","); } binding.excludesUsersTextInputEditTextCustomizePostFilterActivity.append(excludeUser); } if (excludeFlair != null && !excludeFlair.equals("")) { if (!binding.excludesFlairsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.excludesFlairsTextInputEditTextCustomizePostFilterActivity.append(","); } binding.excludesFlairsTextInputEditTextCustomizePostFilterActivity.append(excludeFlair); } if (containFlair != null && !containFlair.equals("")) { if (!binding.containsFlairsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.containsFlairsTextInputEditTextCustomizePostFilterActivity.append(","); } binding.containsFlairsTextInputEditTextCustomizePostFilterActivity.append(containFlair); } if (excludeDomain != null && !excludeDomain.equals("")) { if (!binding.excludeDomainsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.excludeDomainsTextInputEditTextCustomizePostFilterActivity.append(","); } binding.excludeDomainsTextInputEditTextCustomizePostFilterActivity.append(Uri.parse(excludeDomain).getHost()); } if (containDomain != null && !containDomain.equals("")) { if (!binding.containDomainsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.containDomainsTextInputEditTextCustomizePostFilterActivity.append(","); } binding.containDomainsTextInputEditTextCustomizePostFilterActivity.append(Uri.parse(containDomain).getHost()); } if (containUser != null && !containUser.equals("")) { if (!binding.containsUsersTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.containsUsersTextInputEditTextCustomizePostFilterActivity.append(","); } binding.containsUsersTextInputEditTextCustomizePostFilterActivity.append(containUser); } if (containSubreddit != null && !containSubreddit.equals("")) { if (!binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("")) { binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.append(","); } binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.append(containSubreddit); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutCustomizePostFilterActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCustomizePostFilterActivity, binding.collapsingToolbarLayoutCustomizePostFilterActivity, binding.toolbarCustomizePostFilterActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCustomizePostFilterActivity); int primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); int primaryIconColor = mCustomThemeWrapper.getPrimaryIconColor(); int filledCardViewBackgroundColor = mCustomThemeWrapper.getFilledCardViewBackgroundColor(); binding.nameCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.nameExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.nameTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.nameTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.nameTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.postTypeCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.postTypeExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.postTypeTextTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_text_day_night_24dp, primaryIconColor), null, null, null); binding.postTypeTextTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.postTypeLinkTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_link_day_night_24dp, primaryIconColor), null, null, null); binding.postTypeLinkTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.postTypeImageTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_image_day_night_24dp, primaryIconColor), null, null, null); binding.postTypeImageTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.postTypeGifTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_image_day_night_24dp, primaryIconColor), null, null, null); binding.postTypeGifTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.postTypeVideoTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_video_day_night_24dp, primaryIconColor), null, null, null); binding.postTypeVideoTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.postTypeGalleryTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_gallery_day_night_24dp, primaryIconColor), null, null, null); binding.postTypeGalleryTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.onlyNsfwSpoilerCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.onlyNsfwSpoilerExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.onlyNsfwTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_nsfw_on_day_night_24dp, primaryIconColor), null, null, null); binding.onlyNsfwTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.onlySpoilerTextViewCustomizePostFilterActivity.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(this, R.drawable.ic_spoiler_black_24dp, primaryIconColor), null, null, null); binding.onlySpoilerTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleStringsCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.titleExcludeStringsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleExcludesStringsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.titleExcludesStringsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.titleExcludesStringsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleContainsStringsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleContainsStringsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.titleContainsStringsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.titleContainsStringsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleRegexCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.titleExcludesRegexExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleExcludesRegexTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.titleExcludesRegexTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.titleExcludesRegexTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleContainsRegexExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.titleContainsRegexTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.titleContainsRegexTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.titleContainsRegexTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.subredditsCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.excludeSubredditsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.excludesSubredditsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.excludesSubredditsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.excludeAddSubredditsImageViewCustomizePostFilterActivity.setImageDrawable(Utils.getTintedDrawable(this, R.drawable.ic_add_24dp, primaryIconColor)); binding.containSubredditsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containsSubredditsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.containsSubredditsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containAddSubredditsImageViewCustomizePostFilterActivity.setImageDrawable(Utils.getTintedDrawable(this, R.drawable.ic_add_24dp, primaryIconColor)); binding.usersCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.excludeUsersExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.excludesUsersTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.excludesUsersTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.excludesUsersTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.excludeAddUsersImageViewCustomizePostFilterActivity.setImageDrawable(Utils.getTintedDrawable(this, R.drawable.ic_add_24dp, primaryIconColor)); binding.containUsersExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containsUsersTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.containsUsersTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.containsUsersTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containAddUsersImageViewCustomizePostFilterActivity.setImageDrawable(Utils.getTintedDrawable(this, R.drawable.ic_add_24dp, primaryIconColor)); binding.flairsCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.excludeFlairsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.excludesFlairsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.excludesFlairsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.excludesFlairsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containFlairsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containsFlairsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.containsFlairsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.containsFlairsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.domainsCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.excludeDomainsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.excludeDomainsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.excludeDomainsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.excludeDomainsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containDomainsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.containDomainsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.containDomainsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.containDomainsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.voteCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.minVoteExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.minVoteTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.minVoteTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.minVoteTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.maxVoteExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.maxVoteTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.maxVoteTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.maxVoteTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.commentsCardViewCustomizePostFilterActivity.setCardBackgroundColor(filledCardViewBackgroundColor); binding.minCommentsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.minCommentsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.minCommentsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.minCommentsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.maxCommentsExplanationTextViewCustomizePostFilterActivity.setTextColor(primaryTextColor); binding.maxCommentsTextInputLayoutCustomizePostFilterActivity.setBoxStrokeColor(primaryTextColor); binding.maxCommentsTextInputLayoutCustomizePostFilterActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.maxCommentsTextInputEditTextCustomizePostFilterActivity.setTextColor(primaryTextColor); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { binding.nameTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.titleExcludesStringsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.titleContainsStringsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.titleExcludesRegexTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.titleContainsRegexTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.excludesSubredditsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.containsSubredditsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.excludesUsersTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.containsUsersTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.excludesFlairsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.containsFlairsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.excludeDomainsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.containDomainsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.minVoteTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.maxVoteTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.minCommentsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.maxCommentsTextInputLayoutCustomizePostFilterActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); } else { setCursorDrawableColor(binding.nameTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.titleExcludesStringsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.titleContainsStringsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.titleExcludesRegexTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.titleContainsRegexTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.excludesUsersTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.containsUsersTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.excludesFlairsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.containsUsersTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.containsFlairsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.excludeDomainsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.containDomainsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.minVoteTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.maxVoteTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.minCommentsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); setCursorDrawableColor(binding.maxCommentsTextInputEditTextCustomizePostFilterActivity, primaryTextColor); } if (typeface != null) { Utils.setFontToAllTextViews(binding.coordinatorLayoutCustomizePostFilterActivity, typeface); } } private void setCursorDrawableColor(EditText editText, int color) { try { Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); fCursorDrawableRes.setAccessible(true); int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); Field fEditor = TextView.class.getDeclaredField("mEditor"); fEditor.setAccessible(true); Object editor = fEditor.get(editText); Class clazz = editor.getClass(); Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); fCursorDrawable.setAccessible(true); Drawable[] drawables = new Drawable[2]; drawables[0] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[1] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); fCursorDrawable.set(editor, drawables); } catch (Throwable ignored) { } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.customize_post_filter_activity, menu); if (fromSettings) { menu.findItem(R.id.action_save_customize_post_filter_activity).setVisible(false); } applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_save_customize_post_filter_activity) { try { constructPostFilter(); if (getIntent().getBooleanExtra(EXTRA_START_FILTERED_POSTS_WHEN_FINISH, false)) { Intent intent = new Intent(this, FilteredPostsActivity.class); intent.putExtras(getIntent()); intent.putExtra(FilteredPostsActivity.EXTRA_CONSTRUCTED_POST_FILTER, postFilter); startActivity(intent); } else { Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_POST_FILTER, postFilter); setResult(Activity.RESULT_OK, returnIntent); } finish(); } catch (PatternSyntaxException e) { Toast.makeText(this, R.string.invalid_regex, Toast.LENGTH_SHORT).show(); } return true; } else if (item.getItemId() == R.id.action_save_to_database_customize_post_filter_activity) { try { constructPostFilter(); if (!postFilter.name.equals("")) { savePostFilter(originalName); } else { Toast.makeText(CustomizePostFilterActivity.this, R.string.post_filter_requires_a_name, Toast.LENGTH_LONG).show(); } } catch (PatternSyntaxException e) { Toast.makeText(this, R.string.invalid_regex, Toast.LENGTH_SHORT).show(); } } return false; } private void savePostFilter(String originalName) { SavePostFilter.savePostFilter(mExecutor, new Handler(), mRedditDataRoomDatabase, postFilter, originalName, new SavePostFilter.SavePostFilterListener() { @Override public void success() { if (getIntent().getBooleanExtra(EXTRA_START_FILTERED_POSTS_WHEN_FINISH, false)) { Intent intent = new Intent(CustomizePostFilterActivity.this, FilteredPostsActivity.class); intent.putExtras(getIntent()); intent.putExtra(FilteredPostsActivity.EXTRA_CONSTRUCTED_POST_FILTER, postFilter); startActivity(intent); } else { Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_POST_FILTER, postFilter); setResult(Activity.RESULT_OK, returnIntent); } finish(); } @Override public void duplicate() { new MaterialAlertDialogBuilder(CustomizePostFilterActivity.this, R.style.MaterialAlertDialogTheme) .setTitle(getString(R.string.duplicate_post_filter_dialog_title, postFilter.name)) .setMessage(R.string.duplicate_post_filter_dialog_message) .setPositiveButton(R.string.override, (dialogInterface, i) -> savePostFilter(postFilter.name)) .setNegativeButton(R.string.cancel, null) .show(); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && data != null) { if (requestCode == ADD_EXCLUDE_SUBREDDITS_REQUEST_CODE) { ArrayList subredditWithSelections = data.getParcelableArrayListExtra(SubredditMultiselectionActivity.EXTRA_RETURN_SELECTED_SUBREDDITS); updateSubredditsUsersNames(new ArrayList<>(subredditWithSelections.stream().map(SubredditWithSelection::getName).collect(Collectors.toList())), binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity); } else if (requestCode == ADD_CONTAIN_SUBREDDITS_REQUEST_CODE) { ArrayList subredditWithSelections = data.getParcelableArrayListExtra(SubredditMultiselectionActivity.EXTRA_RETURN_SELECTED_SUBREDDITS); updateSubredditsUsersNames(new ArrayList<>(subredditWithSelections.stream().map(SubredditWithSelection::getName).collect(Collectors.toList())), binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity); } else if (requestCode == ADD_EXCLUDE_USERS_REQUEST_CODE) { ArrayList usernames = data.getStringArrayListExtra(SearchActivity.RETURN_EXTRA_SELECTED_USERNAMES); updateSubredditsUsersNames(usernames, binding.excludesUsersTextInputEditTextCustomizePostFilterActivity); } else if (requestCode == ADD_CONTAIN_USERS_REQUEST_CODE) { ArrayList usernames = data.getStringArrayListExtra(SearchActivity.RETURN_EXTRA_SELECTED_USERNAMES); updateSubredditsUsersNames(usernames, binding.containsUsersTextInputEditTextCustomizePostFilterActivity); } } } private void updateSubredditsUsersNames(@Nullable ArrayList subredditNames, com.google.android.material.textfield.TextInputEditText targetEditText) { if (subredditNames == null || subredditNames.isEmpty() || targetEditText == null) return; String current = targetEditText.getText().toString().trim(); if (!current.isEmpty() && current.charAt(current.length() - 1) != ',') { targetEditText.setText(current + ","); } StringBuilder sb = new StringBuilder(); for (String s : subredditNames) { sb.append(s).append(","); } if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); targetEditText.append(sb.toString()); } private void constructPostFilter() throws PatternSyntaxException { postFilter.name = binding.nameTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.maxVote = binding.maxVoteTextInputEditTextCustomizePostFilterActivity.getText() == null || binding.maxVoteTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("") ? -1 : Integer.parseInt(binding.maxVoteTextInputEditTextCustomizePostFilterActivity.getText().toString()); postFilter.minVote = binding.minVoteTextInputEditTextCustomizePostFilterActivity.getText() == null || binding.minVoteTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("") ? -1 : Integer.parseInt(binding.minVoteTextInputEditTextCustomizePostFilterActivity.getText().toString()); postFilter.maxComments = binding.maxCommentsTextInputEditTextCustomizePostFilterActivity.getText() == null || binding.maxCommentsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("") ? -1 : Integer.parseInt(binding.maxCommentsTextInputEditTextCustomizePostFilterActivity.getText().toString()); postFilter.minComments = binding.minCommentsTextInputEditTextCustomizePostFilterActivity.getText() == null || binding.minCommentsTextInputEditTextCustomizePostFilterActivity.getText().toString().equals("") ? -1 : Integer.parseInt(binding.minCommentsTextInputEditTextCustomizePostFilterActivity.getText().toString()); postFilter.maxAwards = -1; postFilter.minAwards = -1; postFilter.postTitleExcludesRegex = binding.titleExcludesRegexTextInputEditTextCustomizePostFilterActivity.getText().toString(); Pattern.compile(postFilter.postTitleExcludesRegex); postFilter.postTitleContainsRegex = binding.titleContainsRegexTextInputEditTextCustomizePostFilterActivity.getText().toString(); Pattern.compile(postFilter.postTitleContainsRegex); postFilter.postTitleExcludesStrings = binding.titleExcludesStringsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.postTitleContainsStrings = binding.titleContainsStringsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.excludeSubreddits = binding.excludesSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.containSubreddits = binding.containsSubredditsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.excludeUsers = binding.excludesUsersTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.containUsers = binding.containsUsersTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.excludeFlairs = binding.excludesFlairsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.containFlairs = binding.containsFlairsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.excludeDomains = binding.excludeDomainsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.containDomains = binding.containDomainsTextInputEditTextCustomizePostFilterActivity.getText().toString(); postFilter.containTextType = binding.postTypeTextSwitchCustomizePostFilterActivity.isChecked(); postFilter.containLinkType = binding.postTypeLinkSwitchCustomizePostFilterActivity.isChecked(); postFilter.containImageType = binding.postTypeImageSwitchCustomizePostFilterActivity.isChecked(); postFilter.containGifType = binding.postTypeGifSwitchCustomizePostFilterActivity.isChecked(); postFilter.containVideoType = binding.postTypeVideoSwitchCustomizePostFilterActivity.isChecked(); postFilter.containGalleryType = binding.postTypeGallerySwitchCustomizePostFilterActivity.isChecked(); postFilter.onlyNSFW = binding.onlyNsfwSwitchCustomizePostFilterActivity.isChecked(); postFilter.onlySpoiler = binding.onlySpoilerSwitchCustomizePostFilterActivity.isChecked(); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(POST_FILTER_STATE, postFilter); outState.putString(ORIGINAL_NAME_STATE, originalName); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/CustomizeThemeActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.CustomizeThemeRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.ServerAPI; import ml.docilealligator.infinityforreddit.asynctasks.GetCustomTheme; import ml.docilealligator.infinityforreddit.asynctasks.InsertCustomTheme; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeSettingsItem; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customtheme.OnlineCustomThemeMetadata; import ml.docilealligator.infinityforreddit.databinding.ActivityCustomizeThemeBinding; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class CustomizeThemeActivity extends BaseActivity { public static final String EXTRA_THEME_TYPE = "ETT"; public static final int EXTRA_LIGHT_THEME = CustomThemeSharedPreferencesUtils.LIGHT; public static final int EXTRA_DARK_THEME = CustomThemeSharedPreferencesUtils.DARK; public static final int EXTRA_AMOLED_THEME = CustomThemeSharedPreferencesUtils.AMOLED; public static final String EXTRA_THEME_NAME = "ETN"; public static final String EXTRA_ONLINE_CUSTOM_THEME_METADATA = "EOCTM"; public static final String EXTRA_INDEX_IN_THEME_LIST = "EIITL"; public static final String EXTRA_IS_PREDEFIINED_THEME = "EIPT"; public static final String EXTRA_CREATE_THEME = "ECT"; public static final String RETURN_EXTRA_THEME_NAME = "RETN"; public static final String RETURN_EXTRA_PRIMARY_COLOR = "REPC"; public static final String RETURN_EXTRA_INDEX_IN_THEME_LIST = "REIITL"; private static final String CUSTOM_THEME_SETTINGS_ITEMS_STATE = "CTSIS"; private static final String THEME_NAME_STATE = "TNS"; @Inject @Named("online_custom_themes") Retrofit onlineCustomThemesRetrofit; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("light_theme") SharedPreferences lightThemeSharedPreferences; @Inject @Named("dark_theme") SharedPreferences darkThemeSharedPreferences; @Inject @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor mExecutor; private String themeName; private OnlineCustomThemeMetadata onlineCustomThemeMetadata; private boolean isPredefinedTheme; private ArrayList customThemeSettingsItems; private CustomizeThemeRecyclerViewAdapter adapter; private ActivityCustomizeThemeBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityCustomizeThemeBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCustomizeThemeActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarCustomizeThemeActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewCustomizeThemeActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarCustomizeThemeActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (getIntent().getBooleanExtra(EXTRA_CREATE_THEME, false)) { setTitle(R.string.customize_theme_activity_create_theme_label); } if (savedInstanceState != null) { customThemeSettingsItems = savedInstanceState.getParcelableArrayList(CUSTOM_THEME_SETTINGS_ITEMS_STATE); themeName = savedInstanceState.getString(THEME_NAME_STATE); } binding.progressBarCustomizeThemeActivity.setVisibility(View.GONE); int androidVersion = Build.VERSION.SDK_INT; if (customThemeSettingsItems == null) { if (getIntent().hasExtra(EXTRA_THEME_TYPE)) { int themeType = getIntent().getIntExtra(EXTRA_THEME_TYPE, EXTRA_LIGHT_THEME); GetCustomTheme.getCustomTheme(mExecutor, new Handler(), redditDataRoomDatabase, themeType, customTheme -> { if (customTheme == null) { isPredefinedTheme = true; switch (themeType) { case EXTRA_DARK_THEME: customThemeSettingsItems = CustomThemeSettingsItem.convertCustomThemeToSettingsItem( CustomizeThemeActivity.this, CustomThemeWrapper.getIndigoDark(CustomizeThemeActivity.this), androidVersion); themeName = getString(R.string.theme_name_indigo_dark); break; case EXTRA_AMOLED_THEME: customThemeSettingsItems = CustomThemeSettingsItem.convertCustomThemeToSettingsItem( CustomizeThemeActivity.this, CustomThemeWrapper.getIndigoAmoled(CustomizeThemeActivity.this), androidVersion); themeName = getString(R.string.theme_name_indigo_amoled); break; default: customThemeSettingsItems = CustomThemeSettingsItem.convertCustomThemeToSettingsItem( CustomizeThemeActivity.this, CustomThemeWrapper.getIndigo(CustomizeThemeActivity.this), androidVersion); themeName = getString(R.string.theme_name_indigo); } } else { customThemeSettingsItems = CustomThemeSettingsItem.convertCustomThemeToSettingsItem( CustomizeThemeActivity.this, customTheme, androidVersion); themeName = customTheme.name; } adapter = new CustomizeThemeRecyclerViewAdapter(this, customThemeWrapper, themeName); binding.recyclerViewCustomizeThemeActivity.setAdapter(adapter); adapter.setCustomThemeSettingsItem(customThemeSettingsItems); }); } else { isPredefinedTheme = getIntent().getBooleanExtra(EXTRA_IS_PREDEFIINED_THEME, false); themeName = getIntent().getStringExtra(EXTRA_THEME_NAME); onlineCustomThemeMetadata = getIntent().getParcelableExtra(EXTRA_ONLINE_CUSTOM_THEME_METADATA); adapter = new CustomizeThemeRecyclerViewAdapter(this, customThemeWrapper, themeName); binding.recyclerViewCustomizeThemeActivity.setAdapter(adapter); if (isPredefinedTheme) { customThemeSettingsItems = CustomThemeSettingsItem.convertCustomThemeToSettingsItem( CustomizeThemeActivity.this, CustomThemeWrapper.getPredefinedCustomTheme(this, themeName), androidVersion); adapter = new CustomizeThemeRecyclerViewAdapter(this, customThemeWrapper, themeName); binding.recyclerViewCustomizeThemeActivity.setAdapter(adapter); adapter.setCustomThemeSettingsItem(customThemeSettingsItems); } else { if (onlineCustomThemeMetadata != null) { binding.progressBarCustomizeThemeActivity.setVisibility(View.VISIBLE); onlineCustomThemesRetrofit.create(ServerAPI.class) .getCustomTheme(onlineCustomThemeMetadata.name, onlineCustomThemeMetadata.username) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { customThemeSettingsItems = CustomThemeSettingsItem.convertCustomThemeToSettingsItem( CustomizeThemeActivity.this, CustomTheme.fromJson(response.body()), androidVersion); adapter.setCustomThemeSettingsItem(customThemeSettingsItems); binding.progressBarCustomizeThemeActivity.setVisibility(View.GONE); } else { Toast.makeText(CustomizeThemeActivity.this, response.message(), Toast.LENGTH_SHORT).show(); finish(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { Toast.makeText(CustomizeThemeActivity.this, R.string.cannot_download_theme_data, Toast.LENGTH_SHORT).show(); finish(); } }); } else { GetCustomTheme.getCustomTheme(mExecutor, new Handler(), redditDataRoomDatabase, themeName, customTheme -> { customThemeSettingsItems = CustomThemeSettingsItem.convertCustomThemeToSettingsItem( CustomizeThemeActivity.this, customTheme, androidVersion); adapter.setCustomThemeSettingsItem(customThemeSettingsItems); }); } } } } else { adapter = new CustomizeThemeRecyclerViewAdapter(this, customThemeWrapper, themeName); binding.recyclerViewCustomizeThemeActivity.setAdapter(adapter); adapter.setCustomThemeSettingsItem(customThemeSettingsItems); } getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { new MaterialAlertDialogBuilder(CustomizeThemeActivity.this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.discard) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> { setEnabled(false); triggerBackPress(); }) .setNegativeButton(R.string.no, null) .show(); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.customize_theme_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_preview_customize_theme_activity) { Intent intent = new Intent(this, CustomThemePreviewActivity.class); intent.putParcelableArrayListExtra(CustomThemePreviewActivity.EXTRA_CUSTOM_THEME_SETTINGS_ITEMS, customThemeSettingsItems); startActivity(intent); return true; } else if (itemId == R.id.action_save_customize_theme_activity) { if (adapter != null) { themeName = adapter.getThemeName(); if (themeName.equals("")) { Snackbar.make(binding.coordinatorCustomizeThemeActivity, R.string.no_theme_name, Snackbar.LENGTH_SHORT).show(); return true; } CustomTheme customTheme = CustomTheme.convertSettingsItemsToCustomTheme(customThemeSettingsItems, themeName); if (onlineCustomThemeMetadata != null && onlineCustomThemeMetadata.username.equals(accountName)) { // This custom theme is uploaded by the current user final int[] option = {0}; new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.save_theme_options_title) //.setMessage(R.string.save_theme_options_message) .setSingleChoiceItems(R.array.save_theme_options, 0, (dialog, which) -> option[0] = which) .setPositiveButton(R.string.ok, (dialogInterface, which) -> { switch (option[0]) { case 0: saveThemeLocally(customTheme); break; case 1: saveThemeOnline(customTheme, false); break; case 2: saveThemeLocally(customTheme); saveThemeOnline(customTheme, false); break; } }) .setNegativeButton(R.string.cancel, null) .show(); } else { /*// This custom theme is from the server but not uploaded by the current user, or it is local final int[] option = {0}; new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.save_theme_options_title) //.setMessage(R.string.save_theme_options_message) .setSingleChoiceItems(R.array.save_theme_options_anonymous_included, 0, (dialog, which) -> option[0] = which) .setPositiveButton(R.string.ok, (dialogInterface, which) -> { switch (option[0]) { case 0: saveThemeLocally(customTheme); break; case 1: saveThemeOnline(customTheme, false); break; case 2: saveThemeOnline(customTheme, true); break; case 3: saveThemeLocally(customTheme); saveThemeOnline(customTheme, false); break; case 4: saveThemeLocally(customTheme); saveThemeOnline(customTheme, true); break; } }) .setNegativeButton(R.string.cancel, null) .show();*/ saveThemeLocally(customTheme); } } return true; } return false; } private void saveThemeLocally(CustomTheme customTheme) { InsertCustomTheme.insertCustomTheme(mExecutor, new Handler(), redditDataRoomDatabase, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, customTheme, false, () -> { Toast.makeText(CustomizeThemeActivity.this, R.string.theme_saved_locally, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new RecreateActivityEvent()); finish(); }); } private void saveThemeOnline(CustomTheme customTheme, boolean anonymous) { Call request; // TODO server access token if (onlineCustomThemeMetadata != null) { request = onlineCustomThemesRetrofit.create(ServerAPI.class).modifyTheme( APIUtils.getServerHeader("", accountName, anonymous), onlineCustomThemeMetadata.id, customTheme.name, customTheme.getJSONModel() ); } else { request = onlineCustomThemesRetrofit.create(ServerAPI.class).createTheme( APIUtils.getServerHeader("", accountName, anonymous), customTheme.name, customTheme.getJSONModel() ); } request.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { Toast.makeText(CustomizeThemeActivity.this, R.string.theme_saved_online, Toast.LENGTH_SHORT).show(); Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_INDEX_IN_THEME_LIST, getIntent().getIntExtra(EXTRA_INDEX_IN_THEME_LIST, -1)); returnIntent.putExtra(RETURN_EXTRA_THEME_NAME, customTheme.name); returnIntent.putExtra(RETURN_EXTRA_PRIMARY_COLOR, '#' + Integer.toHexString(customTheme.colorPrimary)); setResult(RESULT_OK, returnIntent); finish(); } else { Toast.makeText(CustomizeThemeActivity.this, R.string.upload_theme_failed, Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { Toast.makeText(CustomizeThemeActivity.this, R.string.upload_theme_failed, Toast.LENGTH_SHORT).show(); } }); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (adapter != null) { outState.putParcelableArrayList(CUSTOM_THEME_SETTINGS_ITEMS_STATE, customThemeSettingsItems); outState.putString(THEME_NAME_STATE, adapter.getThemeName()); } } @Override public SharedPreferences getDefaultSharedPreferences() { return sharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return customThemeWrapper; } @Override protected void applyCustomTheme() { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCustomizeThemeActivity, binding.collapsingToolbarLayoutCustomizeThemeActivity, binding.toolbarCustomizeThemeActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCustomizeThemeActivity); binding.coordinatorCustomizeThemeActivity.setBackgroundColor(customThemeWrapper.getBackgroundColor()); binding.progressBarCustomizeThemeActivity.setIndicatorColor(customThemeWrapper.getColorAccent()); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/EditCommentActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.giphy.sdk.core.models.Media; import com.giphy.sdk.ui.GPHContentType; import com.giphy.sdk.ui.Giphy; import com.giphy.sdk.ui.views.GiphyDialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import kotlin.Unit; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UploadedImagesBottomSheetFragment; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.comment.ParseComment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityEditCommentBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.markdown.RichTextJSONConverter; import ml.docilealligator.infinityforreddit.repositories.EditCommentActivityRepository; import ml.docilealligator.infinityforreddit.thing.GiphyGif; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.viewmodels.EditCommentActivityViewModel; import retrofit2.Response; import retrofit2.Retrofit; public class EditCommentActivity extends BaseActivity implements UploadImageEnabledActivity, GiphyDialogFragment.GifSelectionListener { public static final String EXTRA_CONTENT = "EC"; public static final String EXTRA_FULLNAME = "EF"; public static final String EXTRA_MEDIA_METADATA_LIST = "EMML"; public static final String EXTRA_POSITION = "EP"; public static final String RETURN_EXTRA_EDITED_COMMENT = "REEC"; public static final String RETURN_EXTRA_EDITED_COMMENT_CONTENT = "REECC"; public static final String RETURN_EXTRA_EDITED_COMMENT_POSITION = "REECP"; private static final int PICK_IMAGE_REQUEST_CODE = 100; private static final int CAPTURE_IMAGE_REQUEST_CODE = 200; private static final int MARKDOWN_PREVIEW_REQUEST_CODE = 300; private static final String UPLOADED_IMAGES_STATE = "UIS"; private static final String GIPHY_GIF_STATE = "GGS"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String mFullName; private String mAccessToken; private String mCommentContent; private boolean isSubmitting = false; private Uri capturedImageUri; private ArrayList uploadedImages = new ArrayList<>(); private GiphyGif giphyGif; private ActivityEditCommentBinding binding; public EditCommentActivityViewModel editCommentActivityViewModel; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityEditCommentBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutEditCommentActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarEditCommentActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutEditCommentActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarEditCommentActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mFullName = getIntent().getStringExtra(EXTRA_FULLNAME); mAccessToken = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCESS_TOKEN, null); mCommentContent = getIntent().getStringExtra(EXTRA_CONTENT); ArrayList mediaMetadataList = getIntent().getParcelableArrayListExtra(EXTRA_MEDIA_METADATA_LIST); if (mediaMetadataList != null) { StringBuilder sb = new StringBuilder(mCommentContent); for (MediaMetadata m : mediaMetadataList) { int index = sb.indexOf(m.original.url); if (index >= 0) { if (index > 0 && sb.charAt(index - 1) == '(') { sb.replace(index, index + m.original.url.length(), m.id); } else { sb.insert(index + m.original.url.length(), ')') .insert(index, "![](") .replace(index + 4, index + 4 + m.original.url.length(), m.id); } uploadedImages.add(new UploadedImage(m.id, m.id)); } } mCommentContent = sb.toString(); } binding.commentEditTextEditCommentActivity.setText(mCommentContent); if (savedInstanceState != null) { uploadedImages = savedInstanceState.getParcelableArrayList(UPLOADED_IMAGES_STATE); giphyGif = savedInstanceState.getParcelable(GIPHY_GIF_STATE); } MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, true, true, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( EditCommentActivity.this, binding.commentEditTextEditCommentActivity, item); } @Override public void onUploadImage() { Utils.hideKeyboard(EditCommentActivity.this); UploadedImagesBottomSheetFragment fragment = new UploadedImagesBottomSheetFragment(); Bundle arguments = new Bundle(); arguments.putParcelableArrayList(UploadedImagesBottomSheetFragment.EXTRA_UPLOADED_IMAGES, uploadedImages); fragment.setArguments(arguments); fragment.show(getSupportFragmentManager(), fragment.getTag()); } @Override public void onSelectGiphyGif() { GiphyDialogFragment.Companion.newInstance().show(getSupportFragmentManager(), "giphy_dialog"); } }); binding.markdownBottomBarRecyclerViewEditCommentActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManager.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewEditCommentActivity.setAdapter(adapter); binding.commentEditTextEditCommentActivity.requestFocus(); Utils.showKeyboard(this, new Handler(), binding.commentEditTextEditCommentActivity); Giphy.INSTANCE.configure(this, APIUtils.getGiphyApiKey(this)); editCommentActivityViewModel = new ViewModelProvider( this, EditCommentActivityViewModel.Companion.provideFactory(new EditCommentActivityRepository(mRedditDataRoomDatabase.commentDraftDao())) ).get(EditCommentActivityViewModel.class); if (savedInstanceState == null) { editCommentActivityViewModel.getCommentDraft(mFullName).observe(this, commentDraft -> { if (commentDraft != null && !commentDraft.getContent().isEmpty()) { binding.commentEditTextEditCommentActivity.setText(commentDraft.getContent()); } }); } getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isSubmitting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_edit_comment_detail, false); } else { String content = binding.commentEditTextEditCommentActivity.getText().toString(); if (content.isEmpty() || content.equals(mCommentContent)) { editCommentActivityViewModel.deleteCommentDraft(mFullName, () -> { setEnabled(false); triggerBackPress(); return Unit.INSTANCE; }); } else { promptAlertDialog(R.string.save_comment_draft, R.string.save_comment_draft_detail, true); } } } }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutEditCommentActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutEditCommentActivity, null, binding.toolbarEditCommentActivity); binding.commentEditTextEditCommentActivity.setTextColor(mCustomThemeWrapper.getCommentColor()); if (contentTypeface != null) { binding.commentEditTextEditCommentActivity.setTypeface(contentTypeface); } } @Override protected void onPause() { super.onPause(); Utils.hideKeyboard(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.edit_comment_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_preview_edit_comment_activity) { Intent intent = new Intent(this, FullMarkdownActivity.class); intent.putExtra(FullMarkdownActivity.EXTRA_MARKDOWN, binding.commentEditTextEditCommentActivity.getText().toString()); intent.putExtra(FullMarkdownActivity.EXTRA_SUBMIT_POST, true); startActivityForResult(intent, MARKDOWN_PREVIEW_REQUEST_CODE); } else if (item.getItemId() == R.id.action_send_edit_comment_activity) { editComment(); return true; } else if (item.getItemId() == android.R.id.home) { triggerBackPress(); return true; } return false; } private void editComment() { if (!isSubmitting) { isSubmitting = true; Snackbar.make(binding.coordinatorLayoutEditCommentActivity, R.string.posting, Snackbar.LENGTH_SHORT).show(); String content = binding.commentEditTextEditCommentActivity.getText().toString(); Map params = new HashMap<>(); params.put(APIUtils.THING_ID_KEY, mFullName); if (!uploadedImages.isEmpty() || giphyGif != null) { try { params.put(APIUtils.RICHTEXT_JSON_KEY, new RichTextJSONConverter().constructRichTextJSON(this, content, uploadedImages, giphyGif)); params.put(APIUtils.TEXT_KEY, ""); } catch (JSONException e) { isSubmitting = false; Snackbar.make(binding.coordinatorLayoutEditCommentActivity, R.string.convert_to_richtext_json_failed, Snackbar.LENGTH_SHORT).show(); return; } } else { params.put(APIUtils.TEXT_KEY, content); } Handler handler = new Handler(getMainLooper()); mExecutor.execute(() -> { try { Response response = mOauthRetrofit.create(RedditAPI.class) .editPostOrComment(APIUtils.getOAuthHeader(mAccessToken), params).execute(); if (response.isSuccessful()) { Comment comment = ParseComment.parseSingleComment(new JSONObject(response.body()), 0); handler.post(() -> { isSubmitting = false; Toast.makeText(EditCommentActivity.this, R.string.edit_success, Toast.LENGTH_SHORT).show(); Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_EDITED_COMMENT, comment); returnIntent.putExtra(RETURN_EXTRA_EDITED_COMMENT_POSITION, getIntent().getExtras().getInt(EXTRA_POSITION)); setResult(RESULT_OK, returnIntent); editCommentActivityViewModel.deleteCommentDraft(mFullName, () -> { finish(); return Unit.INSTANCE; }); }); } else { handler.post(() -> { isSubmitting = false; Snackbar.make(binding.coordinatorLayoutEditCommentActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); }); } } catch (IOException e) { e.printStackTrace(); handler.post(() -> { isSubmitting = false; Snackbar.make(binding.coordinatorLayoutEditCommentActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); }); } catch (JSONException e) { e.printStackTrace(); handler.post(() -> { isSubmitting = false; Toast.makeText(EditCommentActivity.this, R.string.edit_success, Toast.LENGTH_SHORT).show(); Intent returnIntent = new Intent(); returnIntent.putExtra(RETURN_EXTRA_EDITED_COMMENT_CONTENT, Utils.modifyMarkdown(content)); returnIntent.putExtra(RETURN_EXTRA_EDITED_COMMENT_POSITION, getIntent().getExtras().getInt(EXTRA_POSITION)); setResult(RESULT_OK, returnIntent); editCommentActivityViewModel.deleteCommentDraft(mFullName, () -> { finish(); return Unit.INSTANCE; }); }); } }); } } private void promptAlertDialog(int titleResId, int messageResId, boolean canSaveDraft) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.yes, (dialogInterface, i) -> { if (canSaveDraft) { editCommentActivityViewModel.saveCommentDraft(mFullName, binding.commentEditTextEditCommentActivity.getText().toString(), () -> { finish(); return Unit.INSTANCE; }); } else { finish(); } }) .setNegativeButton(R.string.no, (dialog, which) -> { if (canSaveDraft) { finish(); } }) .setNeutralButton(R.string.cancel, null) .show(); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == PICK_IMAGE_REQUEST_CODE) { if (data == null) { Toast.makeText(EditCommentActivity.this, R.string.error_getting_image, Toast.LENGTH_LONG).show(); return; } Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, mAccessToken, binding.commentEditTextEditCommentActivity, binding.coordinatorLayoutEditCommentActivity, data.getData(), uploadedImages); } else if (requestCode == CAPTURE_IMAGE_REQUEST_CODE) { Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, mAccessToken, binding.commentEditTextEditCommentActivity, binding.coordinatorLayoutEditCommentActivity, capturedImageUri, uploadedImages); } else if (requestCode == MARKDOWN_PREVIEW_REQUEST_CODE) { editComment(); } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(UPLOADED_IMAGES_STATE, uploadedImages); outState.putParcelable(GIPHY_GIF_STATE, giphyGif); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Override public void uploadImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getResources().getString(R.string.select_from_gallery)), PICK_IMAGE_REQUEST_CODE); } @Override public void captureImage() { Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { capturedImageUri = FileProvider.getUriForFile(this, getPackageName() + ".provider", File.createTempFile("captured_image", ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES))); pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); startActivityForResult(pictureIntent, CAPTURE_IMAGE_REQUEST_CODE); } catch (IOException ex) { Toast.makeText(this, R.string.error_creating_temp_file, Toast.LENGTH_SHORT).show(); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_camera_available, Toast.LENGTH_SHORT).show(); } } @Override public void insertImageUrl(UploadedImage uploadedImage) { int start = Math.max(binding.commentEditTextEditCommentActivity.getSelectionStart(), 0); int end = Math.max(binding.commentEditTextEditCommentActivity.getSelectionEnd(), 0); int realStart = Math.min(start, end); if (realStart > 0 && binding.commentEditTextEditCommentActivity.getText().toString().charAt(realStart - 1) != '\n') { binding.commentEditTextEditCommentActivity.getText().replace(realStart, Math.max(start, end), "\n![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "\n![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } else { binding.commentEditTextEditCommentActivity.getText().replace(realStart, Math.max(start, end), "![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } } @Override public void didSearchTerm(@NonNull String s) { } @Override public void onGifSelected(@NonNull Media media, @Nullable String s, @NonNull GPHContentType gphContentType) { this.giphyGif = new GiphyGif(media.getId(), true); int start = Math.max(binding.commentEditTextEditCommentActivity.getSelectionStart(), 0); int end = Math.max(binding.commentEditTextEditCommentActivity.getSelectionEnd(), 0); int realStart = Math.min(start, end); if (realStart > 0 && binding.commentEditTextEditCommentActivity.getText().toString().charAt(realStart - 1) != '\n') { binding.commentEditTextEditCommentActivity.getText().replace(realStart, Math.max(start, end), "\n![gif](" + giphyGif.id + ")\n", 0, "\n![gif]()\n".length() + giphyGif.id.length()); } else { binding.commentEditTextEditCommentActivity.getText().replace(realStart, Math.max(start, end), "![gif](" + giphyGif.id + ")\n", 0, "![gif]()\n".length() + giphyGif.id.length()); } } @Override public void onDismissed(@NonNull GPHContentType gphContentType) { } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/EditMultiRedditActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.inputmethod.EditorInfoCompat; import com.google.android.material.snackbar.Snackbar; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityEditMultiRedditBinding; import ml.docilealligator.infinityforreddit.multireddit.EditMultiReddit; import ml.docilealligator.infinityforreddit.multireddit.FetchMultiRedditInfo; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.multireddit.MultiRedditJSONModel; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class EditMultiRedditActivity extends BaseActivity { public static final String EXTRA_MULTI_PATH = "EMP"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 1; private static final String MULTI_REDDIT_STATE = "MRS"; private static final String MULTI_PATH_STATE = "MPS"; @Inject @Named("oauth") Retrofit mRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private MultiReddit multiReddit; private String multipath; private ActivityEditMultiRedditBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityEditMultiRedditBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutEditMultiRedditActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarEditMultiRedditActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.nestedScrollViewEditMultiRedditActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarEditMultiRedditActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.visibilityWrapperLinearLayoutEditMultiRedditActivity.setVisibility(View.GONE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binding.multiRedditNameEditTextEditMultiRedditActivity.setImeOptions(binding.multiRedditNameEditTextEditMultiRedditActivity.getImeOptions() | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING); binding.descriptionEditTextEditMultiRedditActivity.setImeOptions(binding.descriptionEditTextEditMultiRedditActivity.getImeOptions() | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING); } } if (savedInstanceState != null) { multiReddit = savedInstanceState.getParcelable(MULTI_REDDIT_STATE); multipath = savedInstanceState.getString(MULTI_PATH_STATE); } else { multipath = getIntent().getStringExtra(EXTRA_MULTI_PATH); } bindView(); } private void bindView() { if (multiReddit == null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { FetchMultiRedditInfo.anonymousFetchMultiRedditInfo(mExecutor, new Handler(), mRedditDataRoomDatabase, multipath, new FetchMultiRedditInfo.FetchMultiRedditInfoListener() { @Override public void success(MultiReddit multiReddit) { EditMultiRedditActivity.this.multiReddit = multiReddit; binding.progressBarEditMultiRedditActivity.setVisibility(View.GONE); binding.linearLayoutEditMultiRedditActivity.setVisibility(View.VISIBLE); binding.multiRedditNameEditTextEditMultiRedditActivity.setText(multiReddit.getDisplayName()); binding.descriptionEditTextEditMultiRedditActivity.setText(multiReddit.getDescription()); } @Override public void failed() { //Will not be called } }); } else { FetchMultiRedditInfo.fetchMultiRedditInfo(mExecutor, mHandler, mRetrofit, accessToken, multipath, new FetchMultiRedditInfo.FetchMultiRedditInfoListener() { @Override public void success(MultiReddit multiReddit) { EditMultiRedditActivity.this.multiReddit = multiReddit; binding.progressBarEditMultiRedditActivity.setVisibility(View.GONE); binding.linearLayoutEditMultiRedditActivity.setVisibility(View.VISIBLE); binding.multiRedditNameEditTextEditMultiRedditActivity.setText(multiReddit.getDisplayName()); binding.descriptionEditTextEditMultiRedditActivity.setText(multiReddit.getDescription()); binding.visibilitySwitchEditMultiRedditActivity.setChecked(!multiReddit.getVisibility().equals("public")); } @Override public void failed() { Snackbar.make(binding.coordinatorLayoutEditMultiRedditActivity, R.string.cannot_fetch_multireddit, Snackbar.LENGTH_SHORT).show(); } }); } } else { binding.progressBarEditMultiRedditActivity.setVisibility(View.GONE); binding.linearLayoutEditMultiRedditActivity.setVisibility(View.VISIBLE); binding.multiRedditNameEditTextEditMultiRedditActivity.setText(multiReddit.getDisplayName()); binding.descriptionEditTextEditMultiRedditActivity.setText(multiReddit.getDescription()); binding.visibilitySwitchEditMultiRedditActivity.setChecked(!multiReddit.getVisibility().equals("public")); } binding.selectSubredditTextViewEditMultiRedditActivity.setOnClickListener(view -> { Intent intent = new Intent(EditMultiRedditActivity.this, SelectedSubredditsAndUsersActivity.class); if (multiReddit.getSubreddits() != null) { intent.putParcelableArrayListExtra(SelectedSubredditsAndUsersActivity.EXTRA_SELECTED_SUBREDDITS, multiReddit.getSubreddits()); } startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.edit_multi_reddit_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_save_edit_multi_reddit_activity) { if (binding.multiRedditNameEditTextEditMultiRedditActivity.getText() == null || binding.multiRedditNameEditTextEditMultiRedditActivity.getText().toString().equals("")) { Snackbar.make(binding.coordinatorLayoutEditMultiRedditActivity, R.string.no_multi_reddit_name, Snackbar.LENGTH_SHORT).show(); return true; } if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { String name = binding.multiRedditNameEditTextEditMultiRedditActivity.getText().toString(); multiReddit.setDisplayName(name); multiReddit.setName(name); multiReddit.setDescription(binding.descriptionEditTextEditMultiRedditActivity.getText().toString()); EditMultiReddit.anonymousEditMultiReddit(mExecutor, new Handler(), mRedditDataRoomDatabase, multiReddit, new EditMultiReddit.EditMultiRedditListener() { @Override public void success() { finish(); } @Override public void failed() { //Will not be called } }); } else { String jsonModel = new MultiRedditJSONModel(binding.multiRedditNameEditTextEditMultiRedditActivity.getText().toString(), binding.descriptionEditTextEditMultiRedditActivity.getText().toString(), binding.visibilitySwitchEditMultiRedditActivity.isChecked(), multiReddit.getSubreddits()).createJSONModel(); EditMultiReddit.editMultiReddit(mRetrofit, accessToken, multiReddit.getPath(), jsonModel, new EditMultiReddit.EditMultiRedditListener() { @Override public void success() { finish(); } @Override public void failed() { Snackbar.make(binding.coordinatorLayoutEditMultiRedditActivity, R.string.edit_multi_reddit_failed, Snackbar.LENGTH_SHORT).show(); } }); } return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE && resultCode == RESULT_OK) { if (data != null) { multiReddit.setSubreddits(data.getParcelableArrayListExtra( SelectedSubredditsAndUsersActivity.EXTRA_RETURN_SELECTED_SUBREDDITS)); } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(MULTI_REDDIT_STATE, multiReddit); outState.putString(MULTI_PATH_STATE, multipath); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutEditMultiRedditActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutEditMultiRedditActivity, binding.collapsingToolbarLayoutEditMultiRedditActivity, binding.toolbarEditMultiRedditActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutEditMultiRedditActivity); binding.progressBarEditMultiRedditActivity.setIndicatorColor(mCustomThemeWrapper.getColorAccent()); int primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.multiRedditNameEditTextEditMultiRedditActivity.setTextColor(primaryTextColor); binding.multiRedditNameEditTextEditMultiRedditActivity.setHintTextColor(secondaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1EditMultiRedditActivity.setBackgroundColor(dividerColor); binding.divider2EditMultiRedditActivity.setBackgroundColor(dividerColor); binding.descriptionEditTextEditMultiRedditActivity.setTextColor(primaryTextColor); binding.descriptionEditTextEditMultiRedditActivity.setHintTextColor(secondaryTextColor); binding.visibilityTextViewEditMultiRedditActivity.setTextColor(primaryTextColor); binding.selectSubredditTextViewEditMultiRedditActivity.setTextColor(primaryTextColor); if (typeface != null) { Utils.setFontToAllTextViews(binding.coordinatorLayoutEditMultiRedditActivity, typeface); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/EditPostActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UploadedImagesBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityEditPostBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class EditPostActivity extends BaseActivity implements UploadImageEnabledActivity { public static final String EXTRA_TITLE = "ET"; public static final String EXTRA_CONTENT = "EC"; public static final String EXTRA_FULLNAME = "EF"; private static final int PICK_IMAGE_REQUEST_CODE = 100; private static final int CAPTURE_IMAGE_REQUEST_CODE = 200; private static final int MARKDOWN_PREVIEW_REQUEST_CODE = 300; private static final String UPLOADED_IMAGES_STATE = "UIS"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String mFullName; private String mAccessToken; private String mPostContent; private boolean isSubmitting = false; private Uri capturedImageUri; private ArrayList uploadedImages = new ArrayList<>(); private ActivityEditPostBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityEditPostBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutEditPostActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarEditPostActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutEditPostActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarEditPostActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mFullName = getIntent().getStringExtra(EXTRA_FULLNAME); mAccessToken = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCESS_TOKEN, null); binding.postTitleTextViewEditPostActivity.setText(getIntent().getStringExtra(EXTRA_TITLE)); mPostContent = getIntent().getStringExtra(EXTRA_CONTENT); binding.postContentEditTextEditPostActivity.setText(mPostContent); if (savedInstanceState != null) { uploadedImages = savedInstanceState.getParcelableArrayList(UPLOADED_IMAGES_STATE); } MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( EditPostActivity.this, binding.postContentEditTextEditPostActivity, item); } @Override public void onUploadImage() { Utils.hideKeyboard(EditPostActivity.this); UploadedImagesBottomSheetFragment fragment = new UploadedImagesBottomSheetFragment(); Bundle arguments = new Bundle(); arguments.putParcelableArrayList(UploadedImagesBottomSheetFragment.EXTRA_UPLOADED_IMAGES, uploadedImages); fragment.setArguments(arguments); fragment.show(getSupportFragmentManager(), fragment.getTag()); } }); binding.markdownBottomBarRecyclerViewEditPostActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManagerBugFixed.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewEditPostActivity.setAdapter(adapter); binding.postContentEditTextEditPostActivity.requestFocus(); Utils.showKeyboard(this, new Handler(), binding.postContentEditTextEditPostActivity); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isSubmitting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_edit_post_detail); } else { if (binding.postContentEditTextEditPostActivity.getText().toString().equals(mPostContent)) { setEnabled(false); triggerBackPress(); } else { promptAlertDialog(R.string.discard, R.string.discard_detail); } } } }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutEditPostActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutEditPostActivity, null, binding.toolbarEditPostActivity); binding.postTitleTextViewEditPostActivity.setTextColor(mCustomThemeWrapper.getPostTitleColor()); binding.dividerEditPostActivity.setBackgroundColor(mCustomThemeWrapper.getPostTitleColor()); binding.postContentEditTextEditPostActivity.setTextColor(mCustomThemeWrapper.getPostContentColor()); if (titleTypeface != null) { binding.postTitleTextViewEditPostActivity.setTypeface(titleTypeface); } if (contentTypeface != null) { binding.postContentEditTextEditPostActivity.setTypeface(contentTypeface); } } @Override protected void onPause() { super.onPause(); Utils.hideKeyboard(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.edit_post_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_preview_edit_post_activity) { Intent intent = new Intent(this, FullMarkdownActivity.class); intent.putExtra(FullMarkdownActivity.EXTRA_MARKDOWN, binding.postContentEditTextEditPostActivity.getText().toString()); intent.putExtra(FullMarkdownActivity.EXTRA_SUBMIT_POST, true); startActivityForResult(intent, MARKDOWN_PREVIEW_REQUEST_CODE); } else if (item.getItemId() == R.id.action_send_edit_post_activity) { editPost(); return true; } else if (item.getItemId() == android.R.id.home) { triggerBackPress(); return true; } return false; } private void editPost() { if (!isSubmitting) { isSubmitting = true; Snackbar.make(binding.coordinatorLayoutEditPostActivity, R.string.posting, Snackbar.LENGTH_SHORT).show(); Map params = new HashMap<>(); params.put(APIUtils.THING_ID_KEY, mFullName); params.put(APIUtils.TEXT_KEY, binding.postContentEditTextEditPostActivity.getText().toString()); mOauthRetrofit.create(RedditAPI.class) .editPostOrComment(APIUtils.getOAuthHeader(mAccessToken), params) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { isSubmitting = false; Toast.makeText(EditPostActivity.this, R.string.edit_success, Toast.LENGTH_SHORT).show(); Intent returnIntent = new Intent(); setResult(RESULT_OK, returnIntent); finish(); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { isSubmitting = false; Snackbar.make(binding.coordinatorLayoutEditPostActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } }); } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == PICK_IMAGE_REQUEST_CODE) { if (data == null) { Toast.makeText(EditPostActivity.this, R.string.error_getting_image, Toast.LENGTH_LONG).show(); return; } Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, mAccessToken, binding.postContentEditTextEditPostActivity, binding.coordinatorLayoutEditPostActivity, data.getData(), uploadedImages); } else if (requestCode == CAPTURE_IMAGE_REQUEST_CODE) { Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, mAccessToken, binding.postContentEditTextEditPostActivity, binding.coordinatorLayoutEditPostActivity, capturedImageUri, uploadedImages); } else if (requestCode == MARKDOWN_PREVIEW_REQUEST_CODE) { editPost(); } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(UPLOADED_IMAGES_STATE, uploadedImages); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Override public void uploadImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getResources().getString(R.string.select_from_gallery)), PICK_IMAGE_REQUEST_CODE); } @Override public void captureImage() { Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { capturedImageUri = FileProvider.getUriForFile(this, getPackageName() + ".provider", File.createTempFile("captured_image", ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES))); pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); startActivityForResult(pictureIntent, CAPTURE_IMAGE_REQUEST_CODE); } catch (IOException ex) { Toast.makeText(this, R.string.error_creating_temp_file, Toast.LENGTH_SHORT).show(); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_camera_available, Toast.LENGTH_SHORT).show(); } } @Override public void insertImageUrl(UploadedImage uploadedImage) { int start = Math.max(binding.postContentEditTextEditPostActivity.getSelectionStart(), 0); int end = Math.max(binding.postContentEditTextEditPostActivity.getSelectionEnd(), 0); binding.postContentEditTextEditPostActivity.getText().replace(Math.min(start, end), Math.max(start, end), "[" + uploadedImage.imageName + "](" + uploadedImage.imageUrlOrKey + ")", 0, "[]()".length() + uploadedImage.imageName.length() + uploadedImage.imageUrlOrKey.length()); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/EditProfileActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.PersistableBundle; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout.LayoutParams; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityEditProfileBinding; import ml.docilealligator.infinityforreddit.events.SubmitChangeAvatarEvent; import ml.docilealligator.infinityforreddit.events.SubmitChangeBannerEvent; import ml.docilealligator.infinityforreddit.events.SubmitSaveProfileEvent; import ml.docilealligator.infinityforreddit.services.EditProfileService; import ml.docilealligator.infinityforreddit.user.UserViewModel; import ml.docilealligator.infinityforreddit.utils.EditProfileUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class EditProfileActivity extends BaseActivity { private static final int PICK_IMAGE_BANNER_REQUEST_CODE = 0x401; private static final int PICK_IMAGE_AVATAR_REQUEST_CODE = 0x402; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject CustomThemeWrapper mCustomThemeWrapper; private ActivityEditProfileBinding binding; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityEditProfileBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutViewEditProfileActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarViewEditProfileActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.nestedScrollViewEditProfileActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarViewEditProfileActivity); binding.imageViewChangeBannerEditProfileActivity.setOnClickListener(view -> { startPickImage(PICK_IMAGE_BANNER_REQUEST_CODE); }); binding.imageViewChangeAvatarEditProfileActivity.setOnClickListener(view -> { startPickImage(PICK_IMAGE_AVATAR_REQUEST_CODE); }); final RequestManager glide = Glide.with(this); final UserViewModel.Factory userViewModelFactory = new UserViewModel.Factory(mRedditDataRoomDatabase, accountName); final UserViewModel userViewModel = new ViewModelProvider(this, userViewModelFactory).get(UserViewModel.class); userViewModel.getUserLiveData().observe(this, userData -> { if (userData == null) { return; } // BANNER final String userBanner = userData.getBanner(); LayoutParams cBannerLp = (LayoutParams) binding.imageViewChangeBannerEditProfileActivity.getLayoutParams(); if (userBanner == null || userBanner.isEmpty()) { binding.imageViewChangeBannerEditProfileActivity.setLongClickable(false); cBannerLp.gravity = Gravity.CENTER; binding.imageViewChangeBannerEditProfileActivity.setLayoutParams(cBannerLp); binding.imageViewChangeBannerEditProfileActivity.setOnLongClickListener(v -> false); } else { binding.imageViewChangeBannerEditProfileActivity.setLongClickable(true); cBannerLp.gravity = Gravity.END | Gravity.BOTTOM; binding.imageViewChangeBannerEditProfileActivity.setLayoutParams(cBannerLp); glide.load(userBanner).into(binding.imageViewBannerEditProfileActivity); binding.imageViewChangeBannerEditProfileActivity.setOnLongClickListener(view -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { return false; } new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.remove_banner) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> EditProfileUtils.deleteBanner(mOauthRetrofit, accessToken, accountName, new EditProfileUtils.EditProfileUtilsListener() { @Override public void success() { Toast.makeText(EditProfileActivity.this, R.string.message_remove_banner_success, Toast.LENGTH_SHORT).show(); binding.imageViewBannerEditProfileActivity.setImageDrawable(null);// } @Override public void failed(String message) { Toast.makeText(EditProfileActivity.this, getString(R.string.message_remove_banner_failed, message), Toast.LENGTH_SHORT).show(); } })) .setNegativeButton(R.string.no, null) .show(); return true; }); } // AVATAR final String userAvatar = userData.getIconUrl(); glide.load(userAvatar) .transform(new RoundedCornersTransformation(216, 0)) .into(binding.imageViewAvatarEditProfileActivity); LayoutParams cAvatarLp = (LayoutParams) binding.imageViewChangeAvatarEditProfileActivity.getLayoutParams(); if (userAvatar.contains("avatar_default_")) { binding.imageViewChangeAvatarEditProfileActivity.setLongClickable(false); binding.imageViewChangeAvatarEditProfileActivity.setOnLongClickListener(v -> false); } else { binding.imageViewChangeAvatarEditProfileActivity.setLongClickable(true); binding.imageViewChangeAvatarEditProfileActivity.setOnLongClickListener(view -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { return false; } new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.remove_avatar) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> EditProfileUtils.deleteAvatar(mOauthRetrofit, accessToken, accountName, new EditProfileUtils.EditProfileUtilsListener() { @Override public void success() { Toast.makeText(EditProfileActivity.this, R.string.message_remove_avatar_success, Toast.LENGTH_SHORT).show();// } @Override public void failed(String message) { Toast.makeText(EditProfileActivity.this, getString(R.string.message_remove_avatar_failed, message), Toast.LENGTH_SHORT).show(); } })) .setNegativeButton(R.string.no, null) .show(); return true; }); } binding.editTextAboutYouEditProfileActivity.setText(userData.getDescription()); binding.editTextDisplayNameEditProfileActivity.setText(userData.getTitle()); }); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != RESULT_OK || data == null || accountName.equals(Account.ANONYMOUS_ACCOUNT)) { return; } /*Intent intent = new Intent(this, EditProfileService.class); intent.setData(data.getData()); intent.putExtra(EditProfileService.EXTRA_ACCOUNT_NAME, accountName); intent.putExtra(EditProfileService.EXTRA_ACCESS_TOKEN, accessToken); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); switch (requestCode) { case PICK_IMAGE_BANNER_REQUEST_CODE: intent.putExtra(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_CHANGE_BANNER); ContextCompat.startForegroundService(this, intent); break; case PICK_IMAGE_AVATAR_REQUEST_CODE: intent.putExtra(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_CHANGE_AVATAR); ContextCompat.startForegroundService(this, intent); break; default: break; }*/ int contentEstimatedBytes = 0; PersistableBundle extras = new PersistableBundle(); extras.putString(EditProfileService.EXTRA_MEDIA_URI, data.getData().toString()); extras.putString(EditProfileService.EXTRA_ACCOUNT_NAME, accountName); extras.putString(EditProfileService.EXTRA_ACCESS_TOKEN, accessToken); switch (requestCode) { case PICK_IMAGE_BANNER_REQUEST_CODE: { extras.putInt(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_CHANGE_BANNER); //TODO: contentEstimatedBytes JobInfo jobInfo = EditProfileService.constructJobInfo(this, 500000, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); break; } case PICK_IMAGE_AVATAR_REQUEST_CODE: { extras.putInt(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_CHANGE_AVATAR); //TODO: contentEstimatedBytes JobInfo jobInfo = EditProfileService.constructJobInfo(this, 500000, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); break; } default: break; } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.edit_profile_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { final int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_save_edit_profile_activity) { String displayName = null; if (binding.editTextDisplayNameEditProfileActivity.getText() != null) { displayName = binding.editTextDisplayNameEditProfileActivity.getText().toString(); } String aboutYou = null; if (binding.editTextAboutYouEditProfileActivity.getText() != null) { aboutYou = binding.editTextAboutYouEditProfileActivity.getText().toString(); } if (aboutYou == null || displayName == null) return false; // /*Intent intent = new Intent(this, EditProfileService.class); intent.putExtra(EditProfileService.EXTRA_ACCOUNT_NAME, accountName); intent.putExtra(EditProfileService.EXTRA_ACCESS_TOKEN, accessToken); intent.putExtra(EditProfileService.EXTRA_DISPLAY_NAME, displayName); // intent.putExtra(EditProfileService.EXTRA_ABOUT_YOU, aboutYou); // intent.putExtra(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_SAVE_EDIT_PROFILE); ContextCompat.startForegroundService(this, intent);*/ PersistableBundle extras = new PersistableBundle(); extras.putString(EditProfileService.EXTRA_ACCOUNT_NAME, accountName); extras.putString(EditProfileService.EXTRA_ACCESS_TOKEN, accessToken); extras.putString(EditProfileService.EXTRA_DISPLAY_NAME, displayName); extras.putString(EditProfileService.EXTRA_ABOUT_YOU, aboutYou); extras.putInt(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_SAVE_EDIT_PROFILE); JobInfo jobInfo = EditProfileService.constructJobInfo(this, 1000, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); return true; } return false; } @Subscribe public void onSubmitChangeAvatar(SubmitChangeAvatarEvent event) { if (event.isSuccess) { Toast.makeText(this, R.string.message_change_avatar_success, Toast.LENGTH_SHORT).show(); } else { String message = getString(R.string.message_change_avatar_failed, event.errorMessage); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } } @Subscribe public void onSubmitChangeBanner(SubmitChangeBannerEvent event) { if (event.isSuccess) { Toast.makeText(this, R.string.message_change_banner_success, Toast.LENGTH_SHORT).show(); } else { String message = getString(R.string.message_change_banner_failed, event.errorMessage); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } } @Subscribe public void onSubmitSaveProfile(SubmitSaveProfileEvent event) { if (event.isSuccess) { Toast.makeText(this, R.string.message_save_profile_success, Toast.LENGTH_SHORT).show(); } else { String message = getString(R.string.message_save_profile_failed, event.errorMessage); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutViewEditProfileActivity, binding.collapsingToolbarLayoutEditProfileActivity, binding.toolbarViewEditProfileActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutEditProfileActivity); binding.rootLayoutViewEditProfileActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); changeColorTextView(binding.contentViewEditProfileActivity, mCustomThemeWrapper.getPrimaryTextColor()); if (typeface != null) { Utils.setFontToAllTextViews(binding.rootLayoutViewEditProfileActivity, typeface); } } private void changeColorTextView(ViewGroup viewGroup, int color) { final int childCount = viewGroup.getChildCount(); for (int i = 0; i < childCount; i++) { View child = viewGroup.getChildAt(i); if (child instanceof ViewGroup) { changeColorTextView((ViewGroup) child, color); } else if (child instanceof TextView) { ((TextView) child).setTextColor(color); } } } private void startPickImage(int requestId) { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult( Intent.createChooser(intent, getString(R.string.select_from_gallery)), requestId); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/FilteredPostsActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FilteredThingFABMoreOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SearchPostSortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTimeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UserThingSortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityFilteredThingBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.FragmentCommunicator; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import ml.docilealligator.infinityforreddit.subreddit.SubredditViewModel; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class FilteredPostsActivity extends BaseActivity implements SortTypeSelectionCallback, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, ActivityToolbarInterface, MarkPostAsReadInterface, FilteredThingFABMoreOptionsBottomSheetFragment.FABOptionSelectionCallback, RecyclerViewContentScrollingInterface { public static final String EXTRA_NAME = "ESN"; public static final String EXTRA_QUERY = "EQ"; public static final String EXTRA_TRENDING_SOURCE = "ETS"; public static final String EXTRA_POST_TYPE_FILTER = "EPTF"; public static final String EXTRA_CONSTRUCTED_POST_FILTER = "ECPF"; public static final String EXTRA_CONTAIN_FLAIR = "ECF"; public static final String EXTRA_POST_TYPE = "EPT"; public static final String EXTRA_USER_WHERE = "EUW"; private static final String FRAGMENT_OUT_STATE = "FOS"; private static final int CUSTOMIZE_POST_FILTER_ACTIVITY_REQUEST_CODE = 1000; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; public SubredditViewModel mSubredditViewModel; private String name; private String userWhere; private int postType; private PostFragment mFragment; private Menu mMenu; private boolean isNsfwSubreddit = false; private ActivityFilteredThingBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityFilteredThingBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutFilteredPostsActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarFilteredPostsActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); setMargins(binding.frameLayoutFilteredPostsActivity, allInsets.left, BaseActivity.IGNORE_MARGIN, allInsets.right, BaseActivity.IGNORE_MARGIN ); setMargins(binding.fabFilteredThingActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, FilteredPostsActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, FilteredPostsActivity.this) + allInsets.bottom); return insets; } }); /*adjustToolbar(binding.toolbarFilteredPostsActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.fabFilteredThingActivity.getLayoutParams(); params.bottomMargin += navBarHeight; binding.fabFilteredThingActivity.setLayoutParams(params); }*/ } } setSupportActionBar(binding.toolbarFilteredPostsActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarFilteredPostsActivity); name = getIntent().getStringExtra(EXTRA_NAME); postType = getIntent().getIntExtra(EXTRA_POST_TYPE, PostPagingSource.TYPE_FRONT_PAGE); int filter = getIntent().getIntExtra(EXTRA_POST_TYPE_FILTER, -1000); PostFilter postFilter = getIntent().getParcelableExtra(EXTRA_CONSTRUCTED_POST_FILTER); if (postFilter == null) { postFilter = new PostFilter(); switch (filter) { case Post.NSFW_TYPE: postFilter.onlyNSFW = true; break; case Post.TEXT_TYPE: postFilter.containTextType = true; postFilter.containLinkType = false; postFilter.containImageType = false; postFilter.containGifType = false; postFilter.containVideoType = false; postFilter.containGalleryType = false; break; case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: postFilter.containTextType = false; postFilter.containLinkType = true; postFilter.containImageType = false; postFilter.containGifType = false; postFilter.containVideoType = false; postFilter.containGalleryType = false; break; case Post.IMAGE_TYPE: postFilter.containTextType = false; postFilter.containLinkType = false; postFilter.containImageType = true; postFilter.containGifType = false; postFilter.containVideoType = false; postFilter.containGalleryType = false; break; case Post.GIF_TYPE: postFilter.containTextType = false; postFilter.containLinkType = false; postFilter.containImageType = false; postFilter.containGifType = true; postFilter.containVideoType = false; postFilter.containGalleryType = false; break; case Post.VIDEO_TYPE: postFilter.containTextType = false; postFilter.containLinkType = false; postFilter.containImageType = false; postFilter.containGifType = false; postFilter.containVideoType = true; postFilter.containGalleryType = false; break; case Post.GALLERY_TYPE: postFilter.containTextType = false; postFilter.containLinkType = false; postFilter.containImageType = false; postFilter.containGifType = false; postFilter.containVideoType = false; postFilter.containGalleryType = true; break; } String flair = getIntent().getStringExtra(EXTRA_CONTAIN_FLAIR); if (flair != null) { postFilter.containFlairs = flair; } } postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); if (postType == PostPagingSource.TYPE_USER) { userWhere = getIntent().getStringExtra(EXTRA_USER_WHERE); if (userWhere != null && !PostPagingSource.USER_WHERE_SUBMITTED.equals(userWhere) && mMenu != null) { mMenu.findItem(R.id.action_sort_filtered_thing_activity).setVisible(false); } } if (savedInstanceState != null) { mFragment = (PostFragment) getSupportFragmentManager().getFragment(savedInstanceState, FRAGMENT_OUT_STATE); getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout_filtered_posts_activity, mFragment).commit(); bindView(postFilter, false); } else { bindView(postFilter, true); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mFragment != null) { return mFragment.handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutFilteredPostsActivity, binding.collapsingToolbarLayoutFilteredPostsActivity, binding.toolbarFilteredPostsActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutFilteredPostsActivity); applyFABTheme(binding.fabFilteredThingActivity); } private void bindView(PostFilter postFilter, boolean initializeFragment) { switch (postType) { case PostPagingSource.TYPE_FRONT_PAGE: case PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE: getSupportActionBar().setTitle(R.string.home); break; case PostPagingSource.TYPE_SEARCH: getSupportActionBar().setTitle(R.string.search); break; case PostPagingSource.TYPE_SUBREDDIT: if (name.equals("popular") || name.equals("all")) { getSupportActionBar().setTitle(name.substring(0, 1).toUpperCase() + name.substring(1)); } else { String subredditNamePrefixed = "r/" + name; getSupportActionBar().setTitle(subredditNamePrefixed); mSubredditViewModel = new ViewModelProvider(this, new SubredditViewModel.Factory(mRedditDataRoomDatabase, name)) .get(SubredditViewModel.class); mSubredditViewModel.getSubredditLiveData().observe(this, subredditData -> { if (subredditData != null) { isNsfwSubreddit = subredditData.isNSFW(); } }); } break; case PostPagingSource.TYPE_MULTI_REDDIT: case PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: String multiRedditName; if (name.endsWith("/")) { multiRedditName = name.substring(0, name.length() - 1); multiRedditName = multiRedditName.substring(multiRedditName.lastIndexOf("/") + 1); } else { multiRedditName = name.substring(name.lastIndexOf("/") + 1); } getSupportActionBar().setTitle(multiRedditName); break; case PostPagingSource.TYPE_USER: String usernamePrefixed = "u/" + name; getSupportActionBar().setTitle(usernamePrefixed); break; } if (initializeFragment) { mFragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, postType); bundle.putParcelable(PostFragment.EXTRA_FILTER, postFilter); if (postType == PostPagingSource.TYPE_USER) { bundle.putString(PostFragment.EXTRA_USER_NAME, name); bundle.putString(PostFragment.EXTRA_USER_WHERE, userWhere); } else if (postType == PostPagingSource.TYPE_SUBREDDIT || postType == PostPagingSource.TYPE_MULTI_REDDIT || postType == PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT) { bundle.putString(PostFragment.EXTRA_NAME, name); } else if (postType == PostPagingSource.TYPE_SEARCH) { bundle.putString(PostFragment.EXTRA_NAME, name); bundle.putString(PostFragment.EXTRA_QUERY, getIntent().getStringExtra(EXTRA_QUERY)); bundle.putString(PostFragment.EXTRA_TRENDING_SOURCE, getIntent().getStringExtra(EXTRA_TRENDING_SOURCE)); } mFragment.setArguments(bundle); getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout_filtered_posts_activity, mFragment).commit(); } binding.fabFilteredThingActivity.setOnClickListener(view -> { Intent intent = new Intent(this, CustomizePostFilterActivity.class); if (mFragment != null) { intent.putExtra(CustomizePostFilterActivity.EXTRA_POST_FILTER, mFragment.getPostFilter()); } startActivityForResult(intent, CUSTOMIZE_POST_FILTER_ACTIVITY_REQUEST_CODE); }); if (!accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.fabFilteredThingActivity.setOnLongClickListener(view -> { FilteredThingFABMoreOptionsBottomSheetFragment filteredThingFABMoreOptionsBottomSheetFragment = new FilteredThingFABMoreOptionsBottomSheetFragment(); filteredThingFABMoreOptionsBottomSheetFragment.show(getSupportFragmentManager(), filteredThingFABMoreOptionsBottomSheetFragment.getTag()); return true; }); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.filtered_posts_activity, menu); applyMenuItemTheme(menu); mMenu = menu; if (userWhere != null && !PostPagingSource.USER_WHERE_SUBMITTED.equals(userWhere)) { mMenu.findItem(R.id.action_sort_filtered_thing_activity).setVisible(false); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_sort_filtered_thing_activity) { switch (postType) { case PostPagingSource.TYPE_FRONT_PAGE: SortTypeBottomSheetFragment bestSortTypeBottomSheetFragment = SortTypeBottomSheetFragment.getNewInstance(false, mFragment.getSortType()); bestSortTypeBottomSheetFragment.show(getSupportFragmentManager(), bestSortTypeBottomSheetFragment.getTag()); break; case PostPagingSource.TYPE_SEARCH: SearchPostSortTypeBottomSheetFragment searchPostSortTypeBottomSheetFragment = SearchPostSortTypeBottomSheetFragment.getNewInstance(mFragment.getSortType()); searchPostSortTypeBottomSheetFragment.show(getSupportFragmentManager(), searchPostSortTypeBottomSheetFragment.getTag()); break; case PostPagingSource.TYPE_SUBREDDIT: case PostPagingSource.TYPE_MULTI_REDDIT: case PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: case PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE: SortTypeBottomSheetFragment sortTypeBottomSheetFragment = SortTypeBottomSheetFragment.getNewInstance(true, mFragment.getSortType()); sortTypeBottomSheetFragment.show(getSupportFragmentManager(), sortTypeBottomSheetFragment.getTag()); break; case PostPagingSource.TYPE_USER: UserThingSortTypeBottomSheetFragment userThingSortTypeBottomSheetFragment = UserThingSortTypeBottomSheetFragment.getNewInstance(mFragment.getSortType()); userThingSortTypeBottomSheetFragment.show(getSupportFragmentManager(), userThingSortTypeBottomSheetFragment.getTag()); } return true; } else if (itemId == R.id.action_refresh_filtered_thing_activity) { if (mFragment != null) { ((FragmentCommunicator) mFragment).refresh(); } return true; } else if (itemId == R.id.action_change_post_layout_filtered_post_activity) { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == CUSTOMIZE_POST_FILTER_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) { if (mFragment != null) { mFragment.changePostFilter(data.getParcelableExtra(CustomizePostFilterActivity.RETURN_EXTRA_POST_FILTER)); } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); getSupportFragmentManager().putFragment(outState, FRAGMENT_OUT_STATE, mFragment); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Override public void sortTypeSelected(SortType sortType) { mFragment.changeSortType(sortType); } @Override public void postLayoutSelected(int postLayout) { if (mFragment != null) { switch (postType) { case PostPagingSource.TYPE_FRONT_PAGE: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_FRONT_PAGE_POST, postLayout).apply(); break; case PostPagingSource.TYPE_SUBREDDIT: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_SUBREDDIT_POST_BASE + name, postLayout).apply(); break; case PostPagingSource.TYPE_USER: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE + name, postLayout).apply(); break; case PostPagingSource.TYPE_SEARCH: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_SEARCH_POST, postLayout).apply(); } mFragment.changePostLayout(postLayout); } } @Override public void sortTypeSelected(String sortType) { SortTimeBottomSheetFragment sortTimeBottomSheetFragment = new SortTimeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(SortTimeBottomSheetFragment.EXTRA_SORT_TYPE, sortType); sortTimeBottomSheetFragment.setArguments(bundle); sortTimeBottomSheetFragment.show(getSupportFragmentManager(), sortTimeBottomSheetFragment.getTag()); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Override public void onLongPress() { if (mFragment != null) { mFragment.goBackToTop(); } } @Override public void markPostAsRead(Post post) { int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, accountName, post.getId(), readPostsLimit); } @Override public void fabOptionSelected(int option) { if (option == FilteredThingFABMoreOptionsBottomSheetFragment.FAB_OPTION_FILTER) { Intent intent = new Intent(this, CustomizePostFilterActivity.class); if (mFragment != null) { intent.putExtra(CustomizePostFilterActivity.EXTRA_POST_FILTER, mFragment.getPostFilter()); } startActivityForResult(intent, CUSTOMIZE_POST_FILTER_ACTIVITY_REQUEST_CODE); } else if (option == FilteredThingFABMoreOptionsBottomSheetFragment.FAB_OPTION_HIDE_READ_POSTS) { if (mFragment != null) { mFragment.hideReadPosts(); } } } @Override public void contentScrollUp() { binding.fabFilteredThingActivity.show(); } @Override public void contentScrollDown() { binding.fabFilteredThingActivity.hide(); } public boolean isNsfwSubreddit() { return isNsfwSubreddit; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/FullMarkdownActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.Spanned; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import javax.inject.Inject; import javax.inject.Named; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.customviews.SwipeLockInterface; import ml.docilealligator.infinityforreddit.customviews.SwipeLockLinearLayoutManager; import ml.docilealligator.infinityforreddit.databinding.ActivityCommentFullMarkdownBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.markdown.CustomMarkwonAdapter; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class FullMarkdownActivity extends BaseActivity { public static final String EXTRA_MARKDOWN = "EM"; public static final String EXTRA_IS_NSFW = "EIN"; public static final String EXTRA_SUBMIT_POST = "ESP"; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private ActivityCommentFullMarkdownBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityCommentFullMarkdownBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); setSupportActionBar(binding.toolbarCommentFullMarkdownActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setTitle(" "); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCommentFullMarkdownActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarCommentFullMarkdownActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.contentRecyclerViewCommentFullMarkdownActivity.setPadding( (int) Utils.convertDpToPixel(16, FullMarkdownActivity.this) + allInsets.left, 0, (int) Utils.convertDpToPixel(16, FullMarkdownActivity.this) + allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); /*adjustToolbar(binding.toolbarCommentFullMarkdownActivity); binding.contentRecyclerViewCommentFullMarkdownActivity.setPadding(binding.contentRecyclerViewCommentFullMarkdownActivity.getPaddingLeft(), 0, binding.contentRecyclerViewCommentFullMarkdownActivity.getPaddingRight(), getNavBarHeight());*/ } } String markdown = getIntent().getStringExtra(EXTRA_MARKDOWN); boolean isNsfw = getIntent().getBooleanExtra(EXTRA_IS_NSFW, false); int markdownColor = mCustomThemeWrapper.getCommentColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (typeface != null) { textView.setTypeface(typeface); } textView.setTextColor(markdownColor); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(FullMarkdownActivity.this, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_IS_NSFW, isNsfw); startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(mCustomThemeWrapper.getLinkColor()); } }; Markwon markwon = MarkdownUtils.createContentPreviewRedditMarkwon(this, miscPlugin, markdownColor, markdownColor | 0xFF000000); CustomMarkwonAdapter markwonAdapter = MarkdownUtils.createCustomTablesAdapter(this); LinearLayoutManagerBugFixed linearLayoutManager = new SwipeLockLinearLayoutManager(this, new SwipeLockInterface() { @Override public void lockSwipe() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipe() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } }); binding.contentRecyclerViewCommentFullMarkdownActivity.setLayoutManager(linearLayoutManager); binding.contentRecyclerViewCommentFullMarkdownActivity.setAdapter(markwonAdapter); markwonAdapter.setMarkdown(markwon, markdown); // noinspection NotifyDataSetChanged markwonAdapter.notifyDataSetChanged(); } @Override public boolean onCreateOptionsMenu(Menu menu) { if (getIntent().getBooleanExtra(EXTRA_SUBMIT_POST, false)) { getMenuInflater().inflate(R.menu.full_markdown_activity, menu); applyMenuItemTheme(menu); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_send_full_markdown_activity) { Intent returnIntent = new Intent(); setResult(Activity.RESULT_OK, returnIntent); finish(); return true; } return false; } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCommentFullMarkdownActivity, binding.collapsingToolbarLayoutCommentFullMarkdownActivity, binding.toolbarCommentFullMarkdownActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutCommentFullMarkdownActivity); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/HistoryActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityHistoryBinding; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.CommentsListingFragment; import ml.docilealligator.infinityforreddit.fragments.HistoryPostFragment; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class HistoryActivity extends BaseActivity implements ActivityToolbarInterface, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback { @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private FragmentManager fragmentManager; private SectionsPagerAdapter sectionsPagerAdapter; private ActivityHistoryBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityHistoryBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); mViewPager2 = binding.viewPagerHistoryActivity; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutHistoryActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarHistoryActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.viewPagerHistoryActivity.setPadding(allInsets.left, 0, allInsets.right, 0); return insets; } }); //adjustToolbar(binding.toolbarHistoryActivity); } } setSupportActionBar(binding.toolbarHistoryActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarHistoryActivity); fragmentManager = getSupportFragmentManager(); initializeViewPager(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (sectionsPagerAdapter != null) { return sectionsPagerAdapter.handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutHistoryActivity, binding.collapsingToolbarLayoutHistoryActivity, binding.toolbarHistoryActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutHistoryActivity); applyTabLayoutTheme(binding.tabLayoutTabLayoutHistoryActivityActivity); } private void initializeViewPager() { sectionsPagerAdapter = new SectionsPagerAdapter(this); binding.tabLayoutTabLayoutHistoryActivityActivity.setVisibility(View.GONE); binding.viewPagerHistoryActivity.setAdapter(sectionsPagerAdapter); //viewPager2.setUserInputEnabled(!mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false)); binding.viewPagerHistoryActivity.setUserInputEnabled(false); /*new TabLayoutMediator(tabLayout, viewPager2, (tab, position) -> { switch (position) { case 0: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.posts)); break; } }).attach();*/ binding.viewPagerHistoryActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } } }); fixViewPager2Sensitivity(binding.viewPagerHistoryActivity); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.history_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_refresh_history_activity) { sectionsPagerAdapter.refresh(); return true; } else if (itemId == R.id.action_change_post_layout_history_activity) { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } return false; } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onChangeNSFWEvent(ChangeNSFWEvent changeNSFWEvent) { sectionsPagerAdapter.changeNSFW(changeNSFWEvent.nsfw); } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } @Override public void postLayoutSelected(int postLayout) { if (sectionsPagerAdapter != null) { mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.HISTORY_POST_LAYOUT_READ_POST, postLayout).apply(); sectionsPagerAdapter.changePostLayout(postLayout); } } private class SectionsPagerAdapter extends FragmentStateAdapter { SectionsPagerAdapter(FragmentActivity fa) { super(fa); } @NonNull @Override public Fragment createFragment(int position) { HistoryPostFragment fragment = new HistoryPostFragment(); Bundle bundle = new Bundle(); bundle.putInt(HistoryPostFragment.EXTRA_HISTORY_TYPE, HistoryPostFragment.HISTORY_TYPE_READ_POSTS); fragment.setArguments(bundle); return fragment; } @Nullable private Fragment getCurrentFragment() { if (fragmentManager == null) { return null; } return fragmentManager.findFragmentByTag("f" + binding.viewPagerHistoryActivity.getCurrentItem()); } public boolean handleKeyDown(int keyCode) { if (binding.viewPagerHistoryActivity.getCurrentItem() == 0) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { return ((PostFragment) fragment).handleKeyDown(keyCode); } } return false; } public void refresh() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).refresh(); } else if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).refresh(); } } public void changeNSFW(boolean nsfw) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeNSFW(nsfw); } } public void changePostLayout(int postLayout) { Fragment fragment = getCurrentFragment(); if (fragment instanceof HistoryPostFragment) { ((HistoryPostFragment) fragment).changePostLayout(postLayout); } } public void goBackToTop() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).goBackToTop(); } else if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).goBackToTop(); } } @Override public int getItemCount() { return 1; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/InboxActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.textfield.TextInputEditText; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.AccountManagement; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityInboxBinding; import ml.docilealligator.infinityforreddit.events.ChangeInboxCountEvent; import ml.docilealligator.infinityforreddit.events.PassPrivateMessageEvent; import ml.docilealligator.infinityforreddit.events.PassPrivateMessageIndexEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.InboxFragment; import ml.docilealligator.infinityforreddit.message.FetchMessage; import ml.docilealligator.infinityforreddit.message.Message; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class InboxActivity extends BaseActivity implements ActivityToolbarInterface, RecyclerViewContentScrollingInterface { public static final String EXTRA_NEW_ACCOUNT_NAME = "ENAN"; public static final String EXTRA_VIEW_MESSAGE = "EVM"; private static final String NEW_ACCOUNT_NAME_STATE = "NANS"; private static final int SEARCH_USER_REQUEST_CODE = 1; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private SectionsPagerAdapter sectionsPagerAdapter; private FragmentManager fragmentManager; private String mNewAccountName; private ActivityInboxBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityInboxBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutInboxActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarInboxActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.viewPagerInboxActivity.setPadding( allInsets.left, 0, allInsets.right, 0); setMargins(binding.fabInboxActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, InboxActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, InboxActivity.this) + allInsets.bottom); setMargins(binding.tabLayoutInboxActivity, allInsets.left, BaseActivity.IGNORE_MARGIN, allInsets.right, BaseActivity.IGNORE_MARGIN); return insets; } }); /*adjustToolbar(binding.toolbarInboxActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.fabInboxActivity.getLayoutParams(); params.bottomMargin += navBarHeight; binding.fabInboxActivity.setLayoutParams(params); }*/ } } binding.toolbarInboxActivity.setTitle(R.string.inbox); setSupportActionBar(binding.toolbarInboxActivity); setToolbarGoToTop(binding.toolbarInboxActivity); fragmentManager = getSupportFragmentManager(); if (savedInstanceState != null) { mNewAccountName = savedInstanceState.getString(NEW_ACCOUNT_NAME_STATE); } else { mNewAccountName = getIntent().getStringExtra(EXTRA_NEW_ACCOUNT_NAME); } sectionsPagerAdapter = new SectionsPagerAdapter(this); getCurrentAccountAndFetchMessage(savedInstanceState); binding.viewPagerInboxActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { binding.fabInboxActivity.show(); } }); binding.fabInboxActivity.setOnClickListener(view -> { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); thingEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent pmIntent = new Intent(this, SendPrivateMessageActivity.class); pmIntent.putExtra(SendPrivateMessageActivity.EXTRA_RECIPIENT_USERNAME, thingEditText.getText().toString()); startActivity(pmIntent); return true; } return false; }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.choose_a_user) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent pmIntent = new Intent(this, SendPrivateMessageActivity.class); pmIntent.putExtra(SendPrivateMessageActivity.EXTRA_RECIPIENT_USERNAME, thingEditText.getText().toString()); startActivity(pmIntent); }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.search, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); startActivityForResult(intent, SEARCH_USER_REQUEST_CODE); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutInboxActivity, binding.collapsingToolbarLayoutInboxActivity, binding.toolbarInboxActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutInboxActivity); applyTabLayoutTheme(binding.tabLayoutInboxActivity); applyFABTheme(binding.fabInboxActivity); } private void getCurrentAccountAndFetchMessage(Bundle savedInstanceState) { if (mNewAccountName != null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT) || !accountName.equals(mNewAccountName)) { AccountManagement.switchAccount(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), mNewAccountName, newAccount -> { EventBus.getDefault().post(new SwitchAccountEvent(getClass().getName())); Toast.makeText(this, R.string.account_switched, Toast.LENGTH_SHORT).show(); mNewAccountName = null; if (newAccount != null) { accessToken = newAccount.getAccessToken(); accountName = newAccount.getAccountName(); } bindView(savedInstanceState); }); } else { bindView(savedInstanceState); } } else { bindView(savedInstanceState); } } private void bindView(Bundle savedInstanceState) { binding.viewPagerInboxActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } } }); binding.viewPagerInboxActivity.setAdapter(sectionsPagerAdapter); new TabLayoutMediator(binding.tabLayoutInboxActivity, binding.viewPagerInboxActivity, (tab, position) -> { switch (position) { case 0: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.notifications)); break; case 1: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.messages)); break; } }).attach(); if (savedInstanceState == null && getIntent().getBooleanExtra(EXTRA_VIEW_MESSAGE, false)) { binding.viewPagerInboxActivity.setCurrentItem(1, false); } fixViewPager2Sensitivity(binding.viewPagerInboxActivity); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.inbox_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_refresh_inbox_activity) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } return true; } else if (item.getItemId() == R.id.action_read_all_messages_inbox_activity) { if (!accountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(this, R.string.please_wait, Toast.LENGTH_SHORT).show(); mOauthRetrofit.create(RedditAPI.class).readAllMessages(APIUtils.getOAuthHeader(accessToken)) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { Toast.makeText(InboxActivity.this, R.string.read_all_messages_success, Toast.LENGTH_SHORT).show(); if (sectionsPagerAdapter != null) { sectionsPagerAdapter.readAllMessages(); } EventBus.getDefault().post(new ChangeInboxCountEvent(0)); } else { if (response.code() == 429) { Toast.makeText(InboxActivity.this, R.string.read_all_messages_time_limit, Toast.LENGTH_LONG).show(); } else { Toast.makeText(InboxActivity.this, R.string.read_all_messages_failed, Toast.LENGTH_LONG).show(); } } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { Toast.makeText(InboxActivity.this, R.string.read_all_messages_failed, Toast.LENGTH_LONG).show(); } }); } } else if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && requestCode == SEARCH_USER_REQUEST_CODE && data != null) { String username = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); Intent intent = new Intent(this, SendPrivateMessageActivity.class); intent.putExtra(SendPrivateMessageActivity.EXTRA_RECIPIENT_USERNAME, username); startActivity(intent); } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putString(NEW_ACCOUNT_NAME_STATE, mNewAccountName); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } @Subscribe public void onPassPrivateMessageIndexEvent(PassPrivateMessageIndexEvent event) { if (sectionsPagerAdapter != null) { EventBus.getDefault().post(new PassPrivateMessageEvent(sectionsPagerAdapter.getPrivateMessage(event.privateMessageIndex))); } } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } @Override public void contentScrollUp() { binding.fabInboxActivity.show(); } @Override public void contentScrollDown() { binding.fabInboxActivity.hide(); } private class SectionsPagerAdapter extends FragmentStateAdapter { SectionsPagerAdapter(FragmentActivity fa) { super(fa); } @Nullable private Fragment getCurrentFragment() { if (fragmentManager == null) { return null; } return fragmentManager.findFragmentByTag("f" + binding.viewPagerInboxActivity.getCurrentItem()); } void refresh() { InboxFragment fragment = (InboxFragment) getCurrentFragment(); if (fragment != null) { fragment.refresh(); } } void goBackToTop() { InboxFragment fragment = (InboxFragment) getCurrentFragment(); if (fragment != null) { fragment.goBackToTop(); } } void readAllMessages() { InboxFragment fragment = (InboxFragment) getCurrentFragment(); if (fragment != null) { fragment.markAllMessagesRead(); } } Message getPrivateMessage(int index) { if (fragmentManager == null) { return null; } Fragment fragment = fragmentManager.findFragmentByTag("f" + binding.viewPagerInboxActivity.getCurrentItem()); if (fragment instanceof InboxFragment) { return ((InboxFragment) fragment).getMessageByIndex(index); } return null; } @NonNull @Override public Fragment createFragment(int position) { if (position == 0) { InboxFragment fragment = new InboxFragment(); Bundle bundle = new Bundle(); bundle.putString(InboxFragment.EXTRA_MESSAGE_WHERE, FetchMessage.WHERE_INBOX); fragment.setArguments(bundle); return fragment; } else { InboxFragment fragment = new InboxFragment(); Bundle bundle = new Bundle(); bundle.putString(InboxFragment.EXTRA_MESSAGE_WHERE, FetchMessage.WHERE_MESSAGES); fragment.setArguments(bundle); return fragment; } } @Override public int getItemCount() { return 2; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/LinkResolverActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.webkit.URLUtil; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsService; import org.apache.commons.io.FilenameUtils; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; import retrofit2.Retrofit; public class LinkResolverActivity extends AppCompatActivity { public static final String EXTRA_MESSAGE_FULLNAME = "ENF"; public static final String EXTRA_NEW_ACCOUNT_NAME = "ENAN"; public static final String EXTRA_IS_NSFW = "EIN"; public static final String EXTRA_SUBREDDIT_NAME = "ESN_LRA"; public static final String EXTRA_POST_TITLE_KEY = "ET_LRA"; private static final String POST_PATTERN = "/r/[\\w-]+/comments/\\w+/?\\w+/?"; private static final String POST_PATTERN_2 = "/(u|U|user)/[\\w-]+/comments/\\w+/?\\w+/?"; private static final String POST_PATTERN_3 = "/[\\w-]+$"; private static final String COMMENT_PATTERN = "/(r|u|U|user)/[\\w-]+/comments/\\w+/?[\\w-]+/\\w+/?"; private static final String SUBREDDIT_PATTERN = "/[rR]/[\\w-]+/?"; private static final String USER_PATTERN = "/(u|U|user)/[\\w-]+/?"; private static final String SHARELINK_SUBREDDIT_PATTERN = "/r/[\\w-]+/s/[\\w-]+"; private static final String SHARELINK_USER_PATTERN = "/u/[\\w-]+/s/[\\w-]+"; private static final String SIDEBAR_PATTERN = "/[rR]/[\\w-]+/about/sidebar"; private static final String MULTIREDDIT_PATTERN_2 = "/[rR]/(\\w+\\+?)+/?"; private static final String REDD_IT_POST_PATTERN = "/\\w+/?"; private static final String REDGIFS_PATTERN = "/watch/[\\w-]+$"; private static final String IMGUR_GALLERY_PATTERN = "/gallery/\\w+/?"; private static final String IMGUR_ALBUM_PATTERN = "/(album|a)/\\w+/?"; private static final String IMGUR_IMAGE_PATTERN = "/\\w+/?"; private static final String REDDIT_IMAGE_PATTERN = "^/media$"; private static final String WIKI_PATTERN = "/[rR]/[\\w-]+/(wiki|w)(?:/[\\w-]+)*"; private static final String GOOGLE_AMP_PATTERN = "/amp/s/amp.reddit.com/.*"; private static final String STREAMABLE_PATTERN = "/\\w+/?"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private Uri getRedditUriByPath(String path) { if (path.charAt(0) != '/') { return Uri.parse("https://www.reddit.com/" + path); } else { return Uri.parse("https://www.reddit.com" + path); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((Infinity) getApplication()).getAppComponent().inject(this); Uri uri = getIntent().getData(); if (uri == null) { String url = getIntent().getStringExtra(Intent.EXTRA_TEXT); if (!URLUtil.isValidUrl(url)) { Toast.makeText(this, R.string.invalid_link, Toast.LENGTH_SHORT).show(); finish(); return; } try { uri = Uri.parse(url); } catch (NullPointerException e) { Toast.makeText(this, R.string.invalid_link, Toast.LENGTH_SHORT).show(); finish(); return; } } if (uri.getScheme() == null && uri.getHost() == null) { if (uri.toString().isEmpty()) { Toast.makeText(this, R.string.invalid_link, Toast.LENGTH_SHORT).show(); finish(); return; } handleUri(getRedditUriByPath(uri.toString())); } else { handleUri(uri); } } private void handleUri(Uri uri) { if (uri == null) { Toast.makeText(this, R.string.no_link_available, Toast.LENGTH_SHORT).show(); } else { String path = uri.getPath(); if (path == null) { deepLinkError(uri); } else { if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } if (path.endsWith(".jpg") || path.endsWith(".png") || path.endsWith(".jpeg")) { Intent intent = new Intent(this, ViewImageOrGifActivity.class); String url = uri.toString(); String fileName = FilenameUtils.getName(path); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, url); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, fileName); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, fileName); startActivity(intent); } else if (path.endsWith(".gif")) { Intent intent = new Intent(this, ViewImageOrGifActivity.class); String url = uri.toString(); String fileName = FilenameUtils.getName(path); intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, url); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, fileName); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, fileName); startActivity(intent); } else if (path.endsWith(".mp4")) { Intent intent = new Intent(this, ViewVideoActivity.class); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_DIRECT); intent.putExtra(ViewVideoActivity.EXTRA_IS_NSFW, getIntent().getBooleanExtra(EXTRA_IS_NSFW, false)); intent.setData(uri); startActivity(intent); } else { String messageFullname = getIntent().getStringExtra(EXTRA_MESSAGE_FULLNAME); String newAccountName = getIntent().getStringExtra(EXTRA_NEW_ACCOUNT_NAME); String authority = uri.getAuthority(); List segments = uri.getPathSegments(); if (authority != null) { if (authority.equals("reddit-uploaded-media.s3-accelerate.amazonaws.com")) { String unescapedUrl = uri.toString().replace("%2F", "/"); int lastSlashIndex = unescapedUrl.lastIndexOf("/"); if (lastSlashIndex < 0 || lastSlashIndex == unescapedUrl.length() - 1) { deepLinkError(uri); return; } String id = unescapedUrl.substring(lastSlashIndex + 1); Intent intent = new Intent(this, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, uri.toString()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, id + ".jpg"); startActivity(intent); } else if (authority.equals("v.redd.it")) { Intent intent = new Intent(this, ViewVideoActivity.class); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_V_REDD_IT); intent.putExtra(ViewVideoActivity.EXTRA_V_REDD_IT_URL, uri.toString()); startActivity(intent); } else if (authority.contains("reddit.com") || authority.contains("redd.it") || authority.contains("reddit.app")) { if (authority.equals("reddit.app.link") && path.isEmpty()) { String redirect = uri.getQueryParameter("$og_redirect"); if (redirect != null) { handleUri(Uri.parse(redirect)); } else { deepLinkError(uri); } } else if (path.isEmpty()) { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); } else if (path.equals("/report")) { openInWebView(uri); } else if (path.matches(REDDIT_IMAGE_PATTERN)) { // reddit.com/media, actual image url is stored in the "url" query param try { Intent intent = new Intent(this, ViewImageOrGifActivity.class); String real_url = uri.getQueryParameter("url"); Uri real_uri = Uri.parse(real_url); String fileName = FilenameUtils.getBaseName(real_uri.getPath()); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, real_url); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, fileName); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, fileName); startActivity(intent); } catch (Exception e) { deepLinkError(uri); } } else if(segments.size() == 4 && segments.get(0).equals("user") && segments.get(2).equals("m")) { // Multireddit Intent intent = new Intent(this, ViewMultiRedditDetailActivity.class); intent.putExtra(ViewMultiRedditDetailActivity.EXTRA_MULTIREDDIT_PATH, path); startActivity(intent); } else if (path.matches(POST_PATTERN) || path.matches(POST_PATTERN_2)) { int commentsIndex = segments.lastIndexOf("comments"); if (commentsIndex >= 0 && commentsIndex < segments.size() - 1) { Intent intent = new Intent(this, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_ID, segments.get(commentsIndex + 1)); intent.putExtra(ViewPostDetailActivity.EXTRA_MESSAGE_FULLNAME, messageFullname); intent.putExtra(ViewPostDetailActivity.EXTRA_NEW_ACCOUNT_NAME, newAccountName); startActivity(intent); } else { deepLinkError(uri); } } else if (path.matches(POST_PATTERN_3)) { Intent intent = new Intent(this, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_ID, path.substring(1)); intent.putExtra(ViewPostDetailActivity.EXTRA_MESSAGE_FULLNAME, messageFullname); intent.putExtra(ViewPostDetailActivity.EXTRA_NEW_ACCOUNT_NAME, newAccountName); startActivity(intent); } else if (path.matches(COMMENT_PATTERN)) { int commentsIndex = segments.lastIndexOf("comments"); if (commentsIndex >= 0 && commentsIndex < segments.size() - 1) { Intent intent = new Intent(this, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_ID, segments.get(commentsIndex + 1)); intent.putExtra(ViewPostDetailActivity.EXTRA_SINGLE_COMMENT_ID, segments.get(segments.size() - 1)); intent.putExtra(ViewPostDetailActivity.EXTRA_MESSAGE_FULLNAME, messageFullname); intent.putExtra(ViewPostDetailActivity.EXTRA_NEW_ACCOUNT_NAME, newAccountName); startActivity(intent); } else { deepLinkError(uri); } } else if (path.matches(WIKI_PATTERN)) { String[] pathSegments = path.split("/"); String wikiPage; if (pathSegments.length == 4) { wikiPage = "index"; } else { int lengthThroughWiki = 0; for (int i = 1; i <= 3; ++i) { lengthThroughWiki += pathSegments[i].length() + 1; } wikiPage = path.substring(lengthThroughWiki); } Intent intent = new Intent(this, WikiActivity.class); intent.putExtra(WikiActivity.EXTRA_SUBREDDIT_NAME, segments.get(1)); intent.putExtra(WikiActivity.EXTRA_WIKI_PATH, wikiPage); startActivity(intent); } else if (path.matches(SUBREDDIT_PATTERN)) { Intent intent = new Intent(this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, path.substring(3)); intent.putExtra(ViewSubredditDetailActivity.EXTRA_MESSAGE_FULLNAME, messageFullname); intent.putExtra(ViewSubredditDetailActivity.EXTRA_NEW_ACCOUNT_NAME, newAccountName); startActivity(intent); } else if (path.matches(USER_PATTERN)) { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, segments.get(1)); intent.putExtra(ViewUserDetailActivity.EXTRA_MESSAGE_FULLNAME, messageFullname); intent.putExtra(ViewUserDetailActivity.EXTRA_NEW_ACCOUNT_NAME, newAccountName); startActivity(intent); } else if (path.matches(SIDEBAR_PATTERN)) { Intent intent = new Intent(this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, path.substring(3, path.length() - 14)); intent.putExtra(ViewSubredditDetailActivity.EXTRA_VIEW_SIDEBAR, true); startActivity(intent); } else if (path.matches(MULTIREDDIT_PATTERN_2)) { String subredditName = path.substring(3); Intent intent = new Intent(this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditName); intent.putExtra(ViewSubredditDetailActivity.EXTRA_MESSAGE_FULLNAME, messageFullname); intent.putExtra(ViewSubredditDetailActivity.EXTRA_NEW_ACCOUNT_NAME, newAccountName); startActivity(intent); } else if (authority.equals("redd.it") && path.matches(REDD_IT_POST_PATTERN)) { Intent intent = new Intent(this, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_ID, path.substring(1)); startActivity(intent); } else if (uri.getPath().matches(SHARELINK_SUBREDDIT_PATTERN) || uri.getPath().matches(SHARELINK_USER_PATTERN)) { mRetrofit.callFactory().newCall(new Request.Builder().url(uri.toString()).build()).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { Uri newUri = Uri.parse(response.request().url().toString()); if (newUri.getPath() != null) { if (newUri.getPath().matches(SHARELINK_SUBREDDIT_PATTERN) || newUri.getPath().matches(SHARELINK_USER_PATTERN)) { deepLinkError(newUri); } else { handleUri(newUri); } } else { handleUri(uri); } } else { deepLinkError(uri); } } @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { deepLinkError(uri); } }); } else { deepLinkError(uri); } } else if (authority.equals("click.redditmail.com")) { if (path.startsWith("/CL0/")) { handleUri(Uri.parse(path.substring("/CL0/".length()))); } } else if (authority.contains("redgifs.com")) { if (path.matches(REDGIFS_PATTERN)) { Intent intent = new Intent(this, ViewVideoActivity.class); intent.putExtra(ViewVideoActivity.EXTRA_REDGIFS_ID, path.substring(path.lastIndexOf("/") + 1)); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_REDGIFS); intent.putExtra(ViewVideoActivity.EXTRA_IS_NSFW, true); startActivity(intent); } else { deepLinkError(uri); } } else if (authority.contains("imgur.com")) { if (path.matches(IMGUR_GALLERY_PATTERN)) { Intent intent = new Intent(this, ViewImgurMediaActivity.class); intent.putExtra(ViewImgurMediaActivity.EXTRA_IMGUR_TYPE, ViewImgurMediaActivity.IMGUR_TYPE_GALLERY); intent.putExtra(ViewImgurMediaActivity.EXTRA_IMGUR_ID, segments.get(1)); intent.putExtra(ViewImgurMediaActivity.EXTRA_SUBREDDIT_NAME, getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME)); intent.putExtra(ViewImgurMediaActivity.EXTRA_IS_NSFW, getIntent().getBooleanExtra(EXTRA_IS_NSFW, false)); intent.putExtra(ViewImgurMediaActivity.EXTRA_POST_TITLE_KEY, getIntent().getStringExtra(EXTRA_POST_TITLE_KEY)); startActivity(intent); } else if (path.matches(IMGUR_ALBUM_PATTERN)) { Intent intent = new Intent(this, ViewImgurMediaActivity.class); intent.putExtra(ViewImgurMediaActivity.EXTRA_IMGUR_TYPE, ViewImgurMediaActivity.IMGUR_TYPE_ALBUM); intent.putExtra(ViewImgurMediaActivity.EXTRA_IMGUR_ID, segments.get(1)); intent.putExtra(ViewImgurMediaActivity.EXTRA_SUBREDDIT_NAME, getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME)); intent.putExtra(ViewImgurMediaActivity.EXTRA_IS_NSFW, getIntent().getBooleanExtra(EXTRA_IS_NSFW, false)); intent.putExtra(ViewImgurMediaActivity.EXTRA_POST_TITLE_KEY, getIntent().getStringExtra(EXTRA_POST_TITLE_KEY)); startActivity(intent); } else if (path.matches(IMGUR_IMAGE_PATTERN)) { Intent intent = new Intent(this, ViewImgurMediaActivity.class); intent.putExtra(ViewImgurMediaActivity.EXTRA_IMGUR_TYPE, ViewImgurMediaActivity.IMGUR_TYPE_IMAGE); intent.putExtra(ViewImgurMediaActivity.EXTRA_IMGUR_ID, path.substring(1)); intent.putExtra(ViewImgurMediaActivity.EXTRA_SUBREDDIT_NAME, getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME)); intent.putExtra(ViewImgurMediaActivity.EXTRA_IS_NSFW, getIntent().getBooleanExtra(EXTRA_IS_NSFW, false)); intent.putExtra(ViewImgurMediaActivity.EXTRA_POST_TITLE_KEY, getIntent().getStringExtra(EXTRA_POST_TITLE_KEY)); startActivity(intent); } else if (path.endsWith("gifv") || path.endsWith("mp4")) { String url = uri.toString(); if (path.endsWith("gifv")) { url = url.substring(0, url.length() - 5) + ".mp4"; } Intent intent = new Intent(this, ViewVideoActivity.class); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_IMGUR); intent.putExtra(ViewVideoActivity.EXTRA_IS_NSFW, getIntent().getBooleanExtra(EXTRA_IS_NSFW, false)); intent.setData(Uri.parse(url)); startActivity(intent); } else { deepLinkError(uri); } } else if (authority.contains("google.com")) { if (path.matches(GOOGLE_AMP_PATTERN)) { String url = path.substring(11); handleUri(Uri.parse("https://" + url)); } else { deepLinkError(uri); } } else if (authority.equals("streamable.com")) { if (path.matches(STREAMABLE_PATTERN)) { String shortCode = segments.get(0); Intent intent = new Intent(this, ViewVideoActivity.class); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_STREAMABLE); intent.putExtra(ViewVideoActivity.EXTRA_STREAMABLE_SHORT_CODE, shortCode); startActivity(intent); } else { deepLinkError(uri); } } else { deepLinkError(uri); } } else { deepLinkError(uri); } } } } finish(); } private void deepLinkError(Uri uri) { PackageManager pm = getPackageManager(); String authority = uri.getAuthority(); if(authority != null && (authority.contains("reddit.com") || authority.contains("redd.it") || authority.contains("reddit.app.link"))) { openInCustomTabs(uri, pm, false); return; } int linkHandler = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.LINK_HANDLER, "0")); if (linkHandler == 0) { openInBrowser(uri, pm, true); } else if (linkHandler == 1) { openInCustomTabs(uri, pm, true); } else { openInWebView(uri); } } private void openInBrowser(Uri uri, PackageManager pm, boolean handleError) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(uri); try { startActivity(intent); } catch (ActivityNotFoundException e) { if (handleError) { openInCustomTabs(uri, pm, false); } else { openInWebView(uri); } } } private ArrayList getCustomTabsPackages(PackageManager pm) { // Get default VIEW intent handler. Intent activityIntent = new Intent() .setAction(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) .setData(Uri.fromParts("http", "", null)); // Get all apps that can handle VIEW intents. List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); ArrayList packagesSupportingCustomTabs = new ArrayList<>(); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); // Check if this package also resolves the Custom Tabs service. if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs.add(info); } } return packagesSupportingCustomTabs; } private void openInCustomTabs(Uri uri, PackageManager pm, boolean handleError) { ArrayList resolveInfos = getCustomTabsPackages(pm); if (!resolveInfos.isEmpty()) { CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); // add share action to menu list builder.setShareState(CustomTabsIntent.SHARE_STATE_ON); builder.setDefaultColorSchemeParams( new CustomTabColorSchemeParams.Builder() .setToolbarColor(mCustomThemeWrapper.getColorPrimary()) .build()); CustomTabsIntent customTabsIntent = builder.build(); customTabsIntent.intent.setPackage(resolveInfos.get(0).activityInfo.packageName); if (uri.getScheme() == null) { uri = Uri.parse("http://" + uri); } try { customTabsIntent.launchUrl(this, uri); } catch (ActivityNotFoundException e) { if (handleError) { openInBrowser(uri, pm, false); } else { openInWebView(uri); } } } else { if (handleError) { openInBrowser(uri, pm, false); } else { openInWebView(uri); } } } private void openInWebView(Uri uri) { Intent intent = new Intent(this, WebViewActivity.class); intent.setData(uri); startActivity(intent); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/LockScreenActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import android.content.SharedPreferences; import android.os.Bundle; import android.view.View; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityLockScreenBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class LockScreenActivity extends BaseActivity { @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private ActivityLockScreenBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityLockScreenBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); binding.getRoot().setPadding( allInsets.left, allInsets.top, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } binding.unlockButtonLockScreenActivity.setOnClickListener(view -> { authenticate(); }); authenticate(); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { } }); } private void authenticate() { BiometricManager biometricManager = BiometricManager.from(this); if (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) { Executor executor = ContextCompat.getMainExecutor(this); BiometricPrompt biometricPrompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); finish(); } }); BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.unlock)) .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) .build(); biometricPrompt.authenticate(promptInfo); } else { finish(); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.textViewLockScreenActivity.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); binding.unlockButtonLockScreenActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.unlockButtonLockScreenActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); if (typeface != null) { binding.textViewLockScreenActivity.setTypeface(typeface); binding.unlockButtonLockScreenActivity.setTypeface(typeface); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/LoginActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.InflateException; import android.view.MenuItem; import android.view.View; import android.webkit.CookieManager; import android.webkit.JavascriptInterface; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import org.greenrobot.eventbus.EventBus; import org.json.JSONException; import org.json.JSONObject; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.account.FetchMyInfo; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.FetchMyInfo; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.ParseAndInsertNewAccount; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityLoginBinding; import ml.docilealligator.infinityforreddit.events.NewUserLoggedInEvent; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class LoginActivity extends BaseActivity { @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String authCode; private ActivityLoginBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityLoginBinding.inflate(getLayoutInflater()); try { setContentView(binding.getRoot()); } catch (InflateException ie) { Log.e("LoginActivity", "Failed to inflate LoginActivity: " + ie.getMessage()); Toast.makeText(LoginActivity.this, R.string.no_system_webview_error, Toast.LENGTH_SHORT).show(); finish(); return; } applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutLoginActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarLoginActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutLoginActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); setMargins(binding.fabLoginActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, LoginActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, LoginActivity.this) + allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarLoginActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); binding.webviewLoginActivity.getSettings().setJavaScriptEnabled(true); String userAgent = binding.webviewLoginActivity.getSettings().getUserAgentString(); String chromeUserAgent = userAgent .replace("; wv)", ")") .replace("Version/4.0 ", ""); binding.webviewLoginActivity.getSettings().setUserAgentString(chromeUserAgent); Uri baseUri = Uri.parse(APIUtils.OAUTH_URL); Uri.Builder uriBuilder = baseUri.buildUpon(); uriBuilder.appendQueryParameter(APIUtils.CLIENT_ID_KEY, APIUtils.getClientId(getApplicationContext())); uriBuilder.appendQueryParameter(APIUtils.RESPONSE_TYPE_KEY, APIUtils.RESPONSE_TYPE); uriBuilder.appendQueryParameter(APIUtils.STATE_KEY, APIUtils.STATE); uriBuilder.appendQueryParameter(APIUtils.REDIRECT_URI_KEY, APIUtils.REDIRECT_URI); uriBuilder.appendQueryParameter(APIUtils.DURATION_KEY, APIUtils.DURATION); uriBuilder.appendQueryParameter(APIUtils.SCOPE_KEY, APIUtils.SCOPE); String url = uriBuilder.toString(); binding.internetDisconnectedErrorRetryButtonLoginActivity.setOnClickListener(view -> { recreate(); }); binding.fabLoginActivity.setOnClickListener(view -> { Intent intent = new Intent(this, LoginChromeCustomTabActivity.class); startActivity(intent); finish(); }); CookieManager.getInstance().removeAllCookies(aBoolean -> { }); binding.webviewLoginActivity.addJavascriptInterface(new JsRequestLogger(), "AndroidLogger"); binding.webviewLoginActivity.loadUrl(url); binding.webviewLoginActivity.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.contains("&code=") || url.contains("?code=")) { Uri uri = Uri.parse(url); String state = uri.getQueryParameter("state"); if (state.equals(APIUtils.STATE)) { authCode = uri.getQueryParameter("code"); Map params = new HashMap<>(); params.put(APIUtils.GRANT_TYPE_KEY, "authorization_code"); params.put("code", authCode); params.put(APIUtils.REDIRECT_URI_KEY, APIUtils.REDIRECT_URI); RedditAPI api = mRetrofit.create(RedditAPI.class); Call accessTokenCall = api.getAccessToken(APIUtils.getHttpBasicAuthHeader(getApplicationContext()), params); accessTokenCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { try { String accountResponse = response.body(); if (accountResponse == null) { //Handle error return; } JSONObject responseJSON = new JSONObject(accountResponse); String accessToken = responseJSON.getString(APIUtils.ACCESS_TOKEN_KEY); String refreshToken = responseJSON.getString(APIUtils.REFRESH_TOKEN_KEY); FetchMyInfo.fetchAccountInfo(mExecutor, mHandler, mOauthRetrofit, mRedditDataRoomDatabase, accessToken, new FetchMyInfo.FetchMyInfoListener() { @Override public void onFetchMyInfoSuccess(String name, String profileImageUrl, String bannerImageUrl, int karma, boolean isMod) { mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.ACCESS_TOKEN, accessToken) .putString(SharedPreferencesUtils.ACCOUNT_NAME, name) .putString(SharedPreferencesUtils.ACCOUNT_IMAGE_URL, profileImageUrl).apply(); mCurrentAccountSharedPreferences.edit().remove(SharedPreferencesUtils.SUBSCRIBED_THINGS_SYNC_TIME).apply(); ParseAndInsertNewAccount.parseAndInsertNewAccount(mExecutor, new Handler(), name, accessToken, refreshToken, profileImageUrl, bannerImageUrl, karma, isMod, authCode, mRedditDataRoomDatabase.accountDao(), () -> { EventBus.getDefault().post(new NewUserLoggedInEvent()); finish(); }); } @Override public void onFetchMyInfoFailed(boolean parseFailed) { if (parseFailed) { Toast.makeText(LoginActivity.this, R.string.parse_user_info_error, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(LoginActivity.this, R.string.cannot_fetch_user_info, Toast.LENGTH_SHORT).show(); } finish(); } }); } catch (JSONException e) { e.printStackTrace(); Toast.makeText(LoginActivity.this, R.string.parse_json_response_error, Toast.LENGTH_SHORT).show(); finish(); } } else { Toast.makeText(LoginActivity.this, R.string.retrieve_token_error, Toast.LENGTH_SHORT).show(); finish(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { Toast.makeText(LoginActivity.this, R.string.retrieve_token_error, Toast.LENGTH_SHORT).show(); t.printStackTrace(); finish(); } }); } else { Toast.makeText(LoginActivity.this, R.string.something_went_wrong, Toast.LENGTH_SHORT).show(); finish(); } } else if (url.contains("error=access_denied")) { Toast.makeText(LoginActivity.this, R.string.access_denied, Toast.LENGTH_SHORT).show(); finish(); } else { view.loadUrl(url); } return true; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); view.evaluateJavascript( "(function() {" + " document.addEventListener('submit', function(e) {" + " if (e.submitter && e.submitter.name === 'authorize') {" + " AndroidLogger.log('rewriting authorize from [' + e.submitter.value + '] to [Allow]');" + " e.submitter.value = 'Allow';" + " }" + " }, true);" + "})();", null ); } @Override public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { if (request.isForMainFrame() && !Utils.isConnectedToInternet(LoginActivity.this)) { binding.internetDisconnectedErrorLinearLayoutLoginActivity.setVisibility(View.VISIBLE); } else { super.onReceivedError(view, request, error); } } }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { int backgroundColor = mCustomThemeWrapper.getBackgroundColor(); binding.getRoot().setBackgroundColor(backgroundColor); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutLoginActivity, null, binding.toolbarLoginActivity); int primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.twoFaInfOTextViewLoginActivity.setTextColor(primaryTextColor); Drawable infoDrawable = Utils.getTintedDrawable(this, R.drawable.ic_info_preference_day_night_24dp, mCustomThemeWrapper.getPrimaryIconColor()); binding.twoFaInfOTextViewLoginActivity.setCompoundDrawablesWithIntrinsicBounds(infoDrawable, null, null, null); binding.internetDisconnectedErrorLinearLayoutLoginActivity.setBackgroundColor(backgroundColor); binding.internetDisconnectedErrorTextViewLoginActivity.setTextColor(primaryTextColor); binding.internetDisconnectedErrorRetryButtonLoginActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.internetDisconnectedErrorRetryButtonLoginActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); applyFABTheme(binding.fabLoginActivity); if (typeface != null) { binding.twoFaInfOTextViewLoginActivity.setTypeface(typeface); binding.internetDisconnectedErrorTextViewLoginActivity.setTypeface(typeface); binding.internetDisconnectedErrorRetryButtonLoginActivity.setTypeface(typeface); } } private static class JsRequestLogger { @JavascriptInterface public void log(String message) { Log.d("LoginActivity", "[JS] " + message); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/LoginChromeCustomTabActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsService; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.FetchMyInfo; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.ParseAndInsertNewAccount; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityLoginChromeCustomTabBinding; import ml.docilealligator.infinityforreddit.events.NewUserLoggedInEvent; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class LoginChromeCustomTabActivity extends BaseActivity { @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private ActivityLoginChromeCustomTabBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityLoginChromeCustomTabBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarLoginChromeCustomTabActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarLoginChromeCustomTabActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); openLoginPage(); binding.openWebpageButtonLoginChromeCustomTabActivity.setOnClickListener(view -> { openLoginPage(); }); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Uri uri = intent.getData(); if (uri == null) { binding.openWebpageButtonLoginChromeCustomTabActivity.setVisibility(View.VISIBLE); return; } binding.openWebpageButtonLoginChromeCustomTabActivity.setVisibility(View.GONE); String authCode = uri.getQueryParameter("code"); if (authCode != null) { String state = uri.getQueryParameter("state"); if (APIUtils.STATE.equals(state)) { Map params = new HashMap<>(); params.put(APIUtils.GRANT_TYPE_KEY, "authorization_code"); params.put("code", authCode); params.put(APIUtils.REDIRECT_URI_KEY, APIUtils.REDIRECT_URI); RedditAPI api = mRetrofit.create(RedditAPI.class); Call accessTokenCall = api.getAccessToken(APIUtils.getHttpBasicAuthHeader(getApplicationContext()), params); accessTokenCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { try { String accountResponse = response.body(); if (accountResponse == null) { //Handle error return; } JSONObject responseJSON = new JSONObject(accountResponse); String accessToken = responseJSON.getString(APIUtils.ACCESS_TOKEN_KEY); String refreshToken = responseJSON.getString(APIUtils.REFRESH_TOKEN_KEY); FetchMyInfo.fetchAccountInfo(mExecutor, mHandler, mOauthRetrofit, mRedditDataRoomDatabase, accessToken, new FetchMyInfo.FetchMyInfoListener() { @Override public void onFetchMyInfoSuccess(String name, String profileImageUrl, String bannerImageUrl, int karma, boolean isMod) { mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.ACCESS_TOKEN, accessToken) .putString(SharedPreferencesUtils.ACCOUNT_NAME, name) .putString(SharedPreferencesUtils.ACCOUNT_IMAGE_URL, profileImageUrl).apply(); mCurrentAccountSharedPreferences.edit().remove(SharedPreferencesUtils.SUBSCRIBED_THINGS_SYNC_TIME).apply(); ParseAndInsertNewAccount.parseAndInsertNewAccount(mExecutor, new Handler(), name, accessToken, refreshToken, profileImageUrl, bannerImageUrl, karma, isMod, authCode, mRedditDataRoomDatabase.accountDao(), () -> { EventBus.getDefault().post(new NewUserLoggedInEvent()); finish(); }); } @Override public void onFetchMyInfoFailed(boolean parseFailed) { if (parseFailed) { Toast.makeText(LoginChromeCustomTabActivity.this, R.string.parse_user_info_error, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(LoginChromeCustomTabActivity.this, R.string.cannot_fetch_user_info, Toast.LENGTH_SHORT).show(); } finish(); } }); } catch (JSONException e) { e.printStackTrace(); Toast.makeText(LoginChromeCustomTabActivity.this, R.string.parse_json_response_error, Toast.LENGTH_SHORT).show(); finish(); } } else { Toast.makeText(LoginChromeCustomTabActivity.this, R.string.retrieve_token_error, Toast.LENGTH_SHORT).show(); finish(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { Toast.makeText(LoginChromeCustomTabActivity.this, R.string.retrieve_token_error, Toast.LENGTH_SHORT).show(); t.printStackTrace(); finish(); } }); } else { Toast.makeText(this, R.string.something_went_wrong, Toast.LENGTH_SHORT).show(); finish(); } } else if ("access_denied".equals(uri.getQueryParameter("error"))) { Toast.makeText(this, R.string.access_denied, Toast.LENGTH_SHORT).show(); finish(); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutLoginChromeCustomTabActivity, null, binding.toolbarLoginChromeCustomTabActivity); binding.openWebpageButtonLoginChromeCustomTabActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.openWebpageButtonLoginChromeCustomTabActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); if (typeface != null) { binding.openWebpageButtonLoginChromeCustomTabActivity.setTypeface(typeface); } } private void openLoginPage() { Uri.Builder uriBuilder = Uri.parse(APIUtils.OAUTH_URL).buildUpon(); uriBuilder.appendQueryParameter(APIUtils.CLIENT_ID_KEY, APIUtils.getClientId(getApplicationContext())); uriBuilder.appendQueryParameter(APIUtils.RESPONSE_TYPE_KEY, APIUtils.RESPONSE_TYPE); uriBuilder.appendQueryParameter(APIUtils.STATE_KEY, APIUtils.STATE); uriBuilder.appendQueryParameter(APIUtils.REDIRECT_URI_KEY, APIUtils.REDIRECT_URI); uriBuilder.appendQueryParameter(APIUtils.DURATION_KEY, APIUtils.DURATION); uriBuilder.appendQueryParameter(APIUtils.SCOPE_KEY, APIUtils.SCOPE); Uri loginUri = uriBuilder.build(); ArrayList resolveInfos = getCustomTabsPackages(getPackageManager()); if (!resolveInfos.isEmpty()) { String packageName = resolveInfos.get(0).activityInfo.packageName; if (isFirefoxBrowser(packageName)) { // Firefox Custom Tabs don't handle custom scheme redirects properly. // Use a regular browser intent instead — the full browser will // dispatch continuum://localhost via standard Android intent resolution. Intent browserIntent = new Intent(Intent.ACTION_VIEW, loginUri); browserIntent.setPackage(packageName); try { startActivity(browserIntent); } catch (ActivityNotFoundException e) { Snackbar.make(binding.getRoot(), R.string.custom_tab_not_available, Snackbar.LENGTH_LONG).show(); } } else { CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); // add share action to menu list builder.setShareState(CustomTabsIntent.SHARE_STATE_ON); builder.setDefaultColorSchemeParams( new CustomTabColorSchemeParams.Builder() .setToolbarColor(mCustomThemeWrapper.getColorPrimary()) .build()); CustomTabsIntent customTabsIntent = builder.build(); customTabsIntent.intent.setPackage(packageName); customTabsIntent.intent.putExtra("com.google.android.apps.chrome.EXTRA_OPEN_NEW_INCOGNITO_TAB", true); try { customTabsIntent.launchUrl(this, loginUri); } catch (ActivityNotFoundException e) { Snackbar.make(binding.getRoot(), R.string.custom_tab_not_available, Snackbar.LENGTH_LONG).show(); } } } else { // No Custom Tabs browsers found — try opening in any available browser. Intent browserIntent = new Intent(Intent.ACTION_VIEW, loginUri); try { startActivity(browserIntent); } catch (ActivityNotFoundException e) { Snackbar.make(binding.getRoot(), R.string.custom_tab_not_available, Snackbar.LENGTH_LONG).show(); } } } private boolean isFirefoxBrowser(String packageName) { return packageName != null && ( packageName.startsWith("org.mozilla.firefox") || packageName.startsWith("org.mozilla.fenix") || packageName.equals("org.mozilla.focus") || packageName.equals("org.mozilla.klar") ); } private ArrayList getCustomTabsPackages(PackageManager pm) { // Get default VIEW intent handler. Intent activityIntent = new Intent() .setAction(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) .setData(Uri.fromParts("http", "", null)); // Get all apps that can handle VIEW intents. List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); ArrayList packagesSupportingCustomTabs = new ArrayList<>(); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); // Check if this package also resolves the Custom Tabs service. if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs.add(info); } } return packagesSupportingCustomTabs; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/MainActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatDelegate; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.splashscreen.SplashScreen; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.badge.ExperimentalBadgeUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.textfield.TextInputEditText; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Constants; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.account.AccountViewModel; import ml.docilealligator.infinityforreddit.adapters.SubredditAutocompleteRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.adapters.navigationdrawer.NavigationDrawerRecyclerViewMergedAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.AccountManagement; import ml.docilealligator.infinityforreddit.asynctasks.InsertSubscribedThings; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FABMoreOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTimeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.customviews.NavigationWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityMainBinding; import ml.docilealligator.infinityforreddit.events.ChangeDisableSwipingBetweenTabsEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideFabInPostFeedEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideKarmaEvent; import ml.docilealligator.infinityforreddit.events.ChangeInboxCountEvent; import ml.docilealligator.infinityforreddit.events.ChangeLockBottomAppBarEvent; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.ChangeRequireAuthToAccountSectionEvent; import ml.docilealligator.infinityforreddit.events.ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent; import ml.docilealligator.infinityforreddit.events.NewUserLoggedInEvent; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.message.ReadMessage; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.multireddit.MultiRedditViewModel; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.subreddit.ParseSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditViewModel; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.thing.FetchSubscribedThing; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.user.FetchUserData; import ml.docilealligator.infinityforreddit.user.UserData; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesLiveDataKt; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.worker.PullNotificationWorker; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class MainActivity extends BaseActivity implements SortTypeSelectionCallback, PostTypeBottomSheetFragment.PostTypeSelectionCallback, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, ActivityToolbarInterface, FABMoreOptionsBottomSheetFragment.FABOptionSelectionCallback, MarkPostAsReadInterface, RecyclerViewContentScrollingInterface { static final String EXTRA_MESSAGE_FULLNAME = "ENF"; static final String EXTRA_NEW_ACCOUNT_NAME = "ENAN"; private static final String FETCH_USER_INFO_STATE = "FUIS"; private static final String FETCH_SUBSCRIPTIONS_STATE = "FSS"; private static final String DRAWER_ON_ACCOUNT_SWITCH_STATE = "DOASS"; private static final String MESSAGE_FULLNAME_STATE = "MFS"; private static final String NEW_ACCOUNT_NAME_STATE = "NANS"; private static final String INBOX_COUNT_STATE = "ICS"; MultiRedditViewModel multiRedditViewModel; SubscribedSubredditViewModel subscribedSubredditViewModel; AccountViewModel accountViewModel; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("main_activity_tabs") SharedPreferences mMainActivityTabsSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject @Named("bottom_app_bar") SharedPreferences mBottomAppBarSharedPreference; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("navigation_drawer") SharedPreferences mNavigationDrawerSharedPreferences; @Inject @Named("security") SharedPreferences mSecuritySharedPreferences; @Inject @Named("internal") SharedPreferences mInternalSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private FragmentManager fragmentManager; private SectionsPagerAdapter sectionsPagerAdapter; private NavigationDrawerRecyclerViewMergedAdapter adapter; private NavigationWrapper navigationWrapper; private Runnable autoCompleteRunnable; private Call subredditAutocompleteCall; private boolean mFetchUserInfoSuccess = false; private boolean mFetchSubscriptionsSuccess = false; private boolean mDrawerOnAccountSwitch = false; private String mMessageFullname; private String mNewAccountName; private boolean hideFab; private boolean showBottomAppBar; private int mBackButtonAction; private boolean mLockBottomAppBar; private boolean mDisableSwipingBetweenTabs; private boolean mShowFavoriteMultiReddits; private boolean mShowMultiReddits; private boolean mShowFavoriteSubscribedSubreddits; private boolean mShowSubscribedSubreddits; private int fabOption; private int inboxCount; private ActivityMainBinding binding; @ExperimentalBadgeUtils @Override protected void onCreate(Bundle savedInstanceState) { SplashScreen.installSplashScreen(this); ((Infinity) getApplication()).getAppComponent().inject(this); setTheme(R.style.AppTheme_NoActionBarWithTransparentStatusBar); setHasDrawerLayout(); super.onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); hideFab = mSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_FAB_IN_POST_FEED, false); showBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.BOTTOM_APP_BAR_KEY, false); navigationWrapper = new NavigationWrapper(findViewById(R.id.bottom_app_bar_bottom_app_bar), findViewById(R.id.linear_layout_bottom_app_bar), findViewById(R.id.option_1_bottom_app_bar), findViewById(R.id.option_2_bottom_app_bar), findViewById(R.id.option_3_bottom_app_bar), findViewById(R.id.option_4_bottom_app_bar), findViewById(R.id.fab_main_activity), findViewById(R.id.navigation_rail), customThemeWrapper, showBottomAppBar); EventBus.getDefault().register(this); applyCustomTheme(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.includedAppBar.appbarLayoutMainActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { binding.drawerLayout.setStatusBarBackgroundColor(Color.TRANSPARENT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { binding.drawerLayout.setFitsSystemWindows(false); window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); binding.navigationViewMainActivity.setPadding(allInsets.left, 0, 0, 0); if (navigationWrapper.navigationRailView == null) { if (navigationWrapper.bottomAppBar.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, MainActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, MainActivity.this) + allInsets.bottom); } else { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, allInsets.bottom); } } else { if (navigationWrapper.navigationRailView.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, MainActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, MainActivity.this) + allInsets.bottom); binding.includedAppBar.viewPagerMainActivity.setPadding(allInsets.left, 0, allInsets.right, 0); } else { navigationWrapper.navigationRailView.setFitsSystemWindows(false); navigationWrapper.navigationRailView.setPadding(0, 0, 0, allInsets.bottom); setMargins(navigationWrapper.navigationRailView, allInsets.left, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN ); binding.includedAppBar.viewPagerMainActivity.setPadding(0, 0, allInsets.right, 0); } } if (navigationWrapper.bottomAppBar != null) { navigationWrapper.linearLayoutBottomAppBar.setPadding( navigationWrapper.linearLayoutBottomAppBar.getPaddingLeft(), navigationWrapper.linearLayoutBottomAppBar.getPaddingTop(), navigationWrapper.linearLayoutBottomAppBar.getPaddingRight(), allInsets.bottom ); } setMargins(binding.includedAppBar.toolbar, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); setMargins(binding.includedAppBar.tabLayoutMainActivity, allInsets.left, BaseActivity.IGNORE_MARGIN, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.navDrawerRecyclerViewMainActivity.setPadding(0, 0, 0, allInsets.bottom); return insets; } }); /*adjustToolbar(binding.includedAppBar.toolbar); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { if (navigationWrapper.navigationRailView == null) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); params.bottomMargin += navBarHeight; navigationWrapper.floatingActionButton.setLayoutParams(params); } if (navigationWrapper.bottomAppBar != null) { navigationWrapper.linearLayoutBottomAppBar.setPadding(navigationWrapper.linearLayoutBottomAppBar.getPaddingLeft(), navigationWrapper.linearLayoutBottomAppBar.getPaddingTop(), navigationWrapper.linearLayoutBottomAppBar.getPaddingRight(), navBarHeight); } binding.navDrawerRecyclerViewMainActivity.setPadding(0, 0, 0, navBarHeight); }*/ } else { /*ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets inset = Utils.getInsets(insets, false); setMargins(binding.drawerLayout, inset.left, inset.top, inset.right, inset.bottom); return insets; } });*/ binding.drawerLayout.setStatusBarBackgroundColor(mCustomThemeWrapper.getColorPrimaryDark()); } } setSupportActionBar(binding.includedAppBar.toolbar); setToolbarGoToTop(binding.includedAppBar.toolbar); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( this, binding.drawerLayout, binding.includedAppBar.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); toggle.getDrawerArrowDrawable().setColor(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor()); binding.drawerLayout.addDrawerListener(toggle); binding.drawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { @Override public void onDrawerClosed(View drawerView) { if (adapter != null) { adapter.closeAccountManagement(true); } } }); SharedPreferencesLiveDataKt.stringLiveData(mSharedPreferences, SharedPreferencesUtils.NAVIGATION_DRAWER_SWIPE_AREA, "0").observe(this, swipeArea -> { binding.drawerLayout.setSwipeEdgeSize(Integer.parseInt(swipeArea)); }); toggle.syncState(); mViewPager2 = binding.includedAppBar.viewPagerMainActivity; mBackButtonAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.MAIN_PAGE_BACK_BUTTON_ACTION, "0")); mLockBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_BOTTOM_APP_BAR, false); mDisableSwipingBetweenTabs = mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false); fragmentManager = getSupportFragmentManager(); if (savedInstanceState != null) { mFetchUserInfoSuccess = savedInstanceState.getBoolean(FETCH_USER_INFO_STATE); mFetchSubscriptionsSuccess = savedInstanceState.getBoolean(FETCH_SUBSCRIPTIONS_STATE); mDrawerOnAccountSwitch = savedInstanceState.getBoolean(DRAWER_ON_ACCOUNT_SWITCH_STATE); mMessageFullname = savedInstanceState.getString(MESSAGE_FULLNAME_STATE); mNewAccountName = savedInstanceState.getString(NEW_ACCOUNT_NAME_STATE); inboxCount = savedInstanceState.getInt(INBOX_COUNT_STATE); } else { mMessageFullname = getIntent().getStringExtra(EXTRA_MESSAGE_FULLNAME); mNewAccountName = getIntent().getStringExtra(EXTRA_NEW_ACCOUNT_NAME); } /*if (!mInternalSharedPreferences.getBoolean(SharedPreferencesUtils.DO_NOT_SHOW_REDDIT_API_INFO_V2_AGAIN, false)) { ImportantInfoBottomSheetFragment fragment = new ImportantInfoBottomSheetFragment(); fragment.setCancelable(false); fragment.show(getSupportFragmentManager(), fragment.getTag()); }*/ getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (binding.drawerLayout.isOpen()) { binding.drawerLayout.close(); } else { if (mBackButtonAction == SharedPreferencesUtils.MAIN_PAGE_BACK_BUTTON_ACTION_CONFIRM_EXIT) { new MaterialAlertDialogBuilder(MainActivity.this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.exit_app) .setPositiveButton(R.string.yes, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } else if (mBackButtonAction == SharedPreferencesUtils.MAIN_PAGE_BACK_BUTTON_ACTION_OPEN_NAVIGATION_DRAWER) { binding.drawerLayout.open(); } else { setEnabled(false); triggerBackPress(); } } } }); SharedPreferencesLiveDataKt.booleanLiveData(mSharedPreferences, SharedPreferencesUtils.LOCK_TOOLBAR, false).observe(this, lock -> { AppBarLayout.LayoutParams p = (AppBarLayout.LayoutParams) binding.includedAppBar.collapsingToolbarLayoutMainActivity.getLayoutParams(); p.setScrollFlags(lock ? SCROLL_FLAG_NO_SCROLL : SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS); binding.includedAppBar.collapsingToolbarLayoutMainActivity.setLayoutParams(p); }); initializeNotificationAndBindView(); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } public boolean isDisableSwipingBetweenTabs() { return mDisableSwipingBetweenTabs; } @Override protected void applyCustomTheme() { int backgroundColor = mCustomThemeWrapper.getBackgroundColor(); binding.drawerLayout.setBackgroundColor(backgroundColor); navigationWrapper.applyCustomTheme(mCustomThemeWrapper.getBottomAppBarIconColor(), mCustomThemeWrapper.getBottomAppBarBackgroundColor()); binding.navigationViewMainActivity.setBackgroundColor(backgroundColor); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.includedAppBar.appbarLayoutMainActivity, binding.includedAppBar.collapsingToolbarLayoutMainActivity, binding.includedAppBar.toolbar); applyTabLayoutTheme(binding.includedAppBar.tabLayoutMainActivity); applyFABTheme(navigationWrapper.floatingActionButton); } @ExperimentalBadgeUtils private void initializeNotificationAndBindView() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ActivityResultLauncher requestNotificationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), result -> mInternalSharedPreferences.edit().putBoolean(SharedPreferencesUtils.HAS_REQUESTED_NOTIFICATION_PERMISSION, true).apply()); if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { if (!mInternalSharedPreferences.getBoolean(SharedPreferencesUtils.HAS_REQUESTED_NOTIFICATION_PERMISSION, false)) { requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); } } } boolean enableNotification = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_NOTIFICATION_KEY, true); long notificationInterval = Long.parseLong(mSharedPreferences.getString(SharedPreferencesUtils.NOTIFICATION_INTERVAL_KEY, "1")); TimeUnit timeUnit = (notificationInterval == 15 || notificationInterval == 30) ? TimeUnit.MINUTES : TimeUnit.HOURS; WorkManager workManager = WorkManager.getInstance(this); if (mNewAccountName != null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT) || !accountName.equals(mNewAccountName)) { AccountManagement.switchAccount(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), mNewAccountName, newAccount -> { EventBus.getDefault().post(new SwitchAccountEvent(getClass().getName())); Toast.makeText(this, R.string.account_switched, Toast.LENGTH_SHORT).show(); mNewAccountName = null; if (newAccount != null) { accessToken = newAccount.getAccessToken(); accountName = newAccount.getAccountName(); } setNotification(workManager, notificationInterval, timeUnit, enableNotification); bindView(); }); } else { setNotification(workManager, notificationInterval, timeUnit, enableNotification); bindView(); } } else { setNotification(workManager, notificationInterval, timeUnit, enableNotification); bindView(); } } private void setNotification(WorkManager workManager, long notificationInterval, TimeUnit timeUnit, boolean enableNotification) { if (enableNotification) { Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build(); PeriodicWorkRequest pullNotificationRequest = new PeriodicWorkRequest.Builder(PullNotificationWorker.class, notificationInterval, timeUnit) .setConstraints(constraints) .build(); workManager.enqueueUniquePeriodicWork(PullNotificationWorker.UNIQUE_WORKER_NAME, ExistingPeriodicWorkPolicy.KEEP, pullNotificationRequest); } else { workManager.cancelUniqueWork(PullNotificationWorker.UNIQUE_WORKER_NAME); } } private void bottomAppBarOptionAction(int option) { switch (option) { case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SHOW_MULTIREDDITS, true); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX: { Intent intent = new Intent(this, InboxActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_PROFILE: { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: { PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_REFRESH: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: { changeSortType(); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_UPVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_UPVOTED); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_DOWNVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_DOWNVOTED); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDDEN: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_HIDDEN); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SAVED: { Intent intent = new Intent(MainActivity.this, AccountSavedThingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; } } } private int getBottomAppBarOptionDrawableResource(int option) { switch (option) { case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: return R.drawable.ic_subscriptions_bottom_app_bar_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: return R.drawable.ic_multi_reddit_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX: return R.drawable.ic_inbox_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_PROFILE: return R.drawable.ic_account_circle_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: return R.drawable.ic_add_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_REFRESH: return R.drawable.ic_refresh_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: return R.drawable.ic_sort_toolbar_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: return R.drawable.ic_post_layout_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SEARCH: return R.drawable.ic_search_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: return R.drawable.ic_subreddit_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_USER: return R.drawable.ic_user_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: return R.drawable.ic_hide_read_posts_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: return R.drawable.ic_filter_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_UPVOTED: return R.drawable.ic_arrow_upward_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_DOWNVOTED: return R.drawable.ic_arrow_downward_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDDEN: return R.drawable.ic_lock_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SAVED: return R.drawable.ic_bookmarks_day_night_24dp; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: return R.drawable.ic_keyboard_double_arrow_up_day_night_24dp; } } @ExperimentalBadgeUtils private void bindView() { if (isFinishing() || isDestroyed()) { return; } if (showBottomAppBar) { int optionCount = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_COUNT, 4); int option1 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_1, SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS); int option2 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_2, SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_MULTIREDDITS); if (optionCount == 2) { navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2)); navigationWrapper.bindOptions(option1, option2); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); setBottomAppBarContentDescription(navigationWrapper.option2BottomAppBar, option1); setBottomAppBarContentDescription(navigationWrapper.option4BottomAppBar, option2); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } return false; }); } } else { int option3 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_3, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_REFRESH : SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX); int option4 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_4, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE : SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_PROFILE); navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2), getBottomAppBarOptionDrawableResource(option3), getBottomAppBarOptionDrawableResource(option4)); navigationWrapper.bindOptions(option1, option2, option3, option4); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option1BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); navigationWrapper.option3BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option3); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option4); }); setBottomAppBarContentDescription(navigationWrapper.option1BottomAppBar, option1); setBottomAppBarContentDescription(navigationWrapper.option2BottomAppBar, option2); setBottomAppBarContentDescription(navigationWrapper.option3BottomAppBar, option3); setBottomAppBarContentDescription(navigationWrapper.option4BottomAppBar, option4); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } else if (itemId == R.id.navigation_rail_option_3) { bottomAppBarOptionAction(option3); return true; } else if (itemId == R.id.navigation_rail_option_4) { bottomAppBarOptionAction(option4); return true; } return false; }); } } } else { CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); lp.setAnchorId(View.NO_ID); lp.gravity = Gravity.END | Gravity.BOTTOM; navigationWrapper.floatingActionButton.setLayoutParams(lp); } fabOption = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB, SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS); switch (fabOption) { case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_REFRESH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_refresh_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_refresh)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_sort_toolbar_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_change_sort_type)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_post_layout_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_change_post_layout)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_SEARCH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_search_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_search)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_subreddit_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_go_to_subreddit)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_USER: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_user_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_go_to_user)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_FILTER_POSTS; navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_filter_posts)); } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_hide_read_posts_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_hide_read_posts)); } break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_FILTER_POSTS: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_filter_posts)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_TOP: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_keyboard_double_arrow_up_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_go_to_top)); break; default: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_FILTER_POSTS; navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_filter_posts)); } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_add_day_night_24dp); navigationWrapper.floatingActionButton.setContentDescription(getString(R.string.content_description_submit_post)); } break; } navigationWrapper.floatingActionButton.setOnClickListener(view -> { switch (fabOption) { case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_REFRESH: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: { changeSortType(); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_FILTER_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_TOP: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; default: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } }); navigationWrapper.floatingActionButton.setOnLongClickListener(view -> { FABMoreOptionsBottomSheetFragment fabMoreOptionsBottomSheetFragment= new FABMoreOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(FABMoreOptionsBottomSheetFragment.EXTRA_ANONYMOUS_MODE, accountName.equals(Account.ANONYMOUS_ACCOUNT)); fabMoreOptionsBottomSheetFragment.setArguments(bundle); fabMoreOptionsBottomSheetFragment.show(getSupportFragmentManager(), fabMoreOptionsBottomSheetFragment.getTag()); return true; }); navigationWrapper.floatingActionButton.setVisibility(hideFab ? View.GONE : View.VISIBLE); adapter = new NavigationDrawerRecyclerViewMergedAdapter(this, mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mNavigationDrawerSharedPreferences, mSecuritySharedPreferences, mCustomThemeWrapper, accountName, new NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener() { @Override public void onMenuClick(int stringId) { Intent intent = null; if (stringId == R.string.profile) { intent = new Intent(MainActivity.this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); } else if (stringId == R.string.subscriptions) { intent = new Intent(MainActivity.this, SubscribedThingListingActivity.class); } else if (stringId == R.string.multi_reddit) { intent = new Intent(MainActivity.this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SHOW_MULTIREDDITS, true); } else if (stringId == R.string.history) { intent = new Intent(MainActivity.this, HistoryActivity.class); } else if (stringId == R.string.upvoted) { intent = new Intent(MainActivity.this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_UPVOTED); } else if (stringId == R.string.downvoted) { intent = new Intent(MainActivity.this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_DOWNVOTED); } else if (stringId == R.string.hidden) { intent = new Intent(MainActivity.this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_HIDDEN); } else if (stringId == R.string.account_saved_thing_activity_label) { intent = new Intent(MainActivity.this, AccountSavedThingActivity.class); } else if (stringId == R.string.light_theme) { mSharedPreferences.edit().putString(SharedPreferencesUtils.THEME_KEY, "0").apply(); AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO); mCustomThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.LIGHT); } else if (stringId == R.string.dark_theme) { mSharedPreferences.edit().putString(SharedPreferencesUtils.THEME_KEY, "1").apply(); AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { mCustomThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.AMOLED); } else { mCustomThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.DARK); } } else if (stringId == R.string.enable_nsfw) { String nsfwKey = (accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE; mNsfwAndSpoilerSharedPreferences.edit().putBoolean(nsfwKey, true).apply(); EventBus.getDefault().post(new ChangeNSFWEvent(true)); } else if (stringId == R.string.disable_nsfw) { String nsfwKey = (accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE; mNsfwAndSpoilerSharedPreferences.edit().putBoolean(nsfwKey, false).apply(); EventBus.getDefault().post(new ChangeNSFWEvent(false)); } else if (stringId == R.string.settings) { intent = new Intent(MainActivity.this, SettingsActivity.class); } else if (stringId == R.string.add_account) { // Explicitly get default SharedPreferences with MODE_PRIVATE as requested SharedPreferences defaultPrefs = getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); String currentClientId = defaultPrefs.getString(SharedPreferencesUtils.CLIENT_ID_PREF_KEY, getString(R.string.default_client_id)); if (currentClientId.equals(getString(R.string.default_client_id))) { new MaterialAlertDialogBuilder(MainActivity.this, R.style.MaterialAlertDialogTheme) .setMessage(R.string.set_client_id_dialog_message) .setPositiveButton(R.string.ok, null) .show(); } else { intent = new Intent(MainActivity.this, LoginActivity.class); } } else if (stringId == R.string.anonymous_account) { AccountManagement.switchToAnonymousMode(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), false, () -> { Intent anonymousIntent = new Intent(MainActivity.this, MainActivity.class); startActivity(anonymousIntent); finish(); }); } else if (stringId == R.string.log_out) { AccountManagement.switchToAnonymousMode(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), true, () -> { Intent logOutIntent = new Intent(MainActivity.this, MainActivity.class); startActivity(logOutIntent); finish(); }); } if (intent != null) { startActivity(intent); } binding.drawerLayout.closeDrawers(); } @Override public void onSubscribedSubredditClick(String subredditName) { Intent intent = new Intent(MainActivity.this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditName); startActivity(intent); } @Override public void onAccountClick(@NonNull String accountName) { AccountManagement.switchAccount(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), accountName, newAccount -> { Intent intent = new Intent(MainActivity.this, MainActivity.class); startActivity(intent); finish(); }); } @Override public void onAccountLongClick(@NonNull String accountName) { new MaterialAlertDialogBuilder(MainActivity.this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.log_out) .setMessage(accountName) .setPositiveButton(R.string.yes, (dialogInterface, i) -> AccountManagement.removeAccount(mRedditDataRoomDatabase, mExecutor, accountName)) .setNegativeButton(R.string.no, null) .show(); } @Override public void onMenuLongClick(int stringId) { if (stringId == R.string.add_account) { Intent intent = new Intent(MainActivity.this, LoginActivity.class); startActivity(intent); binding.drawerLayout.closeDrawers(); } } }); setInboxCount(); binding.navDrawerRecyclerViewMainActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this)); binding.navDrawerRecyclerViewMainActivity.setAdapter(adapter.getConcatAdapter()); int tabCount = mMainActivityTabsSharedPreferences.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_COUNT, Constants.DEFAULT_TAB_COUNT); mShowFavoriteMultiReddits = mMainActivityTabsSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_FAVORITE_MULTIREDDITS, false); mShowMultiReddits = mMainActivityTabsSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_MULTIREDDITS, false); mShowFavoriteSubscribedSubreddits = mMainActivityTabsSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_FAVORITE_SUBSCRIBED_SUBREDDITS, false); mShowSubscribedSubreddits = mMainActivityTabsSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_SUBSCRIBED_SUBREDDITS, false); sectionsPagerAdapter = new SectionsPagerAdapter(this, tabCount, mShowFavoriteMultiReddits, mShowMultiReddits, mShowFavoriteSubscribedSubreddits, mShowSubscribedSubreddits); binding.includedAppBar.viewPagerMainActivity.setAdapter(sectionsPagerAdapter); binding.includedAppBar.viewPagerMainActivity.setUserInputEnabled(!mDisableSwipingBetweenTabs); if (mMainActivityTabsSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_TAB_NAMES, true)) { if (mShowFavoriteMultiReddits || mShowMultiReddits || mShowFavoriteSubscribedSubreddits || mShowSubscribedSubreddits) { binding.includedAppBar.tabLayoutMainActivity.setTabMode(TabLayout.MODE_SCROLLABLE); } else { binding.includedAppBar.tabLayoutMainActivity.setTabMode(TabLayout.MODE_FIXED); } new TabLayoutMediator(binding.includedAppBar.tabLayoutMainActivity, binding.includedAppBar.viewPagerMainActivity, (tab, position) -> { switch (position) { case 0: Utils.setTitleWithCustomFontToTab(typeface, tab, mMainActivityTabsSharedPreferences.getString((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_TITLE, getString(R.string.home))); break; case 1: Utils.setTitleWithCustomFontToTab(typeface, tab, mMainActivityTabsSharedPreferences.getString((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_TITLE, getString(R.string.popular))); break; case 2: Utils.setTitleWithCustomFontToTab(typeface, tab, mMainActivityTabsSharedPreferences.getString((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_TITLE, getString(R.string.all))); break; case 3: Utils.setTitleWithCustomFontToTab(typeface, tab, mMainActivityTabsSharedPreferences.getString((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_4_TITLE, getString(R.string.upvoted))); break; case 4: Utils.setTitleWithCustomFontToTab(typeface, tab, mMainActivityTabsSharedPreferences.getString((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_5_TITLE, getString(R.string.downvoted))); break; case 5: Utils.setTitleWithCustomFontToTab(typeface, tab, mMainActivityTabsSharedPreferences.getString((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_6_TITLE, getString(R.string.saved))); break; } if (position >= tabCount && (mShowFavoriteMultiReddits || mShowMultiReddits || mShowFavoriteSubscribedSubreddits || mShowSubscribedSubreddits) && sectionsPagerAdapter != null) { if (position - tabCount < sectionsPagerAdapter.favoriteMultiReddits.size()) { Utils.setTitleWithCustomFontToTab(typeface, tab, sectionsPagerAdapter.favoriteMultiReddits.get(position - tabCount).getDisplayName()); } else if (position - tabCount - sectionsPagerAdapter.favoriteMultiReddits.size() < sectionsPagerAdapter.multiReddits.size()) { Utils.setTitleWithCustomFontToTab(typeface, tab, sectionsPagerAdapter.multiReddits.get(position - tabCount - sectionsPagerAdapter.favoriteMultiReddits.size()).getDisplayName()); } else if (position - tabCount - sectionsPagerAdapter.favoriteMultiReddits.size() - sectionsPagerAdapter.multiReddits.size() < sectionsPagerAdapter.favoriteSubscribedSubreddits.size()) { Utils.setTitleWithCustomFontToTab(typeface, tab, sectionsPagerAdapter.favoriteSubscribedSubreddits.get(position - tabCount - sectionsPagerAdapter.favoriteMultiReddits.size() - sectionsPagerAdapter.multiReddits.size()).getName()); } else if (position - tabCount - sectionsPagerAdapter.favoriteMultiReddits.size() - sectionsPagerAdapter.multiReddits.size() - sectionsPagerAdapter.favoriteSubscribedSubreddits.size() < sectionsPagerAdapter.subscribedSubreddits.size()) { Utils.setTitleWithCustomFontToTab(typeface, tab, sectionsPagerAdapter.subscribedSubreddits.get(position - tabCount - sectionsPagerAdapter.favoriteMultiReddits.size() - sectionsPagerAdapter.multiReddits.size() - sectionsPagerAdapter.favoriteSubscribedSubreddits.size()).getName()); } } }).attach(); // Add double-tap to scroll to top functionality for all tabs binding.includedAppBar.tabLayoutMainActivity.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { private long lastTabClickTime = 0; private int lastClickedTabPosition = -1; private static final long DOUBLE_TAP_TIME_DELTA = 300; // milliseconds @Override public void onTabSelected(TabLayout.Tab tab) { handleTabClick(tab); } @Override public void onTabUnselected(TabLayout.Tab tab) {} @Override public void onTabReselected(TabLayout.Tab tab) { handleTabClick(tab); } private void handleTabClick(TabLayout.Tab tab) { int position = tab.getPosition(); long currentTime = System.currentTimeMillis(); if (position == lastClickedTabPosition && currentTime - lastTabClickTime < DOUBLE_TAP_TIME_DELTA) { // Double tap detected on same tab scrollTabToTop(position); lastTabClickTime = 0; // Reset to prevent triple-tap lastClickedTabPosition = -1; } else { lastTabClickTime = currentTime; lastClickedTabPosition = position; } } }); } else { binding.includedAppBar.tabLayoutMainActivity.setVisibility(View.GONE); } binding.includedAppBar.viewPagerMainActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (showBottomAppBar) { navigationWrapper.showNavigation(); } if (!hideFab) { navigationWrapper.showFab(); } sectionsPagerAdapter.displaySortTypeInToolbar(); } }); fixViewPager2Sensitivity(binding.includedAppBar.viewPagerMainActivity); loadSubscriptions(); multiRedditViewModel = new ViewModelProvider(this, new MultiRedditViewModel.Factory( mRedditDataRoomDatabase, accountName)) .get(MultiRedditViewModel.class); multiRedditViewModel.getAllFavoriteMultiReddits().observe(this, multiReddits -> { if (mShowFavoriteMultiReddits && sectionsPagerAdapter != null) { sectionsPagerAdapter.setFavoriteMultiReddits(multiReddits); } }); multiRedditViewModel.getAllMultiReddits().observe(this, multiReddits -> { if (mShowMultiReddits && sectionsPagerAdapter != null) { sectionsPagerAdapter.setMultiReddits(multiReddits); } }); subscribedSubredditViewModel = new ViewModelProvider(this, new SubscribedSubredditViewModel.Factory(mRedditDataRoomDatabase, accountName)) .get(SubscribedSubredditViewModel.class); subscribedSubredditViewModel.getAllSubscribedSubreddits().observe(this, subscribedSubredditData -> { adapter.setSubscribedSubreddits(subscribedSubredditData); if (mShowSubscribedSubreddits && sectionsPagerAdapter != null) { sectionsPagerAdapter.setSubscribedSubreddits(subscribedSubredditData); } }); subscribedSubredditViewModel.getAllFavoriteSubscribedSubreddits().observe(this, subscribedSubredditData -> { adapter.setFavoriteSubscribedSubreddits(subscribedSubredditData); if (mShowFavoriteSubscribedSubreddits && sectionsPagerAdapter != null) { sectionsPagerAdapter.setFavoriteSubscribedSubreddits(subscribedSubredditData); } }); accountViewModel = new ViewModelProvider(this, new AccountViewModel.Factory(mExecutor, mRedditDataRoomDatabase)).get(AccountViewModel.class); accountViewModel.getAccountsExceptCurrentAccountLiveData().observe(this, adapter::changeAccountsDataset); accountViewModel.getCurrentAccountLiveData().observe(this, account -> { if (account != null) { adapter.updateAccountInfo(account.getProfileImageUrl(), account.getBannerImageUrl(), account.getKarma()); } }); loadUserData(); if (!accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (mMessageFullname != null) { ReadMessage.readMessage(mOauthRetrofit, accessToken, mMessageFullname, new ReadMessage.ReadMessageListener() { @Override public void readSuccess() { mMessageFullname = null; } @Override public void readFailed() { } }); } } } public void setBottomAppBarContentDescription(View view, int option) { switch (option) { case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: view.setContentDescription(getString(R.string.content_description_subscriptions)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX: view.setContentDescription(getString(R.string.content_description_inbox)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_PROFILE: view.setContentDescription(getString(R.string.content_description_profile)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: view.setContentDescription(getString(R.string.content_description_multireddits)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: view.setContentDescription(getString(R.string.content_description_submit_post)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_REFRESH: view.setContentDescription(getString(R.string.content_description_refresh)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: view.setContentDescription(getString(R.string.content_description_change_sort_type)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: view.setContentDescription(getString(R.string.content_description_change_post_layout)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SEARCH: view.setContentDescription(getString(R.string.content_description_search)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT : view.setContentDescription(getString(R.string.content_description_go_to_subreddit)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_USER : view.setContentDescription(getString(R.string.content_description_go_to_user)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS : view.setContentDescription(getString(R.string.content_description_hide_read_posts)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_FILTER_POSTS : view.setContentDescription(getString(R.string.content_description_filter_posts)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_UPVOTED : view.setContentDescription(getString(R.string.content_description_upvoted)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_DOWNVOTED : view.setContentDescription(getString(R.string.content_description_downvoted)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDDEN : view.setContentDescription(getString(R.string.content_description_hidden)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SAVED : view.setContentDescription(getString(R.string.content_description_saved)); break; case SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_TOP : default: view.setContentDescription(getString(R.string.content_description_go_to_top)); break; } } private void loadSubscriptions() { if (System.currentTimeMillis() - mCurrentAccountSharedPreferences.getLong(SharedPreferencesUtils.SUBSCRIBED_THINGS_SYNC_TIME, 0L) < 24 * 60 * 60 * 1000) { return; } if (!accountName.equals(Account.ANONYMOUS_ACCOUNT) && !mFetchSubscriptionsSuccess) { FetchSubscribedThing.fetchSubscribedThing(mExecutor, mHandler, mOauthRetrofit, accessToken, accountName, null, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new FetchSubscribedThing.FetchSubscribedThingListener() { @Override public void onFetchSubscribedThingSuccess(ArrayList subscribedSubredditData, ArrayList subscribedUserData, ArrayList subredditData) { mCurrentAccountSharedPreferences.edit().putLong(SharedPreferencesUtils.SUBSCRIBED_THINGS_SYNC_TIME, System.currentTimeMillis()).apply(); InsertSubscribedThings.insertSubscribedThings( mExecutor, new Handler(), mRedditDataRoomDatabase, accountName, subscribedSubredditData, subscribedUserData, subredditData, () -> mFetchSubscriptionsSuccess = true); } @Override public void onFetchSubscribedThingFail() { mFetchSubscriptionsSuccess = false; } }); } } private void loadUserData() { if (!mFetchUserInfoSuccess) { FetchUserData.fetchUserData(mExecutor, mHandler, mRedditDataRoomDatabase, mOauthRetrofit, null, accessToken, accountName, new FetchUserData.FetchUserDataListener() { @ExperimentalBadgeUtils @Override public void onFetchUserDataSuccess(UserData userData, int inboxCount) { MainActivity.this.inboxCount = inboxCount; mCurrentAccountSharedPreferences.edit().putInt(SharedPreferencesUtils.INBOX_COUNT, inboxCount).apply(); accountName = userData.getName(); mFetchUserInfoSuccess = true; EventBus.getDefault().post(new ChangeInboxCountEvent(inboxCount)); } @Override public void onFetchUserDataFailed() { mFetchUserInfoSuccess = false; } }); /*FetchMyInfo.fetchAccountInfo(mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, new FetchMyInfo.FetchMyInfoListener() { @Override public void onFetchMyInfoSuccess(String name, String profileImageUrl, String bannerImageUrl, int karma) { mAccountName = name; mFetchUserInfoSuccess = true; } @Override public void onFetchMyInfoFailed(boolean parseFailed) { mFetchUserInfoSuccess = false; } });*/ } } @ExperimentalBadgeUtils private void setInboxCount() { if (adapter != null) { adapter.setInboxCount(inboxCount); } mHandler.post(() -> navigationWrapper.setInboxCount(this, inboxCount)); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_activity, menu); applyMenuItemTheme(menu); return true; } private void changeSortType() { int currentPostType = sectionsPagerAdapter.getCurrentPostType(); PostFragment postFragment = sectionsPagerAdapter.getCurrentFragment(); if (postFragment != null) { SortTypeBottomSheetFragment sortTypeBottomSheetFragment = SortTypeBottomSheetFragment.getNewInstance(currentPostType != PostPagingSource.TYPE_FRONT_PAGE, postFragment.getSortType()); sortTypeBottomSheetFragment.show(getSupportFragmentManager(), sortTypeBottomSheetFragment.getTag()); } } private void scrollTabToTop(int position) { // Get the fragment at the specified position and scroll to top if (sectionsPagerAdapter != null) { PostFragment fragment = sectionsPagerAdapter.getFragmentAtPosition(position); if (fragment != null) { fragment.goBackToTop(); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_search_main_activity) { Intent intent = new Intent(this, SearchActivity.class); startActivity(intent); return true; } else if (itemId == R.id.action_sort_main_activity) { changeSortType(); return true; } else if (itemId == R.id.action_refresh_main_activity) { sectionsPagerAdapter.refresh(); mFetchUserInfoSuccess = false; loadUserData(); return true; } else if (itemId == R.id.action_change_post_layout_main_activity) { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } return false; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (sectionsPagerAdapter != null) { return sectionsPagerAdapter.handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(FETCH_USER_INFO_STATE, mFetchUserInfoSuccess); outState.putBoolean(FETCH_SUBSCRIPTIONS_STATE, mFetchSubscriptionsSuccess); outState.putBoolean(DRAWER_ON_ACCOUNT_SWITCH_STATE, mDrawerOnAccountSwitch); outState.putString(MESSAGE_FULLNAME_STATE, mMessageFullname); outState.putString(NEW_ACCOUNT_NAME_STATE, mNewAccountName); outState.putInt(INBOX_COUNT_STATE, inboxCount); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Override public void sortTypeSelected(SortType sortType) { sectionsPagerAdapter.changeSortType(sortType); } @Override public void sortTypeSelected(String sortType) { SortTimeBottomSheetFragment sortTimeBottomSheetFragment = new SortTimeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(SortTimeBottomSheetFragment.EXTRA_SORT_TYPE, sortType); sortTimeBottomSheetFragment.setArguments(bundle); sortTimeBottomSheetFragment.show(getSupportFragmentManager(), sortTimeBottomSheetFragment.getTag()); } @Override public void postTypeSelected(int postType) { Intent intent; switch (postType) { case PostTypeBottomSheetFragment.TYPE_TEXT: intent = new Intent(MainActivity.this, PostTextActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_LINK: intent = new Intent(MainActivity.this, PostLinkActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_IMAGE: intent = new Intent(MainActivity.this, PostImageActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_VIDEO: intent = new Intent(MainActivity.this, PostVideoActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_GALLERY: intent = new Intent(MainActivity.this, PostGalleryActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_POLL: intent = new Intent(MainActivity.this, PostPollActivity.class); startActivity(intent); } } @Override public void postLayoutSelected(int postLayout) { sectionsPagerAdapter.changePostLayout(postLayout); } @Override public void contentScrollUp() { if (showBottomAppBar && !mLockBottomAppBar) { navigationWrapper.showNavigation(); } if (!(showBottomAppBar && mLockBottomAppBar) && !hideFab) { navigationWrapper.showFab(); } } @Override public void contentScrollDown() { if (!(showBottomAppBar && mLockBottomAppBar) && !hideFab) { navigationWrapper.hideFab(); } if (showBottomAppBar && !mLockBottomAppBar) { navigationWrapper.hideNavigation(); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } @Subscribe public void onChangeNSFWEvent(ChangeNSFWEvent changeNSFWEvent) { sectionsPagerAdapter.changeNSFW(changeNSFWEvent.nsfw); if (adapter != null) { adapter.setNSFWEnabled(changeNSFWEvent.nsfw); } } @Subscribe(threadMode = ThreadMode.MAIN) public void onRecreateActivityEvent(RecreateActivityEvent recreateActivityEvent) { ActivityCompat.recreate(this); } @Subscribe public void onChangeLockBottomAppBar(ChangeLockBottomAppBarEvent changeLockBottomAppBarEvent) { mLockBottomAppBar = changeLockBottomAppBarEvent.lockBottomAppBar; } @Subscribe public void onChangeDisableSwipingBetweenTabsEvent(ChangeDisableSwipingBetweenTabsEvent changeDisableSwipingBetweenTabsEvent) { mDisableSwipingBetweenTabs = changeDisableSwipingBetweenTabsEvent.disableSwipingBetweenTabs; binding.includedAppBar.viewPagerMainActivity.setUserInputEnabled(!mDisableSwipingBetweenTabs); } @Subscribe public void onChangeRequireAuthToAccountSectionEvent(ChangeRequireAuthToAccountSectionEvent changeRequireAuthToAccountSectionEvent) { if (adapter != null) { adapter.setRequireAuthToAccountSection(changeRequireAuthToAccountSectionEvent.requireAuthToAccountSection); } } @Subscribe public void onChangeShowAvatarOnTheRightInTheNavigationDrawerEvent(ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent event) { if (adapter != null) { adapter.setShowAvatarOnTheRightInTheNavigationDrawer(event.showAvatarOnTheRightInTheNavigationDrawer); int previousPosition = -1; if (binding.navDrawerRecyclerViewMainActivity.getLayoutManager() != null) { previousPosition = ((LinearLayoutManagerBugFixed) binding.navDrawerRecyclerViewMainActivity.getLayoutManager()).findFirstVisibleItemPosition(); } RecyclerView.LayoutManager layoutManager = binding.navDrawerRecyclerViewMainActivity.getLayoutManager(); binding.navDrawerRecyclerViewMainActivity.setAdapter(null); binding.navDrawerRecyclerViewMainActivity.setLayoutManager(null); binding.navDrawerRecyclerViewMainActivity.setAdapter(adapter.getConcatAdapter()); binding.navDrawerRecyclerViewMainActivity.setLayoutManager(layoutManager); if (previousPosition > 0) { binding.navDrawerRecyclerViewMainActivity.scrollToPosition(previousPosition); } } } @ExperimentalBadgeUtils @Subscribe public void onChangeInboxCountEvent(ChangeInboxCountEvent event) { this.inboxCount = event.inboxCount; setInboxCount(); } @Subscribe public void onChangeHideKarmaEvent(ChangeHideKarmaEvent event) { if (adapter != null) { adapter.setHideKarma(event.hideKarma); } } @Subscribe public void onChangeHideFabInPostFeed(ChangeHideFabInPostFeedEvent event) { hideFab = event.hideFabInPostFeed; navigationWrapper.floatingActionButton.setVisibility(hideFab ? View.GONE : View.VISIBLE); } @Subscribe public void onNewUserLoggedInEvent(NewUserLoggedInEvent event) { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); finish(); } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void displaySortType() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.displaySortTypeInToolbar(); } } @Override public void fabOptionSelected(int option) { switch (option) { case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SUBMIT_POST: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_REFRESH: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_SORT_TYPE: changeSortType(); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_POST_LAYOUT: PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SEARCH: Intent intent = new Intent(this, SearchActivity.class); startActivity(intent); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_SUBREDDIT: { goToSubreddit(); break; } case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_USER: { goToUser(); break; } case FABMoreOptionsBottomSheetFragment.FAB_HIDE_READ_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_FILTER_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_GO_TO_TOP: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; } } } private void goToSubreddit() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.includedAppBar.coordinatorLayoutMainActivity, false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); RecyclerView recyclerView = rootView.findViewById(R.id.recycler_view_go_to_thing_edit_text); SubredditAutocompleteRecyclerViewAdapter adapter = new SubredditAutocompleteRecyclerViewAdapter( this, mCustomThemeWrapper, subredditData -> { Utils.hideKeyboard(this); Intent intent = new Intent(MainActivity.this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditData.getName()); startActivity(intent); }); recyclerView.setAdapter(adapter); thingEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); return true; } return false; }); boolean nsfw = mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); Handler handler = new Handler(); thingEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { if (subredditAutocompleteCall != null && subredditAutocompleteCall.isExecuted()) { subredditAutocompleteCall.cancel(); } if (autoCompleteRunnable != null) { handler.removeCallbacks(autoCompleteRunnable); } } @Override public void afterTextChanged(Editable editable) { String currentQuery = editable.toString().trim(); if (!currentQuery.isEmpty()) { autoCompleteRunnable = () -> { subredditAutocompleteCall = mOauthRetrofit.create(RedditAPI.class).subredditAutocomplete(APIUtils.getOAuthHeader(accessToken), currentQuery, nsfw); subredditAutocompleteCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { subredditAutocompleteCall = null; if (response.isSuccessful() && !call.isCanceled()) { ParseSubredditData.parseSubredditListingData(mExecutor, handler, response.body(), nsfw, new ParseSubredditData.ParseSubredditListingDataListener() { @Override public void onParseSubredditListingDataSuccess(ArrayList subredditData, String after) { adapter.setSubreddits(subredditData); } @Override public void onParseSubredditListingDataFail() { } }); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { subredditAutocompleteCall = null; } }); }; handler.postDelayed(autoCompleteRunnable, 500); } } }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_subreddit) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } private void goToUser() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.includedAppBar.coordinatorLayoutMainActivity, false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); thingEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); return true; } return false; }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_user) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } @Override public void markPostAsRead(Post post) { int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, accountName, post.getId(), readPostsLimit); } public void doNotShowRedditAPIInfoAgain() { mInternalSharedPreferences.edit().putBoolean(SharedPreferencesUtils.DO_NOT_SHOW_REDDIT_API_INFO_V2_AGAIN, true).apply(); } private class SectionsPagerAdapter extends FragmentStateAdapter { int tabCount; boolean showFavoriteMultiReddits; boolean showMultiReddits; boolean showFavoriteSubscribedSubreddits; boolean showSubscribedSubreddits; List favoriteMultiReddits; List multiReddits; List favoriteSubscribedSubreddits; List subscribedSubreddits; SectionsPagerAdapter(FragmentActivity fa, int tabCount, boolean showFavoriteMultiReddits, boolean showMultiReddits, boolean showFavoriteSubscribedSubreddits, boolean showSubscribedSubreddits) { super(fa); this.tabCount = tabCount; favoriteMultiReddits = new ArrayList<>(); multiReddits = new ArrayList<>(); favoriteSubscribedSubreddits = new ArrayList<>(); subscribedSubreddits = new ArrayList<>(); this.showFavoriteMultiReddits = showFavoriteMultiReddits; this.showMultiReddits = showMultiReddits; this.showFavoriteSubscribedSubreddits = showFavoriteSubscribedSubreddits; this.showSubscribedSubreddits = showSubscribedSubreddits; } @NonNull @Override public Fragment createFragment(int position) { // First, handle the fixed tabs based on their position if (position < tabCount) { String tabPostTypeKey; String tabNameKey; int defaultPostType; String defaultName = ""; // Default name is usually empty or a generic term switch (position) { case 0: tabPostTypeKey = SharedPreferencesUtils.MAIN_PAGE_TAB_1_POST_TYPE; tabNameKey = SharedPreferencesUtils.MAIN_PAGE_TAB_1_NAME; defaultPostType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_HOME; // Default name for Home is often not needed or handled by MAIN_PAGE_TAB_POST_TYPE_HOME break; case 1: tabPostTypeKey = SharedPreferencesUtils.MAIN_PAGE_TAB_2_POST_TYPE; tabNameKey = SharedPreferencesUtils.MAIN_PAGE_TAB_2_NAME; defaultPostType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_POPULAR; break; case 2: tabPostTypeKey = SharedPreferencesUtils.MAIN_PAGE_TAB_3_POST_TYPE; tabNameKey = SharedPreferencesUtils.MAIN_PAGE_TAB_3_NAME; defaultPostType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL; break; case 3: tabPostTypeKey = SharedPreferencesUtils.MAIN_PAGE_TAB_4_POST_TYPE; tabNameKey = SharedPreferencesUtils.MAIN_PAGE_TAB_4_NAME; defaultPostType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL; // Default to 'All' or similar break; case 4: tabPostTypeKey = SharedPreferencesUtils.MAIN_PAGE_TAB_5_POST_TYPE; tabNameKey = SharedPreferencesUtils.MAIN_PAGE_TAB_5_NAME; defaultPostType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL; // Default to 'All' or similar break; case 5: tabPostTypeKey = SharedPreferencesUtils.MAIN_PAGE_TAB_6_POST_TYPE; tabNameKey = SharedPreferencesUtils.MAIN_PAGE_TAB_6_NAME; defaultPostType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL; // Default to 'All' or similar break; default: // Should not happen if getItemCount() is correct // Return a default/fallback fragment or throw an error return generatePostFragment(SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_POPULAR, ""); } int postType = mMainActivityTabsSharedPreferences.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + tabPostTypeKey, defaultPostType); String name = mMainActivityTabsSharedPreferences.getString((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + tabNameKey, defaultName); return generatePostFragment(postType, name); } // Handle dynamic tabs (favorites, multireddits, etc.) that appear after the fixed tabs // The position here is relative to the end of the fixed tabs int dynamicPosition = position - tabCount; if (showFavoriteMultiReddits) { if (dynamicPosition < favoriteMultiReddits.size()) { int postType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT; String name = favoriteMultiReddits.get(dynamicPosition).getPath(); return generatePostFragment(postType, name); } dynamicPosition -= favoriteMultiReddits.size(); } if (showMultiReddits) { if (dynamicPosition < multiReddits.size()) { int postType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT; String name = multiReddits.get(dynamicPosition).getPath(); return generatePostFragment(postType, name); } dynamicPosition -= multiReddits.size(); } if (showFavoriteSubscribedSubreddits) { if (dynamicPosition < favoriteSubscribedSubreddits.size()) { int postType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT; String name = favoriteSubscribedSubreddits.get(dynamicPosition).getName(); return generatePostFragment(postType, name); } dynamicPosition -= favoriteSubscribedSubreddits.size(); } if (showSubscribedSubreddits) { if (dynamicPosition < subscribedSubreddits.size()) { int postType = SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT; String name = subscribedSubreddits.get(dynamicPosition).getName(); return generatePostFragment(postType, name); } } // Fallback if position is out of bounds for dynamic tabs, though getItemCount should prevent this. return generatePostFragment(SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_POPULAR, ""); // Default fallback } public void setFavoriteMultiReddits(List favoriteMultiReddits) { this.favoriteMultiReddits = favoriteMultiReddits; notifyDataSetChanged(); } public void setMultiReddits(List multiReddits) { this.multiReddits = multiReddits; notifyDataSetChanged(); } public void setFavoriteSubscribedSubreddits(List favoriteSubscribedSubreddits) { this.favoriteSubscribedSubreddits = favoriteSubscribedSubreddits; notifyDataSetChanged(); } public void setSubscribedSubreddits(List subscribedSubreddits) { this.subscribedSubreddits = subscribedSubreddits; notifyDataSetChanged(); } private Fragment generatePostFragment(int postType, String name) { if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_HOME) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE : PostPagingSource.TYPE_FRONT_PAGE); fragment.setArguments(bundle); return fragment; } else if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_SUBREDDIT); bundle.putString(PostFragment.EXTRA_NAME, "all"); fragment.setArguments(bundle); return fragment; } else if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_SUBREDDIT); bundle.putString(PostFragment.EXTRA_NAME, name); fragment.setArguments(bundle); return fragment; } else if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putString(PostFragment.EXTRA_NAME, name); bundle.putInt(PostFragment.EXTRA_POST_TYPE, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT : PostPagingSource.TYPE_MULTI_REDDIT); fragment.setArguments(bundle); return fragment; } else if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_USER); bundle.putString(PostFragment.EXTRA_USER_NAME, name); bundle.putString(PostFragment.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_SUBMITTED); fragment.setArguments(bundle); return fragment; } else if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_UPVOTED || postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_DOWNVOTED || postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_HIDDEN || postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SAVED) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_USER); bundle.putString(PostFragment.EXTRA_USER_NAME, accountName); bundle.putBoolean(PostFragment.EXTRA_DISABLE_READ_POSTS, true); if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_UPVOTED) { bundle.putString(PostFragment.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_UPVOTED); } else if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_DOWNVOTED) { bundle.putString(PostFragment.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_DOWNVOTED); } else if (postType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_HIDDEN) { bundle.putString(PostFragment.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_HIDDEN); } else { bundle.putString(PostFragment.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_SAVED); } fragment.setArguments(bundle); return fragment; } else { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_SUBREDDIT); bundle.putString(PostFragment.EXTRA_NAME, "popular"); fragment.setArguments(bundle); return fragment; } } @Override public int getItemCount() { return tabCount + favoriteMultiReddits.size() + multiReddits.size() + favoriteSubscribedSubreddits.size() + subscribedSubreddits.size(); } @Nullable private PostFragment getCurrentFragment() { if (fragmentManager == null) { return null; } Fragment fragment = fragmentManager.findFragmentByTag("f" + binding.includedAppBar.viewPagerMainActivity.getCurrentItem()); if (fragment instanceof PostFragment) { return (PostFragment) fragment; } return null; } @Nullable private PostFragment getFragmentAtPosition(int position) { if (fragmentManager == null) { return null; } Fragment fragment = fragmentManager.findFragmentByTag("f" + position); if (fragment instanceof PostFragment) { return (PostFragment) fragment; } return null; } boolean handleKeyDown(int keyCode) { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { return currentFragment.handleKeyDown(keyCode); } return false; } int getCurrentPostType() { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { return currentFragment.getPostType(); } return PostPagingSource.TYPE_SUBREDDIT; } void changeSortType(SortType sortType) { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { currentFragment.changeSortType(sortType); } displaySortTypeInToolbar(); } public void refresh() { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { currentFragment.refresh(); } } void changeNSFW(boolean nsfw) { for (int i = 0; i < getItemCount(); i++) { Fragment fragment = fragmentManager.findFragmentByTag("f" + i); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeNSFW(nsfw); } } } void changePostLayout(int postLayout) { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { currentFragment.changePostLayout(postLayout); } } void goBackToTop() { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { currentFragment.goBackToTop(); } } void displaySortTypeInToolbar() { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { SortType sortType = currentFragment.getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.includedAppBar.toolbar); } } void hideReadPosts() { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { currentFragment.hideReadPosts(); } } void filterPosts() { PostFragment currentFragment = getCurrentFragment(); if (currentFragment != null) { currentFragment.filterPosts(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostFilterPreferenceActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.PostFilterWithUsageRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostFilterOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityPostFilterPreferenceBinding; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.postfilter.DeletePostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterWithUsage; import ml.docilealligator.infinityforreddit.postfilter.PostFilterWithUsageViewModel; import ml.docilealligator.infinityforreddit.utils.Utils; public class PostFilterPreferenceActivity extends BaseActivity { public static final String EXTRA_POST = "EP"; public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_USER_NAME = "EUN"; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor executor; public PostFilterWithUsageViewModel postFilterWithUsageViewModel; private PostFilterWithUsageRecyclerViewAdapter adapter; private ActivityPostFilterPreferenceBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityPostFilterPreferenceBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostFilterPreferenceActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarPostFilterPreferenceActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewPostFilterPreferenceActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); setMargins(binding.fabPostFilterPreferenceActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, PostFilterPreferenceActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, PostFilterPreferenceActivity.this) + allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostFilterPreferenceActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); Post post = getIntent().getParcelableExtra(EXTRA_POST); String subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); String username = getIntent().getStringExtra(EXTRA_USER_NAME); binding.fabPostFilterPreferenceActivity.setOnClickListener(view -> { if (post != null) { showPostFilterOptions(post, null); } else if (subredditName != null) { excludeSubredditInFilter(subredditName, null); } else if (username != null) { excludeUserInFilter(username, null); } else { Intent intent = new Intent(PostFilterPreferenceActivity.this, CustomizePostFilterActivity.class); intent.putExtra(CustomizePostFilterActivity.EXTRA_FROM_SETTINGS, true); startActivity(intent); } }); adapter = new PostFilterWithUsageRecyclerViewAdapter(this, customThemeWrapper, postFilter -> { if (post != null) { showPostFilterOptions(post, postFilter); } else if (subredditName != null) { excludeSubredditInFilter(subredditName, postFilter); } else if (username != null) { excludeUserInFilter(username, postFilter); } else { PostFilterOptionsBottomSheetFragment postFilterOptionsBottomSheetFragment = new PostFilterOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(PostFilterOptionsBottomSheetFragment.EXTRA_POST_FILTER, postFilter); postFilterOptionsBottomSheetFragment.setArguments(bundle); postFilterOptionsBottomSheetFragment.show(getSupportFragmentManager(), postFilterOptionsBottomSheetFragment.getTag()); } }); binding.recyclerViewPostFilterPreferenceActivity.setAdapter(adapter); postFilterWithUsageViewModel = new ViewModelProvider(this, new PostFilterWithUsageViewModel.Factory(redditDataRoomDatabase)).get(PostFilterWithUsageViewModel.class); postFilterWithUsageViewModel.getPostFilterWithUsageListLiveData().observe(this, new Observer>() { @Override public void onChanged(List postFilterWithUsages) { adapter.setPostFilterWithUsageList(postFilterWithUsages); } }); } public void showPostFilterOptions(Post post, @Nullable PostFilter postFilter) { String[] options = getResources().getStringArray(R.array.add_to_post_filter_options); boolean[] selectedOptions = new boolean[]{false, false, false, false, false, false, false, false}; new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.select) .setMultiChoiceItems(options, selectedOptions, (dialogInterface, i, b) -> selectedOptions[i] = b) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Intent intent = new Intent(PostFilterPreferenceActivity.this, CustomizePostFilterActivity.class); if (postFilter != null) { intent.putExtra(CustomizePostFilterActivity.EXTRA_POST_FILTER, postFilter); } intent.putExtra(CustomizePostFilterActivity.EXTRA_FROM_SETTINGS, true); for (int j = 0; j < selectedOptions.length; j++) { if (selectedOptions[j]) { switch (j) { case 0: intent.putExtra(CustomizePostFilterActivity.EXTRA_EXCLUDE_SUBREDDIT, post.getSubredditName()); break; case 1: intent.putExtra(CustomizePostFilterActivity.EXTRA_EXCLUDE_USER, post.getAuthor()); break; case 2: intent.putExtra(CustomizePostFilterActivity.EXTRA_EXCLUDE_FLAIR, post.getFlair()); break; case 3: intent.putExtra(CustomizePostFilterActivity.EXTRA_CONTAIN_FLAIR, post.getFlair()); break; case 4: intent.putExtra(CustomizePostFilterActivity.EXTRA_EXCLUDE_DOMAIN, post.getUrl()); break; case 5: intent.putExtra(CustomizePostFilterActivity.EXTRA_CONTAIN_DOMAIN, post.getUrl()); break; case 6: intent.putExtra(CustomizePostFilterActivity.EXTRA_CONTAIN_SUBREDDIT, post.getSubredditName()); break; case 7: intent.putExtra(CustomizePostFilterActivity.EXTRA_CONTAIN_USER, post.getAuthor()); break; } } } startActivity(intent); }) .show(); } public void excludeSubredditInFilter(String subredditName, PostFilter postFilter) { Intent intent = new Intent(this, CustomizePostFilterActivity.class); intent.putExtra(CustomizePostFilterActivity.EXTRA_EXCLUDE_SUBREDDIT, subredditName); if (postFilter != null) { intent.putExtra(CustomizePostFilterActivity.EXTRA_POST_FILTER, postFilter); } startActivity(intent); } public void excludeUserInFilter(String username, PostFilter postFilter) { Intent intent = new Intent(this, CustomizePostFilterActivity.class); intent.putExtra(CustomizePostFilterActivity.EXTRA_EXCLUDE_USER, username); if (postFilter != null) { intent.putExtra(CustomizePostFilterActivity.EXTRA_POST_FILTER, postFilter); } startActivity(intent); } public void editPostFilter(PostFilter postFilter) { Intent intent = new Intent(PostFilterPreferenceActivity.this, CustomizePostFilterActivity.class); intent.putExtra(CustomizePostFilterActivity.EXTRA_POST_FILTER, postFilter); intent.putExtra(CustomizePostFilterActivity.EXTRA_FROM_SETTINGS, true); startActivity(intent); } public void applyPostFilterTo(PostFilter postFilter) { Intent intent = new Intent(this, PostFilterUsageListingActivity.class); intent.putExtra(PostFilterUsageListingActivity.EXTRA_POST_FILTER, postFilter); startActivity(intent); } public void deletePostFilter(PostFilter postFilter) { DeletePostFilter.deletePostFilter(redditDataRoomDatabase, executor, postFilter); } @Override public SharedPreferences getDefaultSharedPreferences() { return sharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return customThemeWrapper; } @Override protected void applyCustomTheme() { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostFilterPreferenceActivity, binding.collapsingToolbarLayoutPostFilterPreferenceActivity, binding.toolbarPostFilterPreferenceActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutPostFilterPreferenceActivity); applyFABTheme(binding.fabPostFilterPreferenceActivity); binding.getRoot().setBackgroundColor(customThemeWrapper.getBackgroundColor()); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostFilterUsageListingActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.os.Bundle; import android.os.Handler; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.PostFilterUsageRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.bottomsheetfragments.NewPostFilterUsageBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostFilterUsageOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityPostFilterApplicationBinding; import ml.docilealligator.infinityforreddit.postfilter.DeletePostFilterUsage; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsageViewModel; import ml.docilealligator.infinityforreddit.postfilter.SavePostFilterUsage; import ml.docilealligator.infinityforreddit.utils.Utils; public class PostFilterUsageListingActivity extends BaseActivity { public static final String EXTRA_POST_FILTER = "EPF"; public static final int ADD_SUBREDDITS_REQUEST_CODE = 666; public static final int ADD_USERS_REQUEST_CODE = 777; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor executor; public PostFilterUsageViewModel postFilterUsageViewModel; private PostFilterUsageRecyclerViewAdapter adapter; private PostFilter postFilter; private ActivityPostFilterApplicationBinding binding; private TextInputEditText textInputEditText; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityPostFilterApplicationBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostFilterApplicationActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarPostFilterApplicationActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewPostFilterApplicationActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); setMargins(binding.fabPostFilterApplicationActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, PostFilterUsageListingActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, PostFilterUsageListingActivity.this) + allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostFilterApplicationActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); postFilter = getIntent().getParcelableExtra(EXTRA_POST_FILTER); setTitle(postFilter.name); binding.fabPostFilterApplicationActivity.setOnClickListener(view -> { NewPostFilterUsageBottomSheetFragment newPostFilterUsageBottomSheetFragment = new NewPostFilterUsageBottomSheetFragment(); newPostFilterUsageBottomSheetFragment.show(getSupportFragmentManager(), newPostFilterUsageBottomSheetFragment.getTag()); }); adapter = new PostFilterUsageRecyclerViewAdapter(this, customThemeWrapper, postFilterUsage -> { PostFilterUsageOptionsBottomSheetFragment postFilterUsageOptionsBottomSheetFragment = new PostFilterUsageOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(PostFilterUsageOptionsBottomSheetFragment.EXTRA_POST_FILTER_USAGE, postFilterUsage); postFilterUsageOptionsBottomSheetFragment.setArguments(bundle); postFilterUsageOptionsBottomSheetFragment.show(getSupportFragmentManager(), postFilterUsageOptionsBottomSheetFragment.getTag()); }); binding.recyclerViewPostFilterApplicationActivity.setAdapter(adapter); postFilterUsageViewModel = new ViewModelProvider(this, new PostFilterUsageViewModel.Factory(redditDataRoomDatabase, postFilter.name)).get(PostFilterUsageViewModel.class); postFilterUsageViewModel.getPostFilterUsageListLiveData().observe(this, postFilterUsages -> adapter.setPostFilterUsages(postFilterUsages)); } public void newPostFilterUsage(int type) { switch (type) { case PostFilterUsage.HOME_TYPE: case PostFilterUsage.SEARCH_TYPE: case PostFilterUsage.HISTORY_TYPE: case PostFilterUsage.UPVOTED_TYPE: case PostFilterUsage.DOWNVOTED_TYPE: case PostFilterUsage.HIDDEN_TYPE: case PostFilterUsage.SAVED_TYPE: PostFilterUsage postFilterUsage = new PostFilterUsage(postFilter.name, type, PostFilterUsage.NO_USAGE); SavePostFilterUsage.savePostFilterUsage(redditDataRoomDatabase, executor, postFilterUsage); break; case PostFilterUsage.SUBREDDIT_TYPE: case PostFilterUsage.USER_TYPE: case PostFilterUsage.MULTIREDDIT_TYPE: editAndPostFilterUsageNameOfUsage(type, null); break; } } private void editAndPostFilterUsageNameOfUsage(int type, String nameOfUsage) { View dialogView = getLayoutInflater().inflate(R.layout.dialog_edit_post_or_comment_filter_name_of_usage, null); TextInputLayout textInputLayout = dialogView.findViewById(R.id.text_input_layout_edit_post_or_comment_filter_name_of_usage_dialog); textInputEditText = dialogView.findViewById(R.id.text_input_edit_text_edit_post_or_comment_filter_name_of_usage_dialog); ImageView excludeIv = dialogView.findViewById(R.id.add_subreddits_users_image_view_customize_post_filter_activity); int primaryIconColor = customThemeWrapper.getPrimaryIconColor(); excludeIv.setImageDrawable( Utils.getTintedDrawable(this, R.drawable.ic_add_24dp, primaryIconColor)); int primaryTextColor = customThemeWrapper.getPrimaryTextColor(); textInputLayout.setBoxStrokeColor(primaryTextColor); textInputLayout.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); textInputEditText.setTextColor(primaryTextColor); if (nameOfUsage != null && !nameOfUsage.equals(PostFilterUsage.NO_USAGE)) { textInputEditText.setText(nameOfUsage); } textInputEditText.requestFocus(); int titleStringId = R.string.subreddit; switch (type) { case PostFilterUsage.SUBREDDIT_TYPE: textInputEditText.setHint(R.string.settings_tab_subreddit_name); excludeIv.setOnClickListener(v -> { Intent intent = new Intent(this, SubredditMultiselectionActivity.class); intent.putExtra(SubredditMultiselectionActivity.EXTRA_GET_SELECTED_SUBREDDITS, textInputEditText.getText().toString().trim()); startActivityForResult(intent, ADD_SUBREDDITS_REQUEST_CODE); }); break; case PostFilterUsage.USER_TYPE: textInputEditText.setHint(R.string.settings_tab_username); titleStringId = R.string.user; excludeIv.setOnClickListener(view -> { Intent intent = new Intent(this, UserMultiselectionActivity.class); intent.putExtra(UserMultiselectionActivity.EXTRA_GET_SELECTED_USERS, textInputEditText.getText().toString().trim()); startActivityForResult(intent, ADD_USERS_REQUEST_CODE); }); break; case PostFilterUsage.MULTIREDDIT_TYPE: textInputEditText.setHint(R.string.settings_tab_multi_reddit_name); titleStringId = R.string.multi_reddit; excludeIv.setVisibility(View.GONE); break; } Utils.showKeyboard(this, new Handler(), textInputEditText); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleStringId) .setView(dialogView) .setPositiveButton(R.string.ok, (editTextDialogInterface, i1) -> { Utils.hideKeyboard(this); PostFilterUsage postFilterUsage; if (textInputEditText.getText().toString().equals("")) { postFilterUsage = new PostFilterUsage(postFilter.name, type, PostFilterUsage.NO_USAGE); } else { postFilterUsage = new PostFilterUsage(postFilter.name, type, textInputEditText.getText().toString()); } SavePostFilterUsage.savePostFilterUsage(redditDataRoomDatabase, executor, postFilterUsage); }) .setNegativeButton(R.string.cancel, null) .setOnDismissListener(editTextDialogInterface -> { Utils.hideKeyboard(this); }) .show(); } public void editPostFilterUsage(PostFilterUsage postFilterUsage) { editAndPostFilterUsageNameOfUsage(postFilterUsage.usage, postFilterUsage.nameOfUsage); } public void deletePostFilterUsage(PostFilterUsage postFilterUsage) { DeletePostFilterUsage.deletePostFilterUsage(redditDataRoomDatabase, executor, postFilterUsage); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override public SharedPreferences getDefaultSharedPreferences() { return sharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return customThemeWrapper; } @Override protected void applyCustomTheme() { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostFilterApplicationActivity, binding.collapsingToolbarLayoutPostFilterApplicationActivity, binding.toolbarPostFilterApplicationActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutPostFilterApplicationActivity); applyFABTheme(binding.fabPostFilterApplicationActivity); binding.getRoot().setBackgroundColor(customThemeWrapper.getBackgroundColor()); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && data != null && textInputEditText != null) { if (requestCode == ADD_SUBREDDITS_REQUEST_CODE) { ArrayList subredditNames = data.getStringArrayListExtra(SubredditMultiselectionActivity.EXTRA_RETURN_SELECTED_SUBREDDITS); applyNewItemsToField(textInputEditText, subredditNames); } else if (requestCode == ADD_USERS_REQUEST_CODE) { ArrayList usernames = data.getStringArrayListExtra(UserMultiselectionActivity.EXTRA_RETURN_SELECTED_USERNAMES); applyNewItemsToField(textInputEditText, usernames); } } } private void applyNewItemsToField(com.google.android.material.textfield.TextInputEditText field, @Nullable ArrayList newItems) { if (newItems == null || newItems.isEmpty()) return; String currentCsv = field.getText().toString().trim(); List toAdd = getToAdd(currentCsv, newItems); if (toAdd.isEmpty()) return; StringBuilder updated = getStringBuilder(currentCsv, toAdd); field.setText(updated.toString()); } @NonNull private static List getToAdd(String currentCsv, List candidates) { Set existing = new HashSet<>(); if (!currentCsv.isEmpty()) { for (String u : currentCsv.split(",")) { String t = u.trim(); if (!t.isEmpty()) existing.add(t); } } List toAdd = new ArrayList<>(); for (String s : candidates) { String trimmed = s.trim(); if (!trimmed.isEmpty() && !existing.contains(trimmed)) { toAdd.add(trimmed); } } return toAdd; } @NonNull private static StringBuilder getStringBuilder(String currentCsv, List toAdd) { StringBuilder sb = new StringBuilder(); if (!currentCsv.isEmpty() && !currentCsv.endsWith(",")) { sb.append(currentCsv).append(","); } else { sb.append(currentCsv); } for (String u : toAdd) { sb.append(u).append(","); } if (sb.length() > 0 && sb.charAt(sb.length() - 1) == ',') { sb.deleteCharAt(sb.length() - 1); } return sb; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostGalleryActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.PersistableBundle; import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.gson.Gson; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.adapters.RedditGallerySubmissionRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SelectOrCaptureImageBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityPostGalleryBinding; import ml.docilealligator.infinityforreddit.events.SubmitGalleryPostEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.post.RedditGalleryPayload; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.UploadImageUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class PostGalleryActivity extends BaseActivity implements FlairBottomSheetFragment.FlairSelectionCallback, AccountChooserBottomSheetFragment.AccountChooserListener { static final String EXTRA_SUBREDDIT_NAME = "ESN"; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String SUBREDDIT_ICON_STATE = "SIS"; private static final String SUBREDDIT_SELECTED_STATE = "SSS"; private static final String SUBREDDIT_IS_USER_STATE = "SIUS"; private static final String LOAD_SUBREDDIT_ICON_STATE = "LSIS"; private static final String IS_POSTING_STATE = "IPS"; private static final String FLAIR_STATE = "FS"; private static final String IS_SPOILER_STATE = "ISS"; private static final String IS_NSFW_STATE = "INS"; private static final String REDDIT_GALLERY_IMAGE_INFO_STATE = "RGIIS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 0; private static final int PICK_IMAGE_REQUEST_CODE = 1; private static final int CAPTURE_IMAGE_REQUEST_CODE = 2; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Account selectedAccount; private ArrayList redditGalleryImageInfoList; private String iconUrl; private String subredditName; private boolean subredditSelected = false; private boolean subredditIsUser; private boolean loadSubredditIconSuccessful = true; private boolean isPosting; private int primaryTextColor; private int flairBackgroundColor; private int flairTextColor; private int spoilerBackgroundColor; private int spoilerTextColor; private int nsfwBackgroundColor; private int nsfwTextColor; private Flair flair; private boolean isSpoiler = false; private boolean isNSFW = false; private Resources resources; private Menu mMemu; private RequestManager mGlide; private FlairBottomSheetFragment flairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private RedditGallerySubmissionRecyclerViewAdapter adapter; private Uri imageUri; private boolean isUploading; private ActivityPostGalleryBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityPostGalleryBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostGalleryActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarPostGalleryActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutPostGalleryActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostGalleryActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); mPostingSnackbar = Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.posting, Snackbar.LENGTH_INDEFINITE); resources = getResources(); adapter = new RedditGallerySubmissionRecyclerViewAdapter(this, mCustomThemeWrapper, () -> { if (!isUploading) { SelectOrCaptureImageBottomSheetFragment fragment = new SelectOrCaptureImageBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); } else { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.please_wait_image_is_uploading, Snackbar.LENGTH_SHORT).show(); } }); binding.imagesRecyclerViewPostGalleryActivity.setAdapter(adapter); Resources resources = getResources(); int nColumns = resources.getBoolean(R.bool.isTablet) || resources.getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 3 : 2; ((GridLayoutManager) binding.imagesRecyclerViewPostGalleryActivity.getLayoutManager()).setSpanCount(nColumns); binding.imagesRecyclerViewPostGalleryActivity.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int offset = (int) (Utils.convertDpToPixel(16, PostGalleryActivity.this)); int halfOffset = offset / 2; outRect.set(halfOffset, 0, halfOffset, offset); } }); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); iconUrl = savedInstanceState.getString(SUBREDDIT_ICON_STATE); subredditSelected = savedInstanceState.getBoolean(SUBREDDIT_SELECTED_STATE); subredditIsUser = savedInstanceState.getBoolean(SUBREDDIT_IS_USER_STATE); loadSubredditIconSuccessful = savedInstanceState.getBoolean(LOAD_SUBREDDIT_ICON_STATE); isPosting = savedInstanceState.getBoolean(IS_POSTING_STATE); flair = savedInstanceState.getParcelable(FLAIR_STATE); isSpoiler = savedInstanceState.getBoolean(IS_SPOILER_STATE); isNSFW = savedInstanceState.getBoolean(IS_NSFW_STATE); redditGalleryImageInfoList = savedInstanceState.getParcelableArrayList(REDDIT_GALLERY_IMAGE_INFO_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostGalleryActivity); binding.accountNameTextViewPostGalleryActivity.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } if (redditGalleryImageInfoList != null && !redditGalleryImageInfoList.isEmpty()) { if (redditGalleryImageInfoList.get(redditGalleryImageInfoList.size() - 1).payload == null) { imageUri = Uri.parse(redditGalleryImageInfoList.get(redditGalleryImageInfoList.size() - 1).imageUrlString); uploadImage(); } } adapter.setRedditGalleryImageInfoList(redditGalleryImageInfoList); if (subredditName != null) { binding.subredditNameTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostGalleryActivity.setText(subredditName); binding.flairCustomTextViewPostGalleryActivity.setVisibility(View.VISIBLE); if (!loadSubredditIconSuccessful) { loadSubredditIcon(); } } displaySubredditIcon(); if (isPosting) { mPostingSnackbar.show(); } if (flair != null) { binding.flairCustomTextViewPostGalleryActivity.setText(flair.getText()); binding.flairCustomTextViewPostGalleryActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostGalleryActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostGalleryActivity.setTextColor(flairTextColor); } if (isSpoiler) { binding.spoilerCustomTextViewPostGalleryActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostGalleryActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostGalleryActivity.setTextColor(spoilerTextColor); } if (isNSFW) { binding.nsfwCustomTextViewPostGalleryActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostGalleryActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostGalleryActivity.setTextColor(nsfwTextColor); } } else { isPosting = false; loadCurrentAccount(); if (getIntent().hasExtra(EXTRA_SUBREDDIT_NAME)) { loadSubredditIconSuccessful = false; subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); subredditSelected = true; binding.subredditNameTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostGalleryActivity.setText(subredditName); binding.flairCustomTextViewPostGalleryActivity.setVisibility(View.VISIBLE); loadSubredditIcon(); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostGalleryActivity); } } binding.accountLinearLayoutPostGalleryActivity.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.subredditRelativeLayoutPostGalleryActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SPECIFIED_ACCOUNT, selectedAccount); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.rulesButtonPostGalleryActivity.setOnClickListener(view -> { if (subredditName == null) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, RulesActivity.class); if (subredditIsUser) { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, subredditName); } startActivity(intent); } }); binding.flairCustomTextViewPostGalleryActivity.setOnClickListener(view -> { if (flair == null) { flairSelectionBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, subredditName); flairSelectionBottomSheetFragment.setArguments(bundle); flairSelectionBottomSheetFragment.show(getSupportFragmentManager(), flairSelectionBottomSheetFragment.getTag()); } else { binding.flairCustomTextViewPostGalleryActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostGalleryActivity.setText(getString(R.string.flair)); flair = null; } }); binding.spoilerCustomTextViewPostGalleryActivity.setOnClickListener(view -> { if (!isSpoiler) { binding.spoilerCustomTextViewPostGalleryActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostGalleryActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostGalleryActivity.setTextColor(spoilerTextColor); isSpoiler = true; } else { binding.spoilerCustomTextViewPostGalleryActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.spoilerCustomTextViewPostGalleryActivity.setTextColor(primaryTextColor); isSpoiler = false; } }); binding.nsfwCustomTextViewPostGalleryActivity.setOnClickListener(view -> { if (!isNSFW) { binding.nsfwCustomTextViewPostGalleryActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostGalleryActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostGalleryActivity.setTextColor(nsfwTextColor); isNSFW = true; } else { binding.nsfwCustomTextViewPostGalleryActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.nsfwCustomTextViewPostGalleryActivity.setTextColor(primaryTextColor); isNSFW = false; } }); binding.receivePostReplyNotificationsLinearLayoutPostGalleryActivity.setOnClickListener(view -> { binding.receivePostReplyNotificationsSwitchMaterialPostGalleryActivity.performClick(); }); MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( PostGalleryActivity.this, binding.postContentEditTextPostGalleryActivity, item); } @Override public void onUploadImage() { } }); binding.markdownBottomBarRecyclerViewPostGalleryActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManager.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewPostGalleryActivity.setAdapter(adapter); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isPosting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_submit_post_detail); } else { redditGalleryImageInfoList = PostGalleryActivity.this.adapter.getRedditGalleryImageInfoList(); if (!binding.postTitleEditTextPostGalleryActivity.getText().toString().isEmpty() || !binding.postContentEditTextPostGalleryActivity.getText().toString().isEmpty() || redditGalleryImageInfoList != null) { promptAlertDialog(R.string.discard, R.string.discard_detail); } else { finish(); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostGalleryActivity); binding.accountNameTextViewPostGalleryActivity.setText(account.getAccountName()); } }); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutPostGalleryActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostGalleryActivity, null, binding.toolbarPostGalleryActivity); primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.accountNameTextViewPostGalleryActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.subredditNameTextViewPostGalleryActivity.setTextColor(secondaryTextColor); binding.rulesButtonPostGalleryActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.rulesButtonPostGalleryActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.receivePostReplyNotificationsTextViewPostGalleryActivity.setTextColor(primaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1PostGalleryActivity.setDividerColor(dividerColor); binding.divider2PostGalleryActivity.setDividerColor(dividerColor); flairBackgroundColor = mCustomThemeWrapper.getFlairBackgroundColor(); flairTextColor = mCustomThemeWrapper.getFlairTextColor(); spoilerBackgroundColor = mCustomThemeWrapper.getSpoilerBackgroundColor(); spoilerTextColor = mCustomThemeWrapper.getSpoilerTextColor(); nsfwBackgroundColor = mCustomThemeWrapper.getNsfwBackgroundColor(); nsfwTextColor = mCustomThemeWrapper.getNsfwTextColor(); binding.flairCustomTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.spoilerCustomTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.nsfwCustomTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostGalleryActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostGalleryActivity.setHintTextColor(secondaryTextColor); binding.postContentEditTextPostGalleryActivity.setTextColor(primaryTextColor); binding.postContentEditTextPostGalleryActivity.setHintTextColor(secondaryTextColor); if (typeface != null) { binding.subredditNameTextViewPostGalleryActivity.setTypeface(typeface); binding.rulesButtonPostGalleryActivity.setTypeface(typeface); binding.receivePostReplyNotificationsTextViewPostGalleryActivity.setTypeface(typeface); binding.flairCustomTextViewPostGalleryActivity.setTypeface(typeface); binding.spoilerCustomTextViewPostGalleryActivity.setTypeface(typeface); binding.nsfwCustomTextViewPostGalleryActivity.setTypeface(typeface); binding.postTitleEditTextPostGalleryActivity.setTypeface(typeface); } if (contentTypeface != null) { binding.postContentEditTextPostGalleryActivity.setTypeface(contentTypeface); } } public void selectImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, resources.getString(R.string.select_from_gallery)), PICK_IMAGE_REQUEST_CODE); } public void captureImage() { Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { imageUri = FileProvider.getUriForFile(this, getPackageName() + ".provider", File.createTempFile("temp_img", ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES))); pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); startActivityForResult(pictureIntent, CAPTURE_IMAGE_REQUEST_CODE); } catch (IOException ex) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.error_creating_temp_file, Snackbar.LENGTH_SHORT).show(); } catch (ActivityNotFoundException e) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.no_camera_available, Snackbar.LENGTH_SHORT).show(); } } private void uploadImage() { Handler handler = new Handler(); isUploading = true; mExecutor.execute(() -> { try { String response = UploadImageUtils.uploadImage(mOauthRetrofit, mUploadMediaRetrofit, getContentResolver(), accessToken, imageUri, true, false); String mediaId = new JSONObject(response).getJSONObject(JSONUtils.ASSET_KEY).getString(JSONUtils.ASSET_ID_KEY); handler.post(() -> { adapter.setImageAsUploaded(mediaId); isUploading = false; }); } catch (XmlPullParserException | JSONException | IOException e) { e.printStackTrace(); handler.post(() -> { adapter.removeFailedToUploadImage(); Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.upload_image_failed, Snackbar.LENGTH_LONG).show(); isUploading = false; }); } }); } private void displaySubredditIcon() { if (iconUrl != null && !iconUrl.isEmpty()) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.subredditIconGifImageViewPostGalleryActivity); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostGalleryActivity); } } private void loadSubredditIcon() { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accessToken, accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { iconUrl = iconImageUrl; displaySubredditIcon(); loadSubredditIconSuccessful = true; }); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.post_gallery_activity, menu); applyMenuItemTheme(menu); mMemu = menu; if (isPosting) { mMemu.findItem(R.id.action_send_post_gallery_activity).setEnabled(false); mMemu.findItem(R.id.action_send_post_gallery_activity).getIcon().setAlpha(130); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_send_post_gallery_activity) { if (!subredditSelected) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); return true; } if (binding.postTitleEditTextPostGalleryActivity.getText() == null || binding.postTitleEditTextPostGalleryActivity.getText().toString().isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.title_required, Snackbar.LENGTH_SHORT).show(); return true; } redditGalleryImageInfoList = adapter.getRedditGalleryImageInfoList(); if (redditGalleryImageInfoList == null || redditGalleryImageInfoList.isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.select_an_image, Snackbar.LENGTH_SHORT).show(); return true; } if (redditGalleryImageInfoList.get(redditGalleryImageInfoList.size() - 1).payload == null) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.please_wait_image_is_uploading, Snackbar.LENGTH_LONG).show(); return true; } isPosting = true; item.setEnabled(false); item.getIcon().setAlpha(130); mPostingSnackbar.show(); String subredditName; if (subredditIsUser) { subredditName = "u_" + binding.subredditNameTextViewPostGalleryActivity.getText().toString(); } else { subredditName = binding.subredditNameTextViewPostGalleryActivity.getText().toString(); } /*Intent intent = new Intent(this, SubmitPostService.class); intent.putExtra(SubmitPostService.EXTRA_ACCOUNT, selectedAccount); intent.putExtra(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); intent.putExtra(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_GALLERY); ArrayList items = new ArrayList<>(); for (RedditGallerySubmissionRecyclerViewAdapter.RedditGalleryImageInfo i : redditGalleryImageInfoList) { items.add(i.payload); } RedditGalleryPayload payload = new RedditGalleryPayload(subredditName, subredditIsUser ? "profile" : "subreddit", binding.postTitleEditTextPostGalleryActivity.getText().toString(), binding.postContentEditTextPostGalleryActivity.getText().toString(), isSpoiler, isNSFW, binding.receivePostReplyNotificationsSwitchMaterialPostGalleryActivity.isChecked(), flair, items); intent.putExtra(SubmitPostService.EXTRA_REDDIT_GALLERY_PAYLOAD, new Gson().toJson(payload)); ContextCompat.startForegroundService(this, intent);*/ PersistableBundle extras = new PersistableBundle(); extras.putString(SubmitPostService.EXTRA_ACCOUNT, selectedAccount.getJSONModel()); extras.putString(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_GALLERY); ArrayList items = new ArrayList<>(); for (RedditGallerySubmissionRecyclerViewAdapter.RedditGalleryImageInfo i : redditGalleryImageInfoList) { items.add(i.payload); } RedditGalleryPayload payload = new RedditGalleryPayload(subredditName, subredditIsUser ? "profile" : "subreddit", binding.postTitleEditTextPostGalleryActivity.getText().toString(), binding.postContentEditTextPostGalleryActivity.getText().toString(), isSpoiler, isNSFW, binding.receivePostReplyNotificationsSwitchMaterialPostGalleryActivity.isChecked(), flair, items); String payloadJSON = new Gson().toJson(payload); extras.putString(SubmitPostService.EXTRA_REDDIT_GALLERY_PAYLOAD, payloadJSON); JobInfo jobInfo = SubmitPostService.constructJobInfo(this, payloadJSON.length() * 2L, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(SUBREDDIT_ICON_STATE, iconUrl); outState.putBoolean(SUBREDDIT_SELECTED_STATE, subredditSelected); outState.putBoolean(SUBREDDIT_IS_USER_STATE, subredditIsUser); outState.putBoolean(LOAD_SUBREDDIT_ICON_STATE, loadSubredditIconSuccessful); outState.putBoolean(IS_POSTING_STATE, isPosting); outState.putParcelable(FLAIR_STATE, flair); outState.putBoolean(IS_SPOILER_STATE, isSpoiler); outState.putBoolean(IS_NSFW_STATE, isNSFW); redditGalleryImageInfoList = adapter.getRedditGalleryImageInfoList(); outState.putParcelableArrayList(REDDIT_GALLERY_IMAGE_INFO_STATE, redditGalleryImageInfoList); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { if (resultCode == RESULT_OK) { subredditName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); iconUrl = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON); subredditSelected = true; subredditIsUser = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT) == SelectThingReturnKey.THING_TYPE.USER; binding.subredditNameTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostGalleryActivity.setText(subredditName); displaySubredditIcon(); binding.flairCustomTextViewPostGalleryActivity.setVisibility(View.VISIBLE); binding.flairCustomTextViewPostGalleryActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostGalleryActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostGalleryActivity.setText(getString(R.string.flair)); flair = null; } } else if (requestCode == PICK_IMAGE_REQUEST_CODE) { if (resultCode == RESULT_OK) { if (data == null) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.error_getting_image, Snackbar.LENGTH_SHORT).show(); return; } imageUri = data.getData(); adapter.addImage(imageUri.toString()); uploadImage(); } } else if (requestCode == CAPTURE_IMAGE_REQUEST_CODE) { if (resultCode == RESULT_OK) { adapter.addImage(imageUri.toString()); uploadImage(); } } } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void flairSelected(Flair flair) { this.flair = flair; binding.flairCustomTextViewPostGalleryActivity.setText(flair.getText()); binding.flairCustomTextViewPostGalleryActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostGalleryActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostGalleryActivity.setTextColor(flairTextColor); } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostGalleryActivity); binding.accountNameTextViewPostGalleryActivity.setText(selectedAccount.getAccountName()); } } public void setCaptionAndUrl(int position, String caption, String url) { if (adapter != null) { adapter.setCaptionAndUrl(position, caption, url); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onSubmitGalleryPostEvent(SubmitGalleryPostEvent submitGalleryPostEvent) { isPosting = false; mPostingSnackbar.dismiss(); if (submitGalleryPostEvent.postSuccess) { Intent intent = new Intent(this, LinkResolverActivity.class); intent.setData(Uri.parse(submitGalleryPostEvent.postUrl)); startActivity(intent); finish(); } else { mMemu.findItem(R.id.action_send_post_gallery_activity).setEnabled(true); mMemu.findItem(R.id.action_send_post_gallery_activity).getIcon().setAlpha(255); if (submitGalleryPostEvent.errorMessage == null || submitGalleryPostEvent.errorMessage.isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutPostGalleryActivity, submitGalleryPostEvent.errorMessage.substring(0, 1).toUpperCase() + submitGalleryPostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostImageActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.Manifest; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.PersistableBundle; import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.io.File; import java.io.IOException; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityPostImageBinding; import ml.docilealligator.infinityforreddit.events.SubmitImagePostEvent; import ml.docilealligator.infinityforreddit.events.SubmitVideoOrGifPostEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class PostImageActivity extends BaseActivity implements FlairBottomSheetFragment.FlairSelectionCallback, AccountChooserBottomSheetFragment.AccountChooserListener { static final String EXTRA_SUBREDDIT_NAME = "ESN"; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String SUBREDDIT_ICON_STATE = "SIS"; private static final String SUBREDDIT_SELECTED_STATE = "SSS"; private static final String SUBREDDIT_IS_USER_STATE = "SIUS"; private static final String IMAGE_URI_STATE = "IUS"; private static final String LOAD_SUBREDDIT_ICON_STATE = "LSIS"; private static final String IS_POSTING_STATE = "IPS"; private static final String FLAIR_STATE = "FS"; private static final String IS_SPOILER_STATE = "ISS"; private static final String IS_NSFW_STATE = "INS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 0; private static final int PICK_IMAGE_REQUEST_CODE = 1; private static final int CAPTURE_IMAGE_REQUEST_CODE = 2; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Account selectedAccount; private String iconUrl; private String subredditName; private boolean subredditSelected = false; private boolean subredditIsUser; private boolean loadSubredditIconSuccessful = true; private boolean isPosting; private Uri imageUri; private int primaryTextColor; private ActivityResultLauncher requestCameraPermissionLauncher; private int flairBackgroundColor; private int flairTextColor; private int spoilerBackgroundColor; private int spoilerTextColor; private int nsfwBackgroundColor; private int nsfwTextColor; private Flair flair; private boolean isSpoiler = false; private boolean isNSFW = false; private Resources resources; private Menu mMemu; private RequestManager mGlide; private FlairBottomSheetFragment flairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private ActivityPostImageBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); requestCameraPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) { captureImage(); } else { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.camera_permission_required, Snackbar.LENGTH_SHORT).show(); } }); binding = ActivityPostImageBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostImageActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarPostImageActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutPostImageActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostImageActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); mPostingSnackbar = Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.posting, Snackbar.LENGTH_INDEFINITE); resources = getResources(); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); iconUrl = savedInstanceState.getString(SUBREDDIT_ICON_STATE); subredditSelected = savedInstanceState.getBoolean(SUBREDDIT_SELECTED_STATE); subredditIsUser = savedInstanceState.getBoolean(SUBREDDIT_IS_USER_STATE); loadSubredditIconSuccessful = savedInstanceState.getBoolean(LOAD_SUBREDDIT_ICON_STATE); isPosting = savedInstanceState.getBoolean(IS_POSTING_STATE); flair = savedInstanceState.getParcelable(FLAIR_STATE); isSpoiler = savedInstanceState.getBoolean(IS_SPOILER_STATE); isNSFW = savedInstanceState.getBoolean(IS_NSFW_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostImageActivity); binding.accountNameTextViewPostImageActivity.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } if (savedInstanceState.getString(IMAGE_URI_STATE) != null) { imageUri = Uri.parse(savedInstanceState.getString(IMAGE_URI_STATE)); loadImage(); } if (subredditName != null) { binding.subredditNameTextViewPostImageActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostImageActivity.setText(subredditName); binding.flairCustomTextViewPostImageActivity.setVisibility(View.VISIBLE); if (!loadSubredditIconSuccessful) { loadSubredditIcon(); } } displaySubredditIcon(); if (isPosting) { mPostingSnackbar.show(); } if (flair != null) { binding.flairCustomTextViewPostImageActivity.setText(flair.getText()); binding.flairCustomTextViewPostImageActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostImageActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostImageActivity.setTextColor(flairTextColor); } if (isSpoiler) { binding.spoilerCustomTextViewPostImageActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostImageActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostImageActivity.setTextColor(spoilerTextColor); } if (isNSFW) { binding.nsfwCustomTextViewPostImageActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostImageActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostImageActivity.setTextColor(nsfwTextColor); } } else { isPosting = false; loadCurrentAccount(); if (getIntent().hasExtra(EXTRA_SUBREDDIT_NAME)) { loadSubredditIconSuccessful = false; subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); subredditSelected = true; binding.subredditNameTextViewPostImageActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostImageActivity.setText(subredditName); binding.flairCustomTextViewPostImageActivity.setVisibility(View.VISIBLE); loadSubredditIcon(); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostImageActivity); } imageUri = getIntent().getData(); if (imageUri != null) { loadImage(); } } binding.accountLinearLayoutPostImageActivity.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.subredditRelativeLayoutPostImageActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SPECIFIED_ACCOUNT, selectedAccount); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.rulesButtonPostImageActivity.setOnClickListener(view -> { if (subredditName == null) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, RulesActivity.class); if (subredditIsUser) { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, subredditName); } startActivity(intent); } }); binding.flairCustomTextViewPostImageActivity.setOnClickListener(view -> { if (flair == null) { flairSelectionBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, subredditName); flairSelectionBottomSheetFragment.setArguments(bundle); flairSelectionBottomSheetFragment.show(getSupportFragmentManager(), flairSelectionBottomSheetFragment.getTag()); } else { binding.flairCustomTextViewPostImageActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostImageActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostImageActivity.setText(getString(R.string.flair)); flair = null; } }); binding.spoilerCustomTextViewPostImageActivity.setOnClickListener(view -> { if (!isSpoiler) { binding.spoilerCustomTextViewPostImageActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostImageActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostImageActivity.setTextColor(spoilerTextColor); isSpoiler = true; } else { binding.spoilerCustomTextViewPostImageActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.spoilerCustomTextViewPostImageActivity.setTextColor(primaryTextColor); isSpoiler = false; } }); binding.nsfwCustomTextViewPostImageActivity.setOnClickListener(view -> { if (!isNSFW) { binding.nsfwCustomTextViewPostImageActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostImageActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostImageActivity.setTextColor(nsfwTextColor); isNSFW = true; } else { binding.nsfwCustomTextViewPostImageActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.nsfwCustomTextViewPostImageActivity.setTextColor(primaryTextColor); isNSFW = false; } }); binding.receivePostReplyNotificationsLinearLayoutPostImageActivity.setOnClickListener(view -> { binding.receivePostReplyNotificationsSwitchMaterialPostImageActivity.performClick(); }); binding.captureFabPostImageActivity.setOnClickListener(view -> { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { captureImage(); } else { requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA); } }); binding.selectFromLibraryFabPostImageActivity.setOnClickListener(view -> { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getString(R.string.select_from_gallery)), PICK_IMAGE_REQUEST_CODE); }); binding.selectAgainTextViewPostImageActivity.setOnClickListener(view -> { imageUri = null; binding.selectAgainTextViewPostImageActivity.setVisibility(View.GONE); mGlide.clear(binding.imageViewPostImageActivity); binding.imageViewPostImageActivity.setVisibility(View.GONE); binding.selectImageConstraintLayoutPostImageActivity.setVisibility(View.VISIBLE); }); MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( PostImageActivity.this, binding.postContentEditTextPostImageActivity, item); } @Override public void onUploadImage() { } }); binding.markdownBottomBarRecyclerViewPostImageActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManager.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewPostImageActivity.setAdapter(adapter); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isPosting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_submit_post_detail); } else { if (!binding.postTitleEditTextPostImageActivity.getText().toString().isEmpty() || !binding.postContentEditTextPostImageActivity.getText().toString().isEmpty() || imageUri != null) { promptAlertDialog(R.string.discard, R.string.discard_detail); } else { finish(); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostImageActivity); binding.accountNameTextViewPostImageActivity.setText(account.getAccountName()); } }); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutPostImageActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostImageActivity, null, binding.toolbarPostImageActivity); primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.accountNameTextViewPostImageActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.subredditNameTextViewPostImageActivity.setTextColor(secondaryTextColor); binding.rulesButtonPostImageActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.rulesButtonPostImageActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.receivePostReplyNotificationsTextViewPostImageActivity.setTextColor(primaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1PostImageActivity.setDividerColor(dividerColor); binding.divider2PostImageActivity.setDividerColor(dividerColor); flairBackgroundColor = mCustomThemeWrapper.getFlairBackgroundColor(); flairTextColor = mCustomThemeWrapper.getFlairTextColor(); spoilerBackgroundColor = mCustomThemeWrapper.getSpoilerBackgroundColor(); spoilerTextColor = mCustomThemeWrapper.getSpoilerTextColor(); nsfwBackgroundColor = mCustomThemeWrapper.getNsfwBackgroundColor(); nsfwTextColor = mCustomThemeWrapper.getNsfwTextColor(); binding.flairCustomTextViewPostImageActivity.setTextColor(primaryTextColor); binding.spoilerCustomTextViewPostImageActivity.setTextColor(primaryTextColor); binding.nsfwCustomTextViewPostImageActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostImageActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostImageActivity.setHintTextColor(secondaryTextColor); binding.postContentEditTextPostImageActivity.setTextColor(primaryTextColor); binding.postContentEditTextPostImageActivity.setHintTextColor(secondaryTextColor); applyFABTheme(binding.captureFabPostImageActivity); applyFABTheme(binding.selectFromLibraryFabPostImageActivity); binding.selectAgainTextViewPostImageActivity.setTextColor(mCustomThemeWrapper.getColorAccent()); if (typeface != null) { binding.subredditNameTextViewPostImageActivity.setTypeface(typeface); binding.rulesButtonPostImageActivity.setTypeface(typeface); binding.receivePostReplyNotificationsTextViewPostImageActivity.setTypeface(typeface); binding.flairCustomTextViewPostImageActivity.setTypeface(typeface); binding.spoilerCustomTextViewPostImageActivity.setTypeface(typeface); binding.nsfwCustomTextViewPostImageActivity.setTypeface(typeface); binding.postTitleEditTextPostImageActivity.setTypeface(typeface); binding.selectAgainTextViewPostImageActivity.setTypeface(typeface); } if (contentTypeface != null) { binding.postContentEditTextPostImageActivity.setTypeface(contentTypeface); } } private void loadImage() { binding.selectImageConstraintLayoutPostImageActivity.setVisibility(View.GONE); binding.imageViewPostImageActivity.setVisibility(View.VISIBLE); binding.selectAgainTextViewPostImageActivity.setVisibility(View.VISIBLE); mGlide.load(imageUri).into(binding.imageViewPostImageActivity); } private void displaySubredditIcon() { if (iconUrl != null && !iconUrl.isEmpty()) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.subredditIconGifImageViewPostImageActivity); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostImageActivity); } } private void loadSubredditIcon() { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accessToken, accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { iconUrl = iconImageUrl; displaySubredditIcon(); loadSubredditIconSuccessful = true; }); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.post_image_activity, menu); applyMenuItemTheme(menu); mMemu = menu; if (isPosting) { mMemu.findItem(R.id.action_send_post_image_activity).setEnabled(false); mMemu.findItem(R.id.action_send_post_image_activity).getIcon().setAlpha(130); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_send_post_image_activity) { if (!subredditSelected) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); return true; } if (binding.postTitleEditTextPostImageActivity.getText() == null || binding.postTitleEditTextPostImageActivity.getText().toString().isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.title_required, Snackbar.LENGTH_SHORT).show(); return true; } if (imageUri == null) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.select_an_image, Snackbar.LENGTH_SHORT).show(); return true; } isPosting = true; item.setEnabled(false); item.getIcon().setAlpha(130); mPostingSnackbar.show(); String subredditName; if (subredditIsUser) { subredditName = "u_" + binding.subredditNameTextViewPostImageActivity.getText().toString(); } else { subredditName = binding.subredditNameTextViewPostImageActivity.getText().toString(); } /*Intent intent = new Intent(this, SubmitPostService.class); intent.putExtra(SubmitPostService.EXTRA_MEDIA_URI, imageUri.toString()); intent.putExtra(SubmitPostService.EXTRA_ACCOUNT, selectedAccount); intent.putExtra(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); intent.putExtra(SubmitPostService.EXTRA_TITLE, binding.postTitleEditTextPostImageActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_CONTENT, binding.postContentEditTextPostImageActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_FLAIR, flair); intent.putExtra(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler); intent.putExtra(SubmitPostService.EXTRA_IS_NSFW, isNSFW); intent.putExtra(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostImageActivity.isChecked()); String mimeType = getContentResolver().getType(imageUri); boolean isGif = false; // Check MIME type first if (mimeType != null && mimeType.contains("gif")) { isGif = true; } // Fallback: check file extension if MIME type detection fails if (!isGif && imageUri.toString().toLowerCase().endsWith(".gif")) { isGif = true; } if (isGif) { intent.putExtra(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_VIDEO); } else { intent.putExtra(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_IMAGE); } intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); ContextCompat.startForegroundService(this, intent);*/ int contentEstimatedBytes = 0; PersistableBundle extras = new PersistableBundle(); //TODO estimate image size extras.putString(SubmitPostService.EXTRA_MEDIA_URI, imageUri.toString()); extras.putString(SubmitPostService.EXTRA_ACCOUNT, selectedAccount.getJSONModel()); extras.putString(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); String title = binding.postTitleEditTextPostImageActivity.getText().toString(); contentEstimatedBytes += title.length() * 2; extras.putString(SubmitPostService.EXTRA_TITLE, title); String content = binding.postContentEditTextPostImageActivity.getText().toString(); contentEstimatedBytes += content.length() * 2; extras.putString(SubmitPostService.EXTRA_CONTENT, content); if (flair != null) { extras.putString(SubmitPostService.EXTRA_FLAIR, flair.getJSONModel()); } extras.putInt(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_IS_NSFW, isNSFW ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostImageActivity.isChecked() ? 1 : 0); String mimeType = getContentResolver().getType(imageUri); boolean isGif = false; // Check MIME type first if (mimeType != null && mimeType.contains("gif")) { isGif = true; } // Fallback: check file extension if MIME type detection fails if (!isGif && imageUri.toString().toLowerCase().endsWith(".gif")) { isGif = true; } if (isGif) { extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_VIDEO); } else { extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_IMAGE); } // TODO: contentEstimatedBytes JobInfo jobInfo = SubmitPostService.constructJobInfo(this, contentEstimatedBytes, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(SUBREDDIT_ICON_STATE, iconUrl); outState.putBoolean(SUBREDDIT_SELECTED_STATE, subredditSelected); outState.putBoolean(SUBREDDIT_IS_USER_STATE, subredditIsUser); if (imageUri != null) { outState.putString(IMAGE_URI_STATE, imageUri.toString()); } outState.putBoolean(LOAD_SUBREDDIT_ICON_STATE, loadSubredditIconSuccessful); outState.putBoolean(IS_POSTING_STATE, isPosting); outState.putParcelable(FLAIR_STATE, flair); outState.putBoolean(IS_SPOILER_STATE, isSpoiler); outState.putBoolean(IS_NSFW_STATE, isNSFW); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { if (resultCode == RESULT_OK) { subredditName = data.getExtras().getString(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); iconUrl = data.getExtras().getString(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON); subredditSelected = true; subredditIsUser = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT) == SelectThingReturnKey.THING_TYPE.USER; binding.subredditNameTextViewPostImageActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostImageActivity.setText(subredditName); displaySubredditIcon(); binding.flairCustomTextViewPostImageActivity.setVisibility(View.VISIBLE); binding.flairCustomTextViewPostImageActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostImageActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostImageActivity.setText(getString(R.string.flair)); flair = null; } } else if (requestCode == PICK_IMAGE_REQUEST_CODE) { if (resultCode == RESULT_OK) { if (data == null) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.error_getting_image, Snackbar.LENGTH_SHORT).show(); return; } imageUri = data.getData(); loadImage(); } } else if (requestCode == CAPTURE_IMAGE_REQUEST_CODE) { if (resultCode == RESULT_OK) { loadImage(); } } } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void flairSelected(Flair flair) { this.flair = flair; binding.flairCustomTextViewPostImageActivity.setText(flair.getText()); binding.flairCustomTextViewPostImageActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostImageActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostImageActivity.setTextColor(flairTextColor); } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostImageActivity); binding.accountNameTextViewPostImageActivity.setText(selectedAccount.getAccountName()); } } private void captureImage() { Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { imageUri = FileProvider.getUriForFile(this, getPackageName() + ".provider", File.createTempFile("temp_img", ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES))); pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); startActivityForResult(pictureIntent, CAPTURE_IMAGE_REQUEST_CODE); } catch (IOException ex) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.error_creating_temp_file, Snackbar.LENGTH_SHORT).show(); } catch (ActivityNotFoundException e) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.no_camera_available, Snackbar.LENGTH_SHORT).show(); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onSubmitImagePostEvent(SubmitImagePostEvent submitImagePostEvent) { isPosting = false; mPostingSnackbar.dismiss(); if (submitImagePostEvent.postSuccess) { Intent intent = new Intent(PostImageActivity.this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); startActivity(intent); finish(); } else { mMemu.findItem(R.id.action_send_post_image_activity).setEnabled(true); mMemu.findItem(R.id.action_send_post_image_activity).getIcon().setAlpha(255); if (submitImagePostEvent.errorMessage == null || submitImagePostEvent.errorMessage.isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutPostImageActivity, submitImagePostEvent.errorMessage.substring(0, 1).toUpperCase() + submitImagePostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } @Subscribe public void onSubmitGifPostEvent(SubmitVideoOrGifPostEvent submitVideoOrGifPostEvent) { isPosting = false; mPostingSnackbar.dismiss(); mMemu.findItem(R.id.action_send_post_image_activity).setEnabled(true); mMemu.findItem(R.id.action_send_post_image_activity).getIcon().setAlpha(255); if (submitVideoOrGifPostEvent.postSuccess) { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); startActivity(intent); finish(); } else if (submitVideoOrGifPostEvent.errorProcessingVideoOrGif) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.error_processing_image, Snackbar.LENGTH_SHORT).show(); } else { if (submitVideoOrGifPostEvent.errorMessage == null || submitVideoOrGifPostEvent.errorMessage.isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostImageActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutPostImageActivity, submitVideoOrGifPostEvent.errorMessage.substring(0, 1).toUpperCase() + submitVideoOrGifPostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostLinkActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.os.PersistableBundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.webkit.URLUtil; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.TitleSuggestion; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityPostLinkBinding; import ml.docilealligator.infinityforreddit.events.SubmitTextOrLinkPostEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.scalars.ScalarsConverterFactory; public class PostLinkActivity extends BaseActivity implements FlairBottomSheetFragment.FlairSelectionCallback, AccountChooserBottomSheetFragment.AccountChooserListener { static final String EXTRA_SUBREDDIT_NAME = "ESN"; static final String EXTRA_LINK = "EL"; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String SUBREDDIT_ICON_STATE = "SIS"; private static final String SUBREDDIT_SELECTED_STATE = "SSS"; private static final String SUBREDDIT_IS_USER_STATE = "SIUS"; private static final String LOAD_SUBREDDIT_ICON_STATE = "LSIS"; private static final String IS_POSTING_STATE = "IPS"; private static final String FLAIR_STATE = "FS"; private static final String IS_SPOILER_STATE = "ISS"; private static final String IS_NSFW_STATE = "INS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 0; private static final int MARKDOWN_PREVIEW_REQUEST_CODE = 300; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Account selectedAccount; private String iconUrl; private String subredditName; private boolean subredditSelected = false; private boolean subredditIsUser; private boolean loadSubredditIconSuccessful = true; private boolean isPosting; private int primaryTextColor; private int flairBackgroundColor; private int flairTextColor; private int spoilerBackgroundColor; private int spoilerTextColor; private int nsfwBackgroundColor; private int nsfwTextColor; private Flair flair; private boolean isSpoiler = false; private boolean isNSFW = false; private Resources resources; private Menu mMemu; private RequestManager mGlide; private FlairBottomSheetFragment flairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private ActivityPostLinkBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityPostLinkBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostLinkActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarPostLinkActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutPostLinkActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostLinkActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); mPostingSnackbar = Snackbar.make(binding.coordinatorLayoutPostLinkActivity, R.string.posting, Snackbar.LENGTH_INDEFINITE); resources = getResources(); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); iconUrl = savedInstanceState.getString(SUBREDDIT_ICON_STATE); subredditSelected = savedInstanceState.getBoolean(SUBREDDIT_SELECTED_STATE); subredditIsUser = savedInstanceState.getBoolean(SUBREDDIT_IS_USER_STATE); loadSubredditIconSuccessful = savedInstanceState.getBoolean(LOAD_SUBREDDIT_ICON_STATE); isPosting = savedInstanceState.getBoolean(IS_POSTING_STATE); flair = savedInstanceState.getParcelable(FLAIR_STATE); isSpoiler = savedInstanceState.getBoolean(IS_SPOILER_STATE); isNSFW = savedInstanceState.getBoolean(IS_NSFW_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostLinkActivity); binding.accountNameTextViewPostLinkActivity.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } if (subredditName != null) { binding.subredditNameTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostLinkActivity.setText(subredditName); binding.flairCustomTextViewPostLinkActivity.setVisibility(View.VISIBLE); if (!loadSubredditIconSuccessful) { loadSubredditIcon(); } } displaySubredditIcon(); if (isPosting) { mPostingSnackbar.show(); } if (flair != null) { binding.flairCustomTextViewPostLinkActivity.setText(flair.getText()); binding.flairCustomTextViewPostLinkActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostLinkActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostLinkActivity.setTextColor(flairTextColor); } if (isSpoiler) { binding.spoilerCustomTextViewPostLinkActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostLinkActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostLinkActivity.setTextColor(spoilerTextColor); } if (isNSFW) { binding.nsfwCustomTextViewPostLinkActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostLinkActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostLinkActivity.setTextColor(nsfwTextColor); } } else { isPosting = false; loadCurrentAccount(); if (getIntent().hasExtra(EXTRA_SUBREDDIT_NAME)) { loadSubredditIconSuccessful = false; subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); subredditSelected = true; binding.subredditNameTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostLinkActivity.setText(subredditName); binding.flairCustomTextViewPostLinkActivity.setVisibility(View.VISIBLE); loadSubredditIcon(); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostLinkActivity); } String link = getIntent().getStringExtra(EXTRA_LINK); if (link != null) { binding.postLinkEditTextPostLinkActivity.setText(link); } } binding.accountLinearLayoutPostLinkActivity.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.subredditRelativeLayoutPostLinkActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SPECIFIED_ACCOUNT, selectedAccount); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.rulesButtonPostLinkActivity.setOnClickListener(view -> { if (subredditName == null) { Snackbar.make(binding.coordinatorLayoutPostLinkActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, RulesActivity.class); if (subredditIsUser) { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, subredditName); } startActivity(intent); } }); binding.flairCustomTextViewPostLinkActivity.setOnClickListener(view -> { if (flair == null) { flairSelectionBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, subredditName); flairSelectionBottomSheetFragment.setArguments(bundle); flairSelectionBottomSheetFragment.show(getSupportFragmentManager(), flairSelectionBottomSheetFragment.getTag()); } else { binding.flairCustomTextViewPostLinkActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostLinkActivity.setText(getString(R.string.flair)); flair = null; } }); binding.spoilerCustomTextViewPostLinkActivity.setOnClickListener(view -> { if (!isSpoiler) { binding.spoilerCustomTextViewPostLinkActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostLinkActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostLinkActivity.setTextColor(spoilerTextColor); isSpoiler = true; } else { binding.spoilerCustomTextViewPostLinkActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.spoilerCustomTextViewPostLinkActivity.setTextColor(primaryTextColor); isSpoiler = false; } }); binding.nsfwCustomTextViewPostLinkActivity.setOnClickListener(view -> { if (!isNSFW) { binding.nsfwCustomTextViewPostLinkActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostLinkActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostLinkActivity.setTextColor(nsfwTextColor); isNSFW = true; } else { binding.nsfwCustomTextViewPostLinkActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.nsfwCustomTextViewPostLinkActivity.setTextColor(primaryTextColor); isNSFW = false; } }); binding.receivePostReplyNotificationsLinearLayoutPostLinkActivity.setOnClickListener(view -> { binding.receivePostReplyNotificationsSwitchMaterialPostLinkActivity.performClick(); }); binding.suggestTitleButtonPostLinkActivity.setOnClickListener(view -> { Toast.makeText(this, R.string.please_wait, Toast.LENGTH_SHORT).show(); String url = binding.postLinkEditTextPostLinkActivity.getText().toString().trim(); if (!URLUtil.isHttpsUrl(url) && !URLUtil.isHttpUrl(url)) { url = "https://" + url; } mRetrofit.newBuilder() .baseUrl("http://localhost/") .addConverterFactory(ScalarsConverterFactory.create()) .build().create(TitleSuggestion.class).getHtml(url).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { String body = response.body(); if (body != null) { int start = body.indexOf(""); if (start >= 0) { int end = body.indexOf(""); if (end > start) { binding.postTitleEditTextPostLinkActivity.setText(body.substring(start + 7, end)); return; } } } Toast.makeText(PostLinkActivity.this, R.string.suggest_title_failed, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(PostLinkActivity.this, R.string.suggest_title_failed, Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { Toast.makeText(PostLinkActivity.this, R.string.suggest_title_failed, Toast.LENGTH_SHORT).show(); } }); }); MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( PostLinkActivity.this, binding.postContentEditTextPostLinkActivity, item); } @Override public void onUploadImage() { } }); binding.markdownBottomBarRecyclerViewPostLinkActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManager.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewPostLinkActivity.setAdapter(adapter); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isPosting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_submit_post_detail); } else { if (!binding.postTitleEditTextPostLinkActivity.getText().toString().isEmpty() || !binding.postContentEditTextPostLinkActivity.getText().toString().isEmpty() || !binding.postLinkEditTextPostLinkActivity.getText().toString().isEmpty()) { promptAlertDialog(R.string.discard, R.string.discard_detail); } else { setEnabled(false); triggerBackPress(); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostLinkActivity); binding.accountNameTextViewPostLinkActivity.setText(account.getAccountName()); } }); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutPostLinkActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostLinkActivity, null, binding.toolbarPostLinkActivity); primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.accountNameTextViewPostLinkActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.subredditNameTextViewPostLinkActivity.setTextColor(secondaryTextColor); binding.rulesButtonPostLinkActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.rulesButtonPostLinkActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.receivePostReplyNotificationsTextViewPostLinkActivity.setTextColor(primaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1PostLinkActivity.setDividerColor(dividerColor); binding.divider2PostLinkActivity.setDividerColor(dividerColor); flairBackgroundColor = mCustomThemeWrapper.getFlairBackgroundColor(); flairTextColor = mCustomThemeWrapper.getFlairTextColor(); spoilerBackgroundColor = mCustomThemeWrapper.getSpoilerBackgroundColor(); spoilerTextColor = mCustomThemeWrapper.getSpoilerTextColor(); nsfwBackgroundColor = mCustomThemeWrapper.getNsfwBackgroundColor(); nsfwTextColor = mCustomThemeWrapper.getNsfwTextColor(); binding.flairCustomTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.spoilerCustomTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.nsfwCustomTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostLinkActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostLinkActivity.setHintTextColor(secondaryTextColor); binding.suggestTitleButtonPostLinkActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.suggestTitleButtonPostLinkActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.postLinkEditTextPostLinkActivity.setTextColor(primaryTextColor); binding.postLinkEditTextPostLinkActivity.setHintTextColor(secondaryTextColor); binding.postContentEditTextPostLinkActivity.setTextColor(primaryTextColor); binding.postContentEditTextPostLinkActivity.setHintTextColor(secondaryTextColor); if (typeface != null) { binding.subredditNameTextViewPostLinkActivity.setTypeface(typeface); binding.rulesButtonPostLinkActivity.setTypeface(typeface); binding.receivePostReplyNotificationsTextViewPostLinkActivity.setTypeface(typeface); binding.flairCustomTextViewPostLinkActivity.setTypeface(typeface); binding.spoilerCustomTextViewPostLinkActivity.setTypeface(typeface); binding.nsfwCustomTextViewPostLinkActivity.setTypeface(typeface); binding.postTitleEditTextPostLinkActivity.setTypeface(typeface); binding.suggestTitleButtonPostLinkActivity.setTypeface(typeface); } if (contentTypeface != null) { binding.postContentEditTextPostLinkActivity.setTypeface(contentTypeface); binding.postLinkEditTextPostLinkActivity.setTypeface(contentTypeface); } } private void displaySubredditIcon() { if (iconUrl != null && !iconUrl.isEmpty()) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.subredditIconGifImageViewPostLinkActivity); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostLinkActivity); } } private void loadSubredditIcon() { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accessToken, accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { iconUrl = iconImageUrl; displaySubredditIcon(); loadSubredditIconSuccessful = true; }); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.post_link_activity, menu); applyMenuItemTheme(menu); mMemu = menu; if (isPosting) { mMemu.findItem(R.id.action_send_post_link_activity).setEnabled(false); mMemu.findItem(R.id.action_send_post_link_activity).getIcon().setAlpha(130); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_send_post_link_activity) { if (!subredditSelected) { Snackbar.make(binding.coordinatorLayoutPostLinkActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); return true; } if (binding.postTitleEditTextPostLinkActivity.getText() == null || binding.postTitleEditTextPostLinkActivity.getText().toString().isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostLinkActivity, R.string.title_required, Snackbar.LENGTH_SHORT).show(); return true; } if (binding.postLinkEditTextPostLinkActivity.getText() == null || binding.postLinkEditTextPostLinkActivity.getText().toString().isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostLinkActivity, R.string.link_required, Snackbar.LENGTH_SHORT).show(); return true; } isPosting = true; item.setEnabled(false); item.getIcon().setAlpha(130); mPostingSnackbar.show(); String subredditName; if (subredditIsUser) { subredditName = "u_" + binding.subredditNameTextViewPostLinkActivity.getText().toString(); } else { subredditName = binding.subredditNameTextViewPostLinkActivity.getText().toString(); } /*Intent intent = new Intent(this, SubmitPostService.class); intent.putExtra(SubmitPostService.EXTRA_ACCOUNT, selectedAccount); intent.putExtra(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); intent.putExtra(SubmitPostService.EXTRA_TITLE, binding.postTitleEditTextPostLinkActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_CONTENT, binding.postContentEditTextPostLinkActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_URL, binding.postLinkEditTextPostLinkActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_KIND, APIUtils.KIND_LINK); intent.putExtra(SubmitPostService.EXTRA_FLAIR, flair); intent.putExtra(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler); intent.putExtra(SubmitPostService.EXTRA_IS_NSFW, isNSFW); intent.putExtra(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostLinkActivity.isChecked()); intent.putExtra(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TEXT_OR_LINK); ContextCompat.startForegroundService(this, intent);*/ int contentEstimatedBytes = 0; PersistableBundle extras = new PersistableBundle(); extras.putString(SubmitPostService.EXTRA_ACCOUNT, selectedAccount.getJSONModel()); extras.putString(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); String title = binding.postTitleEditTextPostLinkActivity.getText().toString(); contentEstimatedBytes += title.length() * 2; extras.putString(SubmitPostService.EXTRA_TITLE, title); String content = binding.postContentEditTextPostLinkActivity.getText().toString(); contentEstimatedBytes += content.length() * 2; extras.putString(SubmitPostService.EXTRA_CONTENT, content); String link = binding.postLinkEditTextPostLinkActivity.getText().toString(); contentEstimatedBytes += link.length() * 2; extras.putString(SubmitPostService.EXTRA_URL, link); extras.putString(SubmitPostService.EXTRA_KIND, APIUtils.KIND_LINK); if (flair != null) { extras.putString(SubmitPostService.EXTRA_FLAIR, flair.getJSONModel()); } extras.putInt(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_IS_NSFW, isNSFW ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostLinkActivity.isChecked() ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TEXT_OR_LINK); JobInfo jobInfo = SubmitPostService.constructJobInfo(this, contentEstimatedBytes, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(SUBREDDIT_ICON_STATE, iconUrl); outState.putBoolean(SUBREDDIT_SELECTED_STATE, subredditSelected); outState.putBoolean(SUBREDDIT_IS_USER_STATE, subredditIsUser); outState.putBoolean(LOAD_SUBREDDIT_ICON_STATE, loadSubredditIconSuccessful); outState.putBoolean(IS_POSTING_STATE, isPosting); outState.putParcelable(FLAIR_STATE, flair); outState.putBoolean(IS_SPOILER_STATE, isSpoiler); outState.putBoolean(IS_NSFW_STATE, isNSFW); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { subredditName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); iconUrl = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON); subredditSelected = true; subredditIsUser = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT) == SelectThingReturnKey.THING_TYPE.USER; binding.subredditNameTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostLinkActivity.setText(subredditName); displaySubredditIcon(); binding.flairCustomTextViewPostLinkActivity.setVisibility(View.VISIBLE); binding.flairCustomTextViewPostLinkActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostLinkActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostLinkActivity.setText(getString(R.string.flair)); flair = null; }/* else if (requestCode == MARKDOWN_PREVIEW_REQUEST_CODE) { submitPost(mMenu.findItem(R.id.action_send_post_text_activity)); }*/ } } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void flairSelected(Flair flair) { this.flair = flair; binding.flairCustomTextViewPostLinkActivity.setText(flair.getText()); binding.flairCustomTextViewPostLinkActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostLinkActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostLinkActivity.setTextColor(flairTextColor); } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostLinkActivity); binding.accountNameTextViewPostLinkActivity.setText(selectedAccount.getAccountName()); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onSubmitLinkPostEvent(SubmitTextOrLinkPostEvent submitTextOrLinkPostEvent) { isPosting = false; mPostingSnackbar.dismiss(); if (submitTextOrLinkPostEvent.postSuccess) { Intent intent = new Intent(PostLinkActivity.this, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_DATA, submitTextOrLinkPostEvent.post); startActivity(intent); finish(); } else { mMemu.findItem(R.id.action_send_post_link_activity).setEnabled(true); mMemu.findItem(R.id.action_send_post_link_activity).getIcon().setAlpha(255); if (submitTextOrLinkPostEvent.errorMessage == null || submitTextOrLinkPostEvent.errorMessage.isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostLinkActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutPostLinkActivity, submitTextOrLinkPostEvent.errorMessage.substring(0, 1).toUpperCase() + submitTextOrLinkPostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostPollActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.PersistableBundle; import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.gson.Gson; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.json.JSONException; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.post.PollPayload; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UploadedImagesBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityPostPollBinding; import ml.docilealligator.infinityforreddit.events.SubmitPollPostEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.markdown.RichTextJSONConverter; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class PostPollActivity extends BaseActivity implements FlairBottomSheetFragment.FlairSelectionCallback, UploadImageEnabledActivity, AccountChooserBottomSheetFragment.AccountChooserListener { static final String EXTRA_SUBREDDIT_NAME = "ESN"; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String SUBREDDIT_ICON_STATE = "SIS"; private static final String SUBREDDIT_SELECTED_STATE = "SSS"; private static final String SUBREDDIT_IS_USER_STATE = "SIUS"; private static final String LOAD_SUBREDDIT_ICON_STATE = "LSIS"; private static final String IS_POSTING_STATE = "IPS"; private static final String FLAIR_STATE = "FS"; private static final String IS_SPOILER_STATE = "ISS"; private static final String IS_NSFW_STATE = "INS"; private static final String UPLOADED_IMAGES_STATE = "UIS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 0; private static final int PICK_IMAGE_REQUEST_CODE = 100; private static final int CAPTURE_IMAGE_REQUEST_CODE = 200; private static final int MARKDOWN_PREVIEW_REQUEST_CODE = 300; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Account selectedAccount; private String iconUrl; private String subredditName; private boolean subredditSelected = false; private boolean subredditIsUser; private boolean loadSubredditIconSuccessful = true; private boolean isPosting; private int primaryTextColor; private int flairBackgroundColor; private int flairTextColor; private int spoilerBackgroundColor; private int spoilerTextColor; private int nsfwBackgroundColor; private int nsfwTextColor; private Flair flair; private boolean isSpoiler = false; private boolean isNSFW = false; private Resources resources; private Menu mMenu; private RequestManager mGlide; private FlairBottomSheetFragment flairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private Uri capturedImageUri; private ArrayList uploadedImages = new ArrayList<>(); private ActivityPostPollBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityPostPollBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostPollActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarPostPollActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutPostPollActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostPollActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); mPostingSnackbar = Snackbar.make(binding.coordinatorLayoutPostPollActivity, R.string.posting, Snackbar.LENGTH_INDEFINITE); resources = getResources(); Resources resources = getResources(); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); iconUrl = savedInstanceState.getString(SUBREDDIT_ICON_STATE); subredditSelected = savedInstanceState.getBoolean(SUBREDDIT_SELECTED_STATE); subredditIsUser = savedInstanceState.getBoolean(SUBREDDIT_IS_USER_STATE); loadSubredditIconSuccessful = savedInstanceState.getBoolean(LOAD_SUBREDDIT_ICON_STATE); isPosting = savedInstanceState.getBoolean(IS_POSTING_STATE); flair = savedInstanceState.getParcelable(FLAIR_STATE); isSpoiler = savedInstanceState.getBoolean(IS_SPOILER_STATE); isNSFW = savedInstanceState.getBoolean(IS_NSFW_STATE); uploadedImages = savedInstanceState.getParcelableArrayList(UPLOADED_IMAGES_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostPollActivity); binding.accountNameTextViewPostPollActivity.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } if (subredditName != null) { binding.subredditNameTextViewPostPollActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostPollActivity.setText(subredditName); binding.flairCustomTextViewPostPollActivity.setVisibility(View.VISIBLE); if (!loadSubredditIconSuccessful) { loadSubredditIcon(); } } displaySubredditIcon(); if (isPosting) { mPostingSnackbar.show(); } if (flair != null) { binding.flairCustomTextViewPostPollActivity.setText(flair.getText()); binding.flairCustomTextViewPostPollActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostPollActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostPollActivity.setTextColor(flairTextColor); } if (isSpoiler) { binding.spoilerCustomTextViewPostPollActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostPollActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostPollActivity.setTextColor(spoilerTextColor); } if (isNSFW) { binding.nsfwCustomTextViewPostPollActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostPollActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostPollActivity.setTextColor(nsfwTextColor); } } else { isPosting = false; loadCurrentAccount(); if (getIntent().hasExtra(EXTRA_SUBREDDIT_NAME)) { loadSubredditIconSuccessful = false; subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); subredditSelected = true; binding.subredditNameTextViewPostPollActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostPollActivity.setText(subredditName); binding.flairCustomTextViewPostPollActivity.setVisibility(View.VISIBLE); loadSubredditIcon(); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostPollActivity); } } binding.accountLinearLayoutPostPollActivity.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.subredditRelativeLayoutPostPollActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SPECIFIED_ACCOUNT, selectedAccount); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.rulesButtonPostPollActivity.setOnClickListener(view -> { if (subredditName == null) { Snackbar.make(binding.coordinatorLayoutPostPollActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, RulesActivity.class); if (subredditIsUser) { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, subredditName); } startActivity(intent); } }); binding.flairCustomTextViewPostPollActivity.setOnClickListener(view -> { if (flair == null) { flairSelectionBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, subredditName); flairSelectionBottomSheetFragment.setArguments(bundle); flairSelectionBottomSheetFragment.show(getSupportFragmentManager(), flairSelectionBottomSheetFragment.getTag()); } else { binding.flairCustomTextViewPostPollActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostPollActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostPollActivity.setText(getString(R.string.flair)); flair = null; } }); binding.spoilerCustomTextViewPostPollActivity.setOnClickListener(view -> { if (!isSpoiler) { binding.spoilerCustomTextViewPostPollActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostPollActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostPollActivity.setTextColor(spoilerTextColor); isSpoiler = true; } else { binding.spoilerCustomTextViewPostPollActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.spoilerCustomTextViewPostPollActivity.setTextColor(primaryTextColor); isSpoiler = false; } }); binding.nsfwCustomTextViewPostPollActivity.setOnClickListener(view -> { if (!isNSFW) { binding.nsfwCustomTextViewPostPollActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostPollActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostPollActivity.setTextColor(nsfwTextColor); isNSFW = true; } else { binding.nsfwCustomTextViewPostPollActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.nsfwCustomTextViewPostPollActivity.setTextColor(primaryTextColor); isNSFW = false; } }); binding.receivePostReplyNotificationsLinearLayoutPostPollActivity.setOnClickListener(view -> { binding.receivePostReplyNotificationsSwitchMaterialPostPollActivity.performClick(); }); binding.votingLengthTextViewPostPollActivity.setText(getString(R.string.voting_length, (int) binding.votingLengthSliderPostPollActivity.getValue())); binding.votingLengthSliderPostPollActivity.addOnChangeListener((slider, value, fromUser) -> binding.votingLengthTextViewPostPollActivity.setText(getString(R.string.voting_length, (int) value))); MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, true, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( PostPollActivity.this, binding.postContentEditTextPostPollActivity, item); } @Override public void onUploadImage() { Utils.hideKeyboard(PostPollActivity.this); UploadedImagesBottomSheetFragment fragment = new UploadedImagesBottomSheetFragment(); Bundle arguments = new Bundle(); arguments.putParcelableArrayList(UploadedImagesBottomSheetFragment.EXTRA_UPLOADED_IMAGES, uploadedImages); fragment.setArguments(arguments); fragment.show(getSupportFragmentManager(), fragment.getTag()); } }); binding.markdownBottomBarRecyclerViewPostPollActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManager.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewPostPollActivity.setAdapter(adapter); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isPosting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_submit_post_detail); } else { if (!binding.postTitleEditTextPostPollActivity.getText().toString().isEmpty() || !binding.postContentEditTextPostPollActivity.getText().toString().isEmpty() || !binding.option1TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty() || !binding.option2TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty() || !binding.option3TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty() || !binding.option4TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty() || !binding.option5TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty() || !binding.option6TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty()) { promptAlertDialog(R.string.discard, R.string.discard_detail); } else { setEnabled(false); triggerBackPress(); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostPollActivity); binding.accountNameTextViewPostPollActivity.setText(account.getAccountName()); } }); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutPostPollActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostPollActivity, null, binding.toolbarPostPollActivity); primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.accountNameTextViewPostPollActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.subredditNameTextViewPostPollActivity.setTextColor(secondaryTextColor); binding.rulesButtonPostPollActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.rulesButtonPostPollActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.receivePostReplyNotificationsTextViewPostPollActivity.setTextColor(primaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1PostPollActivity.setDividerColor(dividerColor); binding.divider2PostPollActivity.setDividerColor(dividerColor); binding.divider3PostPollActivity.setDividerColor(dividerColor); flairBackgroundColor = mCustomThemeWrapper.getFlairBackgroundColor(); flairTextColor = mCustomThemeWrapper.getFlairTextColor(); spoilerBackgroundColor = mCustomThemeWrapper.getSpoilerBackgroundColor(); spoilerTextColor = mCustomThemeWrapper.getSpoilerTextColor(); nsfwBackgroundColor = mCustomThemeWrapper.getNsfwBackgroundColor(); nsfwTextColor = mCustomThemeWrapper.getNsfwTextColor(); binding.flairCustomTextViewPostPollActivity.setTextColor(primaryTextColor); binding.spoilerCustomTextViewPostPollActivity.setTextColor(primaryTextColor); binding.nsfwCustomTextViewPostPollActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostPollActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostPollActivity.setHintTextColor(secondaryTextColor); binding.postContentEditTextPostPollActivity.setTextColor(primaryTextColor); binding.postContentEditTextPostPollActivity.setHintTextColor(secondaryTextColor); binding.votingLengthTextViewPostPollActivity.setTextColor(secondaryTextColor); binding.option1TextInputLayoutPostPollActivity.setBoxStrokeColor(primaryTextColor); binding.option1TextInputLayoutPostPollActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.option1TextInputLayoutEditTextPostPollActivity.setTextColor(primaryTextColor); binding.option2TextInputLayoutPostPollActivity.setBoxStrokeColor(primaryTextColor); binding.option2TextInputLayoutPostPollActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.option2TextInputLayoutEditTextPostPollActivity.setTextColor(primaryTextColor); binding.option3TextInputLayoutPostPollActivity.setBoxStrokeColor(primaryTextColor); binding.option3TextInputLayoutPostPollActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.option3TextInputLayoutEditTextPostPollActivity.setTextColor(primaryTextColor); binding.option4TextInputLayoutPostPollActivity.setBoxStrokeColor(primaryTextColor); binding.option4TextInputLayoutPostPollActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.option4TextInputLayoutEditTextPostPollActivity.setTextColor(primaryTextColor); binding.option5TextInputLayoutPostPollActivity.setBoxStrokeColor(primaryTextColor); binding.option5TextInputLayoutPostPollActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.option5TextInputLayoutEditTextPostPollActivity.setTextColor(primaryTextColor); binding.option6TextInputLayoutPostPollActivity.setBoxStrokeColor(primaryTextColor); binding.option6TextInputLayoutPostPollActivity.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.option6TextInputLayoutEditTextPostPollActivity.setTextColor(primaryTextColor); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { binding.option1TextInputLayoutPostPollActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.option2TextInputLayoutPostPollActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.option3TextInputLayoutPostPollActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.option4TextInputLayoutPostPollActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.option5TextInputLayoutPostPollActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.option6TextInputLayoutPostPollActivity.setCursorColor(ColorStateList.valueOf(primaryTextColor)); } else { setCursorDrawableColor(binding.option1TextInputLayoutEditTextPostPollActivity, primaryTextColor); setCursorDrawableColor(binding.option2TextInputLayoutEditTextPostPollActivity, primaryTextColor); setCursorDrawableColor(binding.option3TextInputLayoutEditTextPostPollActivity, primaryTextColor); setCursorDrawableColor(binding.option4TextInputLayoutEditTextPostPollActivity, primaryTextColor); setCursorDrawableColor(binding.option5TextInputLayoutEditTextPostPollActivity, primaryTextColor); setCursorDrawableColor(binding.option6TextInputLayoutEditTextPostPollActivity, primaryTextColor); } if (typeface != null) { binding.subredditNameTextViewPostPollActivity.setTypeface(typeface); binding.rulesButtonPostPollActivity.setTypeface(typeface); binding.receivePostReplyNotificationsTextViewPostPollActivity.setTypeface(typeface); binding.flairCustomTextViewPostPollActivity.setTypeface(typeface); binding.spoilerCustomTextViewPostPollActivity.setTypeface(typeface); binding.nsfwCustomTextViewPostPollActivity.setTypeface(typeface); binding.postTitleEditTextPostPollActivity.setTypeface(typeface); binding.option1TextInputLayoutEditTextPostPollActivity.setTypeface(typeface); binding.option2TextInputLayoutEditTextPostPollActivity.setTypeface(typeface); binding.option3TextInputLayoutEditTextPostPollActivity.setTypeface(typeface); binding.option4TextInputLayoutEditTextPostPollActivity.setTypeface(typeface); binding.option5TextInputLayoutEditTextPostPollActivity.setTypeface(typeface); binding.option6TextInputLayoutEditTextPostPollActivity.setTypeface(typeface); } if (contentTypeface != null) { binding.postContentEditTextPostPollActivity.setTypeface(contentTypeface); } } public void setCursorDrawableColor(EditText editText, int color) { try { Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); fCursorDrawableRes.setAccessible(true); int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); Field fEditor = TextView.class.getDeclaredField("mEditor"); fEditor.setAccessible(true); Object editor = fEditor.get(editText); Class clazz = editor.getClass(); Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); fCursorDrawable.setAccessible(true); Drawable[] drawables = new Drawable[2]; drawables[0] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[1] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); fCursorDrawable.set(editor, drawables); } catch (Throwable ignored) { } } private void displaySubredditIcon() { if (iconUrl != null && !iconUrl.isEmpty()) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.subredditIconGifImageViewPostPollActivity); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostPollActivity); } } private void loadSubredditIcon() { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accessToken, accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { iconUrl = iconImageUrl; displaySubredditIcon(); loadSubredditIconSuccessful = true; }); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.post_poll_activity, menu); applyMenuItemTheme(menu); mMenu = menu; if (isPosting) { mMenu.findItem(R.id.action_send_post_poll_activity).setEnabled(false); mMenu.findItem(R.id.action_send_post_poll_activity).getIcon().setAlpha(130); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_preview_post_poll_activity) { Intent intent = new Intent(this, FullMarkdownActivity.class); intent.putExtra(FullMarkdownActivity.EXTRA_MARKDOWN, binding.postContentEditTextPostPollActivity.getText().toString()); intent.putExtra(FullMarkdownActivity.EXTRA_SUBMIT_POST, true); startActivityForResult(intent, MARKDOWN_PREVIEW_REQUEST_CODE); } else if (itemId == R.id.action_send_post_poll_activity) { submitPost(item); return true; } return false; } private void submitPost(MenuItem item) { if (!subredditSelected) { Snackbar.make(binding.coordinatorLayoutPostPollActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); return; } if (binding.postTitleEditTextPostPollActivity.getText() == null) { Snackbar.make(binding.coordinatorLayoutPostPollActivity, R.string.title_required, Snackbar.LENGTH_SHORT).show(); return; } String subredditName; if (subredditIsUser) { subredditName = "u_" + binding.subredditNameTextViewPostPollActivity.getText().toString(); } else { subredditName = binding.subredditNameTextViewPostPollActivity.getText().toString(); } ArrayList optionList = new ArrayList<>(); if (!binding.option1TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty()) { optionList.add(binding.option1TextInputLayoutEditTextPostPollActivity.getText().toString()); } if (!binding.option2TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty()) { optionList.add(binding.option2TextInputLayoutEditTextPostPollActivity.getText().toString()); } if (!binding.option3TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty()) { optionList.add(binding.option3TextInputLayoutEditTextPostPollActivity.getText().toString()); } if (!binding.option4TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty()) { optionList.add(binding.option4TextInputLayoutEditTextPostPollActivity.getText().toString()); } if (!binding.option5TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty()) { optionList.add(binding.option5TextInputLayoutEditTextPostPollActivity.getText().toString()); } if (!binding.option6TextInputLayoutEditTextPostPollActivity.getText().toString().isEmpty()) { optionList.add(binding.option6TextInputLayoutEditTextPostPollActivity.getText().toString()); } if (optionList.size() < 2) { Snackbar.make(binding.coordinatorLayoutPostPollActivity, R.string.two_options_required, Snackbar.LENGTH_SHORT).show(); return; } isPosting = true; item.setEnabled(false); item.getIcon().setAlpha(130); mPostingSnackbar.show(); PersistableBundle extras = new PersistableBundle(); extras.putString(SubmitPostService.EXTRA_ACCOUNT, selectedAccount.getJSONModel()); extras.putString(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_POLL); PollPayload payload; if (!binding.postContentEditTextPostPollActivity.getText().toString().isEmpty()) { if (uploadedImages.isEmpty()) { payload = new PollPayload(subredditName, binding.postTitleEditTextPostPollActivity.getText().toString(), optionList.toArray(new String[0]), (int) binding.votingLengthSliderPostPollActivity.getValue(), isNSFW, isSpoiler, flair, null, binding.postContentEditTextPostPollActivity.getText().toString(), binding.receivePostReplyNotificationsSwitchMaterialPostPollActivity.isChecked(), subredditIsUser ? "profile" : "subreddit"); } else { try { payload = new PollPayload(subredditName, binding.postTitleEditTextPostPollActivity.getText().toString(), optionList.toArray(new String[0]), (int) binding.votingLengthSliderPostPollActivity.getValue(), isNSFW, isSpoiler, flair, new RichTextJSONConverter().constructRichTextJSON(this, binding.postContentEditTextPostPollActivity.getText().toString(), uploadedImages), null, binding.receivePostReplyNotificationsSwitchMaterialPostPollActivity.isChecked(), subredditIsUser ? "profile" : "subreddit"); } catch (JSONException e) { Snackbar.make(binding.coordinatorLayoutPostPollActivity, R.string.convert_to_richtext_json_failed, Snackbar.LENGTH_SHORT).show(); return; } } } else { payload = new PollPayload(subredditName, binding.postTitleEditTextPostPollActivity.getText().toString(), optionList.toArray(new String[0]), (int) binding.votingLengthSliderPostPollActivity.getValue(), isNSFW, isSpoiler, flair, binding.receivePostReplyNotificationsSwitchMaterialPostPollActivity.isChecked(), subredditIsUser ? "profile" : "subreddit"); } String payloadJSON = new Gson().toJson(payload); extras.putString(SubmitPostService.EXTRA_POLL_PAYLOAD, payloadJSON); JobInfo jobInfo = SubmitPostService.constructJobInfo(this, payloadJSON.length() * 2L, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(SUBREDDIT_ICON_STATE, iconUrl); outState.putBoolean(SUBREDDIT_SELECTED_STATE, subredditSelected); outState.putBoolean(SUBREDDIT_IS_USER_STATE, subredditIsUser); outState.putBoolean(LOAD_SUBREDDIT_ICON_STATE, loadSubredditIconSuccessful); outState.putBoolean(IS_POSTING_STATE, isPosting); outState.putParcelable(FLAIR_STATE, flair); outState.putBoolean(IS_SPOILER_STATE, isSpoiler); outState.putBoolean(IS_NSFW_STATE, isNSFW); outState.putParcelableArrayList(UPLOADED_IMAGES_STATE, uploadedImages); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { subredditName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); iconUrl = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON); subredditSelected = true; subredditIsUser = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT) == SelectThingReturnKey.THING_TYPE.USER; binding.subredditNameTextViewPostPollActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostPollActivity.setText(subredditName); displaySubredditIcon(); binding.flairCustomTextViewPostPollActivity.setVisibility(View.VISIBLE); binding.flairCustomTextViewPostPollActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostPollActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostPollActivity.setText(getString(R.string.flair)); flair = null; } else if (requestCode == PICK_IMAGE_REQUEST_CODE) { if (data == null) { Toast.makeText(PostPollActivity.this, R.string.error_getting_image, Toast.LENGTH_LONG).show(); return; } Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, accessToken, binding.postContentEditTextPostPollActivity, binding.coordinatorLayoutPostPollActivity, data.getData(), uploadedImages); } else if (requestCode == CAPTURE_IMAGE_REQUEST_CODE) { Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, accessToken, binding.postContentEditTextPostPollActivity, binding.coordinatorLayoutPostPollActivity, capturedImageUri, uploadedImages); } else if (requestCode == MARKDOWN_PREVIEW_REQUEST_CODE) { submitPost(mMenu.findItem(R.id.action_send_post_poll_activity)); } } } @Override public void flairSelected(Flair flair) { this.flair = flair; binding.flairCustomTextViewPostPollActivity.setText(flair.getText()); binding.flairCustomTextViewPostPollActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostPollActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostPollActivity.setTextColor(flairTextColor); } @Override public void uploadImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getResources().getString(R.string.select_from_gallery)), PICK_IMAGE_REQUEST_CODE); } @Override public void captureImage() { Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { capturedImageUri = FileProvider.getUriForFile(this, getPackageName() + ".provider", File.createTempFile("captured_image", ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES))); pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); startActivityForResult(pictureIntent, CAPTURE_IMAGE_REQUEST_CODE); } catch (IOException ex) { Toast.makeText(this, R.string.error_creating_temp_file, Toast.LENGTH_SHORT).show(); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_camera_available, Toast.LENGTH_SHORT).show(); } } @Override public void insertImageUrl(UploadedImage uploadedImage) { int start = Math.max(binding.postContentEditTextPostPollActivity.getSelectionStart(), 0); int end = Math.max(binding.postContentEditTextPostPollActivity.getSelectionEnd(), 0); int realStart = Math.min(start, end); if (realStart > 0 && binding.postContentEditTextPostPollActivity.getText().toString().charAt(realStart - 1) != '\n') { binding.postContentEditTextPostPollActivity.getText().replace(realStart, Math.max(start, end), "\n![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "\n![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } else { binding.postContentEditTextPostPollActivity.getText().replace(realStart, Math.max(start, end), "![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostPollActivity); binding.accountNameTextViewPostPollActivity.setText(selectedAccount.getAccountName()); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onSubmitPollPostEvent(SubmitPollPostEvent submitPollPostEvent) { isPosting = false; mPostingSnackbar.dismiss(); if (submitPollPostEvent.postSuccess) { Intent intent = new Intent(this, LinkResolverActivity.class); intent.setData(Uri.parse(submitPollPostEvent.postUrl)); startActivity(intent); finish(); } else { mMenu.findItem(R.id.action_send_post_poll_activity).setEnabled(true); mMenu.findItem(R.id.action_send_post_poll_activity).getIcon().setAlpha(255); if (submitPollPostEvent.errorMessage == null || submitPollPostEvent.errorMessage.isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostPollActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutPostPollActivity, submitPollPostEvent.errorMessage.substring(0, 1).toUpperCase() + submitPollPostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostTextActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.PersistableBundle; import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UploadedImagesBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityPostTextBinding; import ml.docilealligator.infinityforreddit.events.SubmitTextOrLinkPostEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class PostTextActivity extends BaseActivity implements FlairBottomSheetFragment.FlairSelectionCallback, UploadImageEnabledActivity, AccountChooserBottomSheetFragment.AccountChooserListener { static final String EXTRA_SUBREDDIT_NAME = "ESN"; static final String EXTRA_CONTENT = "EC"; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String SUBREDDIT_ICON_STATE = "SIS"; private static final String SUBREDDIT_SELECTED_STATE = "SSS"; private static final String SUBREDDIT_IS_USER_STATE = "SIUS"; private static final String LOAD_SUBREDDIT_ICON_STATE = "LSIS"; private static final String IS_POSTING_STATE = "IPS"; private static final String FLAIR_STATE = "FS"; private static final String IS_SPOILER_STATE = "ISS"; private static final String IS_NSFW_STATE = "INS"; private static final String UPLOADED_IMAGES_STATE = "UIS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 0; private static final int PICK_IMAGE_REQUEST_CODE = 100; private static final int CAPTURE_IMAGE_REQUEST_CODE = 200; private static final int MARKDOWN_PREVIEW_REQUEST_CODE = 300; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Account selectedAccount; private String iconUrl; private String subredditName; private boolean subredditSelected = false; private boolean subredditIsUser; private boolean loadSubredditIconSuccessful = true; private boolean isPosting; private int primaryTextColor; private int flairBackgroundColor; private int flairTextColor; private int spoilerBackgroundColor; private int spoilerTextColor; private int nsfwBackgroundColor; private int nsfwTextColor; private Flair flair; private boolean isSpoiler = false; private boolean isNSFW = false; private Resources resources; private Menu mMenu; private RequestManager mGlide; private FlairBottomSheetFragment flairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private Uri capturedImageUri; private ArrayList uploadedImages = new ArrayList<>(); private ActivityPostTextBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityPostTextBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostTextActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarPostTextActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutPostTextActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostTextActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); mPostingSnackbar = Snackbar.make(binding.coordinatorLayoutPostTextActivity, R.string.posting, Snackbar.LENGTH_INDEFINITE); resources = getResources(); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); iconUrl = savedInstanceState.getString(SUBREDDIT_ICON_STATE); subredditSelected = savedInstanceState.getBoolean(SUBREDDIT_SELECTED_STATE); subredditIsUser = savedInstanceState.getBoolean(SUBREDDIT_IS_USER_STATE); loadSubredditIconSuccessful = savedInstanceState.getBoolean(LOAD_SUBREDDIT_ICON_STATE); isPosting = savedInstanceState.getBoolean(IS_POSTING_STATE); flair = savedInstanceState.getParcelable(FLAIR_STATE); isSpoiler = savedInstanceState.getBoolean(IS_SPOILER_STATE); isNSFW = savedInstanceState.getBoolean(IS_NSFW_STATE); uploadedImages = savedInstanceState.getParcelableArrayList(UPLOADED_IMAGES_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostTextActivity); binding.accountNameTextViewPostTextActivity.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } if (subredditName != null) { binding.subredditNameTextViewPostTextActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostTextActivity.setText(subredditName); binding.flairCustomTextViewPostTextActivity.setVisibility(View.VISIBLE); if (!loadSubredditIconSuccessful) { loadSubredditIcon(); } } displaySubredditIcon(); if (isPosting) { mPostingSnackbar.show(); } if (flair != null) { binding.flairCustomTextViewPostTextActivity.setText(flair.getText()); binding.flairCustomTextViewPostTextActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostTextActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostTextActivity.setTextColor(flairTextColor); } if (isSpoiler) { binding.spoilerCustomTextViewPostTextActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostTextActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostTextActivity.setTextColor(spoilerTextColor); } if (isNSFW) { binding.nsfwCustomTextViewPostTextActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostTextActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostTextActivity.setTextColor(nsfwTextColor); } } else { isPosting = false; loadCurrentAccount(); if (getIntent().hasExtra(EXTRA_SUBREDDIT_NAME)) { loadSubredditIconSuccessful = false; subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); subredditSelected = true; binding.subredditNameTextViewPostTextActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostTextActivity.setText(subredditName); binding.flairCustomTextViewPostTextActivity.setVisibility(View.VISIBLE); loadSubredditIcon(); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostTextActivity); } String text = getIntent().getStringExtra(EXTRA_CONTENT); if (text != null) { binding.postTextContentEditTextPostTextActivity.setText(text); } } binding.accountLinearLayoutPostTextActivity.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.subredditRelativeLayoutPostTextActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SPECIFIED_ACCOUNT, selectedAccount); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.rulesButtonPostTextActivity.setOnClickListener(view -> { if (subredditName == null) { Snackbar.make(binding.coordinatorLayoutPostTextActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, RulesActivity.class); if (subredditIsUser) { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, subredditName); } startActivity(intent); } }); binding.flairCustomTextViewPostTextActivity.setOnClickListener(view -> { if (flair == null) { flairSelectionBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); if (subredditIsUser) { bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, subredditName); } flairSelectionBottomSheetFragment.setArguments(bundle); flairSelectionBottomSheetFragment.show(getSupportFragmentManager(), flairSelectionBottomSheetFragment.getTag()); } else { binding.flairCustomTextViewPostTextActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostTextActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostTextActivity.setText(getString(R.string.flair)); flair = null; } }); binding.spoilerCustomTextViewPostTextActivity.setOnClickListener(view -> { if (!isSpoiler) { binding.spoilerCustomTextViewPostTextActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostTextActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostTextActivity.setTextColor(spoilerTextColor); isSpoiler = true; } else { binding.spoilerCustomTextViewPostTextActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.spoilerCustomTextViewPostTextActivity.setTextColor(primaryTextColor); isSpoiler = false; } }); binding.nsfwCustomTextViewPostTextActivity.setOnClickListener(view -> { if (!isNSFW) { binding.nsfwCustomTextViewPostTextActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostTextActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostTextActivity.setTextColor(nsfwTextColor); isNSFW = true; } else { binding.nsfwCustomTextViewPostTextActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.nsfwCustomTextViewPostTextActivity.setTextColor(primaryTextColor); isNSFW = false; } }); binding.receivePostReplyNotificationsLinearLayoutPostTextActivity.setOnClickListener(view -> { binding.receivePostReplyNotificationsSwitchMaterialPostTextActivity.performClick(); }); MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, true, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( PostTextActivity.this, binding.postTextContentEditTextPostTextActivity, item); } @Override public void onUploadImage() { Utils.hideKeyboard(PostTextActivity.this); UploadedImagesBottomSheetFragment fragment = new UploadedImagesBottomSheetFragment(); Bundle arguments = new Bundle(); arguments.putParcelableArrayList(UploadedImagesBottomSheetFragment.EXTRA_UPLOADED_IMAGES, uploadedImages); fragment.setArguments(arguments); fragment.show(getSupportFragmentManager(), fragment.getTag()); } }); binding.markdownBottomBarRecyclerViewPostTextActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManager.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewPostTextActivity.setAdapter(adapter); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isPosting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_submit_post_detail); } else { if (!binding.postTitleEditTextPostTextActivity.getText().toString().equals("") || !binding.postTextContentEditTextPostTextActivity.getText().toString().equals("")) { promptAlertDialog(R.string.discard, R.string.discard_detail); } else { setEnabled(false); triggerBackPress(); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostTextActivity); binding.accountNameTextViewPostTextActivity.setText(account.getAccountName()); } }); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutPostTextActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostTextActivity, null, binding.toolbarPostTextActivity); primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.accountNameTextViewPostTextActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.subredditNameTextViewPostTextActivity.setTextColor(secondaryTextColor); binding.rulesButtonPostTextActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.rulesButtonPostTextActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.receivePostReplyNotificationsTextViewPostTextActivity.setTextColor(primaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1PostTextActivity.setDividerColor(dividerColor); binding.divider2PostTextActivity.setDividerColor(dividerColor); flairBackgroundColor = mCustomThemeWrapper.getFlairBackgroundColor(); flairTextColor = mCustomThemeWrapper.getFlairTextColor(); spoilerBackgroundColor = mCustomThemeWrapper.getSpoilerBackgroundColor(); spoilerTextColor = mCustomThemeWrapper.getSpoilerTextColor(); nsfwBackgroundColor = mCustomThemeWrapper.getNsfwBackgroundColor(); nsfwTextColor = mCustomThemeWrapper.getNsfwTextColor(); binding.flairCustomTextViewPostTextActivity.setTextColor(primaryTextColor); binding.spoilerCustomTextViewPostTextActivity.setTextColor(primaryTextColor); binding.nsfwCustomTextViewPostTextActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostTextActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostTextActivity.setHintTextColor(secondaryTextColor); binding.postTextContentEditTextPostTextActivity.setTextColor(primaryTextColor); binding.postTextContentEditTextPostTextActivity.setHintTextColor(secondaryTextColor); if (typeface != null) { binding.subredditNameTextViewPostTextActivity.setTypeface(typeface); binding.rulesButtonPostTextActivity.setTypeface(typeface); binding.receivePostReplyNotificationsTextViewPostTextActivity.setTypeface(typeface); binding.flairCustomTextViewPostTextActivity.setTypeface(typeface); binding.spoilerCustomTextViewPostTextActivity.setTypeface(typeface); binding.nsfwCustomTextViewPostTextActivity.setTypeface(typeface); binding.postTitleEditTextPostTextActivity.setTypeface(typeface); } if (contentTypeface != null) { binding.postTextContentEditTextPostTextActivity.setTypeface(contentTypeface); } } private void displaySubredditIcon() { if (iconUrl != null && !iconUrl.equals("")) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.subredditIconGifImageViewPostTextActivity); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostTextActivity); } } private void loadSubredditIcon() { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accessToken, accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { iconUrl = iconImageUrl; displaySubredditIcon(); loadSubredditIconSuccessful = true; }); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.cancel, null) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.post_text_activity, menu); applyMenuItemTheme(menu); mMenu = menu; if (isPosting) { mMenu.findItem(R.id.action_send_post_text_activity).setEnabled(false); mMenu.findItem(R.id.action_send_post_text_activity).getIcon().setAlpha(130); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_preview_post_text_activity) { Intent intent = new Intent(this, FullMarkdownActivity.class); intent.putExtra(FullMarkdownActivity.EXTRA_MARKDOWN, binding.postTextContentEditTextPostTextActivity.getText().toString()); intent.putExtra(FullMarkdownActivity.EXTRA_SUBMIT_POST, true); startActivityForResult(intent, MARKDOWN_PREVIEW_REQUEST_CODE); } else if (itemId == R.id.action_send_post_text_activity) { submitPost(item); return true; } return false; } private void submitPost(MenuItem item) { if (!subredditSelected) { Snackbar.make(binding.coordinatorLayoutPostTextActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); return; } if (binding.postTitleEditTextPostTextActivity.getText() == null || binding.postTitleEditTextPostTextActivity.getText().toString().equals("")) { Snackbar.make(binding.coordinatorLayoutPostTextActivity, R.string.title_required, Snackbar.LENGTH_SHORT).show(); return; } isPosting = true; if (item != null) { item.setEnabled(false); item.getIcon().setAlpha(130); } mPostingSnackbar.show(); String subredditName; if (subredditIsUser) { subredditName = "u_" + binding.subredditNameTextViewPostTextActivity.getText().toString(); } else { subredditName = binding.subredditNameTextViewPostTextActivity.getText().toString(); } /*Intent intent = new Intent(this, SubmitPostService.class); intent.putExtra(SubmitPostService.EXTRA_ACCOUNT, selectedAccount); intent.putExtra(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); intent.putExtra(SubmitPostService.EXTRA_TITLE, binding.postTitleEditTextPostTextActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_CONTENT, binding.postTextContentEditTextPostTextActivity.getText().toString()); if (!uploadedImages.isEmpty()) { intent.putExtra(SubmitPostService.EXTRA_IS_RICHTEXT_JSON, true); intent.putExtra(SubmitPostService.EXTRA_UPLOADED_IMAGES, uploadedImages); } intent.putExtra(SubmitPostService.EXTRA_KIND, APIUtils.KIND_SELF); intent.putExtra(SubmitPostService.EXTRA_FLAIR, flair); intent.putExtra(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler); intent.putExtra(SubmitPostService.EXTRA_IS_NSFW, isNSFW); intent.putExtra(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostTextActivity.isChecked()); intent.putExtra(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TEXT_OR_LINK); ContextCompat.startForegroundService(this, intent);*/ int contentEstimatedBytes = 0; PersistableBundle extras = new PersistableBundle(); extras.putString(SubmitPostService.EXTRA_ACCOUNT, selectedAccount.getJSONModel()); extras.putString(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); String title = binding.postTitleEditTextPostTextActivity.getText().toString(); contentEstimatedBytes += title.length() * 2; extras.putString(SubmitPostService.EXTRA_TITLE, title); String content = binding.postTextContentEditTextPostTextActivity.getText().toString(); contentEstimatedBytes += content.length() * 2; extras.putString(SubmitPostService.EXTRA_CONTENT, content); if (!uploadedImages.isEmpty()) { extras.putInt(SubmitPostService.EXTRA_IS_RICHTEXT_JSON, 1); String uploadedImagesJSON = UploadedImage.getArrayListJSONModel(uploadedImages); contentEstimatedBytes += uploadedImagesJSON.length() * 2; extras.putString(SubmitPostService.EXTRA_UPLOADED_IMAGES, uploadedImagesJSON); } extras.putString(SubmitPostService.EXTRA_KIND, APIUtils.KIND_SELF); if (flair != null) { extras.putString(SubmitPostService.EXTRA_FLAIR, flair.getJSONModel()); } extras.putInt(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_IS_NSFW, isNSFW ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostTextActivity.isChecked() ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TEXT_OR_LINK); JobInfo jobInfo = SubmitPostService.constructJobInfo(this, contentEstimatedBytes, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(SUBREDDIT_ICON_STATE, iconUrl); outState.putBoolean(SUBREDDIT_SELECTED_STATE, subredditSelected); outState.putBoolean(SUBREDDIT_IS_USER_STATE, subredditIsUser); outState.putBoolean(LOAD_SUBREDDIT_ICON_STATE, loadSubredditIconSuccessful); outState.putBoolean(IS_POSTING_STATE, isPosting); outState.putParcelable(FLAIR_STATE, flair); outState.putBoolean(IS_SPOILER_STATE, isSpoiler); outState.putBoolean(IS_NSFW_STATE, isNSFW); outState.putParcelableArrayList(UPLOADED_IMAGES_STATE, uploadedImages); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { subredditName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); iconUrl = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON); subredditSelected = true; subredditIsUser = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT) == SelectThingReturnKey.THING_TYPE.USER; binding.subredditNameTextViewPostTextActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostTextActivity.setText(subredditName); displaySubredditIcon(); binding.flairCustomTextViewPostTextActivity.setVisibility(View.VISIBLE); binding.flairCustomTextViewPostTextActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostTextActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostTextActivity.setText(getString(R.string.flair)); flair = null; } else if (requestCode == PICK_IMAGE_REQUEST_CODE) { if (data == null) { Toast.makeText(PostTextActivity.this, R.string.error_getting_image, Toast.LENGTH_LONG).show(); return; } Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, accessToken, binding.postTextContentEditTextPostTextActivity, binding.coordinatorLayoutPostTextActivity, data.getData(), uploadedImages); } else if (requestCode == CAPTURE_IMAGE_REQUEST_CODE) { Utils.uploadImageToReddit(this, mExecutor, mOauthRetrofit, mUploadMediaRetrofit, accessToken, binding.postTextContentEditTextPostTextActivity, binding.coordinatorLayoutPostTextActivity, capturedImageUri, uploadedImages); } else if (requestCode == MARKDOWN_PREVIEW_REQUEST_CODE) { submitPost(mMenu.findItem(R.id.action_send_post_text_activity)); } } } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void flairSelected(Flair flair) { this.flair = flair; binding.flairCustomTextViewPostTextActivity.setText(flair.getText()); binding.flairCustomTextViewPostTextActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostTextActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostTextActivity.setTextColor(flairTextColor); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onSubmitTextPostEvent(SubmitTextOrLinkPostEvent submitTextOrLinkPostEvent) { isPosting = false; mPostingSnackbar.dismiss(); if (submitTextOrLinkPostEvent.postSuccess) { Intent intent = new Intent(PostTextActivity.this, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_DATA, submitTextOrLinkPostEvent.post); startActivity(intent); finish(); } else { mMenu.findItem(R.id.action_send_post_text_activity).setEnabled(true); mMenu.findItem(R.id.action_send_post_text_activity).getIcon().setAlpha(255); if (submitTextOrLinkPostEvent.errorMessage == null || submitTextOrLinkPostEvent.errorMessage.equals("")) { Snackbar.make(binding.coordinatorLayoutPostTextActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutPostTextActivity, submitTextOrLinkPostEvent.errorMessage.substring(0, 1).toUpperCase() + submitTextOrLinkPostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } @Override public void uploadImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getResources().getString(R.string.select_from_gallery)), PICK_IMAGE_REQUEST_CODE); } @Override public void captureImage() { Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { capturedImageUri = FileProvider.getUriForFile(this, getPackageName() + ".provider", File.createTempFile("captured_image", ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES))); pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); startActivityForResult(pictureIntent, CAPTURE_IMAGE_REQUEST_CODE); } catch (IOException ex) { Toast.makeText(this, R.string.error_creating_temp_file, Toast.LENGTH_SHORT).show(); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_camera_available, Toast.LENGTH_SHORT).show(); } } @Override public void insertImageUrl(UploadedImage uploadedImage) { int start = Math.max(binding.postTextContentEditTextPostTextActivity.getSelectionStart(), 0); int end = Math.max(binding.postTextContentEditTextPostTextActivity.getSelectionEnd(), 0); int realStart = Math.min(start, end); if (realStart > 0 && binding.postTextContentEditTextPostTextActivity.getText().toString().charAt(realStart - 1) != '\n') { binding.postTextContentEditTextPostTextActivity.getText().replace(realStart, Math.max(start, end), "\n![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "\n![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } else { binding.postTextContentEditTextPostTextActivity.getText().replace(realStart, Math.max(start, end), "![](" + uploadedImage.imageUrlOrKey + ")\n", 0, "![]()\n".length() + uploadedImage.imageUrlOrKey.length()); } } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostTextActivity); binding.accountNameTextViewPostTextActivity.setText(selectedAccount.getAccountName()); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostVideoActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.PersistableBundle; import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultDataSourceFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.ProgressiveMediaSource; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.MarkdownBottomBarRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityPostVideoBinding; import ml.docilealligator.infinityforreddit.events.SubmitVideoOrGifPostEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class PostVideoActivity extends BaseActivity implements FlairBottomSheetFragment.FlairSelectionCallback, AccountChooserBottomSheetFragment.AccountChooserListener { static final String EXTRA_SUBREDDIT_NAME = "ESN"; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String SUBREDDIT_ICON_STATE = "SIS"; private static final String SUBREDDIT_SELECTED_STATE = "SSS"; private static final String SUBREDDIT_IS_USER_STATE = "SIUS"; private static final String VIDEO_URI_STATE = "IUS"; private static final String LOAD_SUBREDDIT_ICON_STATE = "LSIS"; private static final String IS_POSTING_STATE = "IPS"; private static final String FLAIR_STATE = "FS"; private static final String IS_SPOILER_STATE = "ISS"; private static final String IS_NSFW_STATE = "INS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 0; private static final int PICK_VIDEO_REQUEST_CODE = 1; private static final int CAPTURE_VIDEO_REQUEST_CODE = 2; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject @Named("upload_video") Retrofit mUploadVideoRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Account selectedAccount; private String iconUrl; private String subredditName; private boolean subredditSelected = false; private boolean subredditIsUser; private Uri videoUri; private boolean loadSubredditIconSuccessful = true; private boolean isPosting; private boolean wasPlaying; private int primaryTextColor; private int flairBackgroundColor; private int flairTextColor; private int spoilerBackgroundColor; private int spoilerTextColor; private int nsfwBackgroundColor; private int nsfwTextColor; private Flair flair; private boolean isSpoiler = false; private boolean isNSFW = false; private Resources resources; private Menu mMemu; private RequestManager mGlide; private FlairBottomSheetFragment mFlairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private DataSource.Factory dataSourceFactory; private ExoPlayer player; private ActivityPostVideoBinding binding; @OptIn(markerClass = UnstableApi.class) @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityPostVideoBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutPostVideoActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarPostVideoActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutPostVideoActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarPostVideoActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); player = new ExoPlayer.Builder(this).build(); binding.playerViewPostVideoActivity.setPlayer(player); dataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, "Infinity")); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.LOOP_VIDEO, true)) { player.setRepeatMode(Player.REPEAT_MODE_ALL); } else { player.setRepeatMode(Player.REPEAT_MODE_OFF); } Drawable playDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_play_arrow_24dp, null); Drawable pauseDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_pause_24dp, null); MaterialButton playPauseButton = binding.getRoot().findViewById(R.id.exo_play_pause_button_exo_playback_control_view); playPauseButton.setOnClickListener((view) -> { Util.handlePlayPauseButtonAction(player); }); player.addListener(new Player.Listener() { @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { if (events.containsAny( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { playPauseButton.setIcon(Util.shouldShowPlayButton(player) ? playDrawable : pauseDrawable); } } }); mPostingSnackbar = Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.posting, Snackbar.LENGTH_INDEFINITE); resources = getResources(); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); iconUrl = savedInstanceState.getString(SUBREDDIT_ICON_STATE); subredditSelected = savedInstanceState.getBoolean(SUBREDDIT_SELECTED_STATE); subredditIsUser = savedInstanceState.getBoolean(SUBREDDIT_IS_USER_STATE); loadSubredditIconSuccessful = savedInstanceState.getBoolean(LOAD_SUBREDDIT_ICON_STATE); isPosting = savedInstanceState.getBoolean(IS_POSTING_STATE); flair = savedInstanceState.getParcelable(FLAIR_STATE); isSpoiler = savedInstanceState.getBoolean(IS_SPOILER_STATE); isNSFW = savedInstanceState.getBoolean(IS_NSFW_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostVideoActivity); binding.accountNameTextViewPostVideoActivity.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } if (savedInstanceState.getString(VIDEO_URI_STATE) != null) { videoUri = Uri.parse(savedInstanceState.getString(VIDEO_URI_STATE)); loadVideo(); } if (subredditName != null) { binding.subredditNameTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostVideoActivity.setText(subredditName); binding.flairCustomTextViewPostVideoActivity.setVisibility(View.VISIBLE); if (!loadSubredditIconSuccessful) { loadSubredditIcon(); } } displaySubredditIcon(); if (isPosting) { mPostingSnackbar.show(); } if (flair != null) { binding.flairCustomTextViewPostVideoActivity.setText(flair.getText()); binding.flairCustomTextViewPostVideoActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostVideoActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostVideoActivity.setTextColor(flairTextColor); } if (isSpoiler) { binding.spoilerCustomTextViewPostVideoActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostVideoActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostVideoActivity.setTextColor(spoilerTextColor); } if (isNSFW) { binding.nsfwCustomTextViewPostVideoActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostVideoActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostVideoActivity.setTextColor(nsfwTextColor); } } else { isPosting = false; loadCurrentAccount(); if (getIntent().hasExtra(EXTRA_SUBREDDIT_NAME)) { loadSubredditIconSuccessful = false; subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); subredditSelected = true; binding.subredditNameTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostVideoActivity.setText(subredditName); binding.flairCustomTextViewPostVideoActivity.setVisibility(View.VISIBLE); loadSubredditIcon(); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostVideoActivity); } videoUri = getIntent().getData(); if (videoUri != null) { loadVideo(); } } binding.accountLinearLayoutPostVideoActivity.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.subredditRelativeLayoutPostVideoActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SPECIFIED_ACCOUNT, selectedAccount); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.rulesButtonPostVideoActivity.setOnClickListener(view -> { if (subredditName == null) { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, RulesActivity.class); if (subredditIsUser) { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, subredditName); } startActivity(intent); } }); binding.flairCustomTextViewPostVideoActivity.setOnClickListener(view -> { if (flair == null) { mFlairSelectionBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, subredditName); mFlairSelectionBottomSheetFragment.setArguments(bundle); mFlairSelectionBottomSheetFragment.show(getSupportFragmentManager(), mFlairSelectionBottomSheetFragment.getTag()); } else { binding.flairCustomTextViewPostVideoActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostVideoActivity.setText(getString(R.string.flair)); flair = null; } }); binding.spoilerCustomTextViewPostVideoActivity.setOnClickListener(view -> { if (!isSpoiler) { binding.spoilerCustomTextViewPostVideoActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostVideoActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewPostVideoActivity.setTextColor(spoilerTextColor); isSpoiler = true; } else { binding.spoilerCustomTextViewPostVideoActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.spoilerCustomTextViewPostVideoActivity.setTextColor(primaryTextColor); isSpoiler = false; } }); binding.nsfwCustomTextViewPostVideoActivity.setOnClickListener(view -> { if (!isNSFW) { binding.nsfwCustomTextViewPostVideoActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostVideoActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewPostVideoActivity.setTextColor(nsfwTextColor); isNSFW = true; } else { binding.nsfwCustomTextViewPostVideoActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.nsfwCustomTextViewPostVideoActivity.setTextColor(primaryTextColor); isNSFW = false; } }); binding.receivePostReplyNotificationsLinearLayoutPostVideoActivity.setOnClickListener(view -> { binding.receivePostReplyNotificationsSwitchMaterialPostVideoActivity.performClick(); }); binding.captureFabPostVideoActivity.setOnClickListener(view -> { Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); try { startActivityForResult(takeVideoIntent, CAPTURE_VIDEO_REQUEST_CODE); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_camera_available, Toast.LENGTH_SHORT).show(); } }); binding.selectFromLibraryFabPostVideoActivity.setOnClickListener(view -> { Intent intent = new Intent(); intent.setType("video/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, resources.getString(R.string.select_from_gallery)), PICK_VIDEO_REQUEST_CODE); }); binding.selectAgainTextViewPostVideoActivity.setOnClickListener(view -> { wasPlaying = false; player.setPlayWhenReady(false); videoUri = null; binding.playerViewPostVideoActivity.setVisibility(View.GONE); binding.selectAgainTextViewPostVideoActivity.setVisibility(View.GONE); binding.selectVideoConstraintLayoutPostVideoActivity.setVisibility(View.VISIBLE); }); MarkdownBottomBarRecyclerViewAdapter adapter = new MarkdownBottomBarRecyclerViewAdapter( mCustomThemeWrapper, new MarkdownBottomBarRecyclerViewAdapter.ItemClickListener() { @Override public void onClick(int item) { MarkdownBottomBarRecyclerViewAdapter.bindEditTextWithItemClickListener( PostVideoActivity.this, binding.postContentEditTextPostVideoActivity, item); } @Override public void onUploadImage() { } }); binding.markdownBottomBarRecyclerViewPostVideoActivity.setLayoutManager(new LinearLayoutManagerBugFixed(this, LinearLayoutManager.HORIZONTAL, true).setStackFromEndAndReturnCurrentObject()); binding.markdownBottomBarRecyclerViewPostVideoActivity.setAdapter(adapter); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isPosting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_submit_post_detail); } else { if (!binding.postTitleEditTextPostVideoActivity.getText().toString().isEmpty() || !binding.postContentEditTextPostVideoActivity.getText().toString().isEmpty() || videoUri != null) { promptAlertDialog(R.string.discard, R.string.discard_detail); } else { setEnabled(false); triggerBackPress(); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostVideoActivity); binding.accountNameTextViewPostVideoActivity.setText(account.getAccountName()); } }); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.coordinatorLayoutPostVideoActivity.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutPostVideoActivity, null, binding.toolbarPostVideoActivity); primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.accountNameTextViewPostVideoActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.subredditNameTextViewPostVideoActivity.setTextColor(secondaryTextColor); binding.rulesButtonPostVideoActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.rulesButtonPostVideoActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.receivePostReplyNotificationsTextViewPostVideoActivity.setTextColor(primaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1PostVideoActivity.setDividerColor(dividerColor); binding.divider2PostVideoActivity.setDividerColor(dividerColor); flairBackgroundColor = mCustomThemeWrapper.getFlairBackgroundColor(); flairTextColor = mCustomThemeWrapper.getFlairTextColor(); spoilerBackgroundColor = mCustomThemeWrapper.getSpoilerBackgroundColor(); spoilerTextColor = mCustomThemeWrapper.getSpoilerTextColor(); nsfwBackgroundColor = mCustomThemeWrapper.getNsfwBackgroundColor(); nsfwTextColor = mCustomThemeWrapper.getNsfwTextColor(); binding.flairCustomTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.spoilerCustomTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.nsfwCustomTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostVideoActivity.setTextColor(primaryTextColor); binding.postTitleEditTextPostVideoActivity.setHintTextColor(secondaryTextColor); binding.postContentEditTextPostVideoActivity.setTextColor(primaryTextColor); binding.postContentEditTextPostVideoActivity.setHintTextColor(secondaryTextColor); applyFABTheme(binding.captureFabPostVideoActivity); applyFABTheme(binding.selectFromLibraryFabPostVideoActivity); binding.selectAgainTextViewPostVideoActivity.setTextColor(mCustomThemeWrapper.getColorAccent()); if (typeface != null) { binding.subredditNameTextViewPostVideoActivity.setTypeface(typeface); binding.rulesButtonPostVideoActivity.setTypeface(typeface); binding.receivePostReplyNotificationsTextViewPostVideoActivity.setTypeface(typeface); binding.flairCustomTextViewPostVideoActivity.setTypeface(typeface); binding.spoilerCustomTextViewPostVideoActivity.setTypeface(typeface); binding.nsfwCustomTextViewPostVideoActivity.setTypeface(typeface); binding.postTitleEditTextPostVideoActivity.setTypeface(typeface); binding.selectAgainTextViewPostVideoActivity.setTypeface(typeface); } if (contentTypeface != null) { binding.postContentEditTextPostVideoActivity.setTypeface(contentTypeface); } } @OptIn(markerClass = UnstableApi.class) private void loadVideo() { binding.selectVideoConstraintLayoutPostVideoActivity.setVisibility(View.GONE); binding.selectAgainTextViewPostVideoActivity.setVisibility(View.VISIBLE); binding.playerViewPostVideoActivity.setVisibility(View.VISIBLE); player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(videoUri))); player.setPlayWhenReady(true); wasPlaying = true; } private void displaySubredditIcon() { if (iconUrl != null && !iconUrl.isEmpty()) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.subredditIconGifImageViewPostVideoActivity); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewPostVideoActivity); } } private void loadSubredditIcon() { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accessToken, accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { iconUrl = iconImageUrl; displaySubredditIcon(); loadSubredditIconSuccessful = true; }); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.discard_dialog_button, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.post_video_activity, menu); applyMenuItemTheme(menu); mMemu = menu; if (isPosting) { mMemu.findItem(R.id.action_send_post_video_activity).setEnabled(false); mMemu.findItem(R.id.action_send_post_video_activity).getIcon().setAlpha(130); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_send_post_video_activity) { if (!subredditSelected) { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); return true; } if (binding.postTitleEditTextPostVideoActivity.getText() == null || binding.postTitleEditTextPostVideoActivity.getText().toString().isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.title_required, Snackbar.LENGTH_SHORT).show(); return true; } if (videoUri == null) { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.select_an_image, Snackbar.LENGTH_SHORT).show(); return true; } isPosting = true; item.setEnabled(false); item.getIcon().setAlpha(130); mPostingSnackbar.show(); String subredditName; if (subredditIsUser) { subredditName = "u_" + binding.subredditNameTextViewPostVideoActivity.getText().toString(); } else { subredditName = binding.subredditNameTextViewPostVideoActivity.getText().toString(); } /*Intent intent = new Intent(this, SubmitPostService.class); intent.putExtra(SubmitPostService.EXTRA_MEDIA_URI, videoUri.toString()); intent.putExtra(SubmitPostService.EXTRA_ACCOUNT, selectedAccount); intent.putExtra(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); intent.putExtra(SubmitPostService.EXTRA_TITLE, binding.postTitleEditTextPostVideoActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_CONTENT, binding.postContentEditTextPostVideoActivity.getText().toString()); intent.putExtra(SubmitPostService.EXTRA_FLAIR, flair); intent.putExtra(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler); intent.putExtra(SubmitPostService.EXTRA_IS_NSFW, isNSFW); intent.putExtra(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostVideoActivity.isChecked()); intent.putExtra(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_VIDEO); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); ContextCompat.startForegroundService(this, intent);*/ int contentEstimatedBytes = 0; PersistableBundle extras = new PersistableBundle(); //TODO estimate video size extras.putString(SubmitPostService.EXTRA_MEDIA_URI, videoUri.toString()); extras.putString(SubmitPostService.EXTRA_ACCOUNT, selectedAccount.getJSONModel()); extras.putString(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); String title = binding.postTitleEditTextPostVideoActivity.getText().toString(); contentEstimatedBytes += title.length() * 2; extras.putString(SubmitPostService.EXTRA_TITLE, title); String content = binding.postContentEditTextPostVideoActivity.getText().toString(); contentEstimatedBytes += content.length() * 2; extras.putString(SubmitPostService.EXTRA_CONTENT, content); if (flair != null) { extras.putString(SubmitPostService.EXTRA_FLAIR, flair.getJSONModel()); } extras.putInt(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_IS_NSFW, isNSFW ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialPostVideoActivity.isChecked() ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_VIDEO); // TODO: contentEstimatedBytes JobInfo jobInfo = SubmitPostService.constructJobInfo(this, contentEstimatedBytes, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); return true; } return false; } @Override protected void onStart() { super.onStart(); if (wasPlaying) { player.setPlayWhenReady(true); } } @Override protected void onStop() { super.onStop(); player.setPlayWhenReady(false); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(SUBREDDIT_ICON_STATE, iconUrl); outState.putBoolean(SUBREDDIT_SELECTED_STATE, subredditSelected); outState.putBoolean(SUBREDDIT_IS_USER_STATE, subredditIsUser); if (videoUri != null) { outState.putString(VIDEO_URI_STATE, videoUri.toString()); } outState.putBoolean(LOAD_SUBREDDIT_ICON_STATE, loadSubredditIconSuccessful); outState.putBoolean(IS_POSTING_STATE, isPosting); outState.putParcelable(FLAIR_STATE, flair); outState.putBoolean(IS_SPOILER_STATE, isSpoiler); outState.putBoolean(IS_NSFW_STATE, isNSFW); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { if (resultCode == RESULT_OK) { if (data != null) { subredditName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); iconUrl = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON); subredditSelected = true; subredditIsUser = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT) == SelectThingReturnKey.THING_TYPE.USER; binding.subredditNameTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewPostVideoActivity.setText(subredditName); displaySubredditIcon(); binding.flairCustomTextViewPostVideoActivity.setVisibility(View.VISIBLE); binding.flairCustomTextViewPostVideoActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewPostVideoActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewPostVideoActivity.setText(getString(R.string.flair)); flair = null; } } } else if (requestCode == PICK_VIDEO_REQUEST_CODE) { if (resultCode == RESULT_OK) { if (data == null) { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.error_getting_video, Snackbar.LENGTH_SHORT).show(); return; } videoUri = data.getData(); loadVideo(); } } else if (requestCode == CAPTURE_VIDEO_REQUEST_CODE) { if (resultCode == RESULT_OK) { if (data != null && data.getData() != null) { videoUri = data.getData(); loadVideo(); } else { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.error_getting_video, Snackbar.LENGTH_SHORT).show(); } } } } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); player.seekToDefaultPosition(); player.stop(); player.release(); } @Override public void flairSelected(Flair flair) { this.flair = flair; binding.flairCustomTextViewPostVideoActivity.setText(flair.getText()); binding.flairCustomTextViewPostVideoActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewPostVideoActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewPostVideoActivity.setTextColor(flairTextColor); } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewPostVideoActivity); binding.accountNameTextViewPostVideoActivity.setText(selectedAccount.getAccountName()); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onSubmitVideoPostEvent(SubmitVideoOrGifPostEvent submitVideoOrGifPostEvent) { isPosting = false; mPostingSnackbar.dismiss(); mMemu.findItem(R.id.action_send_post_video_activity).setEnabled(true); mMemu.findItem(R.id.action_send_post_video_activity).getIcon().setAlpha(255); if (submitVideoOrGifPostEvent.postSuccess) { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); startActivity(intent); finish(); } else if (submitVideoOrGifPostEvent.errorProcessingVideoOrGif) { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.error_processing_video, Snackbar.LENGTH_SHORT).show(); } else { if (submitVideoOrGifPostEvent.errorMessage == null || submitVideoOrGifPostEvent.errorMessage.isEmpty()) { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.coordinatorLayoutPostVideoActivity, submitVideoOrGifPostEvent.errorMessage.substring(0, 1).toUpperCase() + submitVideoOrGifPostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/QRCodeScannerActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.view.MenuItem; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import com.google.zxing.ResultPoint; import com.journeyapps.barcodescanner.BarcodeCallback; import com.journeyapps.barcodescanner.BarcodeResult; import com.journeyapps.barcodescanner.DecoratedBarcodeView; import java.util.List; import ml.docilealligator.infinityforreddit.R; public class QRCodeScannerActivity extends AppCompatActivity { public static final String EXTRA_QR_CODE_RESULT = "EQCR"; private DecoratedBarcodeView barcodeView; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) { // Permission granted, initialize the scanner initializeScanner(); } else { // Permission denied Toast.makeText(this, R.string.camera_permission_required, Toast.LENGTH_SHORT).show(); setResult(RESULT_CANCELED); finish(); } }); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // No flags that would clear the activity stack setContentView(R.layout.activity_qrcode_scanner); Toolbar toolbar = findViewById(R.id.toolbar_qrcode_scanner_activity); setSupportActionBar(toolbar); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } setTitle(R.string.scan_qr_code); barcodeView = findViewById(R.id.barcode_scanner); // Check camera permission if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { // Permission already granted, initialize scanner initializeScanner(); } else { // Request permission requestPermissionLauncher.launch(Manifest.permission.CAMERA); } } private void initializeScanner() { barcodeView.decodeContinuous(new BarcodeCallback() { @Override public void barcodeResult(BarcodeResult result) { if (result.getText() != null) { // Create result intent and finish properly Intent resultIntent = new Intent(); resultIntent.putExtra(EXTRA_QR_CODE_RESULT, result.getText()); setResult(RESULT_OK, resultIntent); finish(); } } @Override public void possibleResultPoints(List resultPoints) { // Not used } }); } @Override protected void onResume() { super.onResume(); barcodeView.resume(); } @Override protected void onPause() { super.onPause(); barcodeView.pause(); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { // Ensure we set a result when user presses back setResult(RESULT_CANCELED); finish(); return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { // Ensure we set a result when user presses back button setResult(RESULT_CANCELED); super.onBackPressed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ReportActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.snackbar.Snackbar; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.ReportReasonRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityReportBinding; import ml.docilealligator.infinityforreddit.post.FetchRules; import ml.docilealligator.infinityforreddit.subreddit.Rule; import ml.docilealligator.infinityforreddit.thing.ReportReason; import ml.docilealligator.infinityforreddit.thing.ReportThing; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class ReportActivity extends BaseActivity { public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_THING_FULLNAME = "ETF"; private static final String GENERAL_REASONS_STATE = "GRS"; private static final String RULES_REASON_STATE = "RRS"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String mFullname; private String mSubredditName; private ArrayList generalReasons; private ArrayList rulesReasons; private ReportReasonRecyclerViewAdapter mAdapter; private ActivityReportBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityReportBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); attachSliderPanelIfApplicable(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutReportActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarReportActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewReportActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarReportActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mFullname = getIntent().getStringExtra(EXTRA_THING_FULLNAME); mSubredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); if (savedInstanceState != null) { generalReasons = savedInstanceState.getParcelableArrayList(GENERAL_REASONS_STATE); rulesReasons = savedInstanceState.getParcelableArrayList(RULES_REASON_STATE); } if (generalReasons != null) { mAdapter = new ReportReasonRecyclerViewAdapter(this, mCustomThemeWrapper, generalReasons); } else { mAdapter = new ReportReasonRecyclerViewAdapter(this, mCustomThemeWrapper, ReportReason.getGeneralReasons(this)); } binding.recyclerViewReportActivity.setAdapter(mAdapter); if (rulesReasons == null) { FetchRules.fetchRules(mExecutor, new Handler(), accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, accessToken, accountName, mSubredditName, new FetchRules.FetchRulesListener() { @Override public void success(ArrayList rules) { mAdapter.setRules(ReportReason.convertRulesToReasons(rules)); } @Override public void failed() { Snackbar.make(binding.getRoot(), R.string.error_loading_rules_without_retry, Snackbar.LENGTH_SHORT).show(); } }); } else { mAdapter.setRules(rulesReasons); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.report_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_send_report_activity) { ReportReason reportReason = mAdapter.getSelectedReason(); if (reportReason != null) { Toast.makeText(ReportActivity.this, R.string.reporting, Toast.LENGTH_SHORT).show(); ReportThing.reportThing(mOauthRetrofit, accessToken, mFullname, mSubredditName, reportReason.getReasonType(), reportReason.getReportReason(), new ReportThing.ReportThingListener() { @Override public void success() { Toast.makeText(ReportActivity.this, R.string.report_successful, Toast.LENGTH_SHORT).show(); finish(); } @Override public void failed() { Toast.makeText(ReportActivity.this, R.string.report_failed, Toast.LENGTH_SHORT).show(); } }); } else { Toast.makeText(ReportActivity.this, R.string.report_reason_not_selected, Toast.LENGTH_SHORT).show(); } return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (mAdapter != null) { outState.putParcelableArrayList(GENERAL_REASONS_STATE, mAdapter.getGeneralReasons()); outState.putParcelableArrayList(RULES_REASON_STATE, mAdapter.getRules()); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutReportActivity, null, binding.toolbarReportActivity); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/RulesActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.RulesRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityRulesBinding; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.post.FetchRules; import ml.docilealligator.infinityforreddit.subreddit.Rule; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class RulesActivity extends BaseActivity { static final String EXTRA_SUBREDDIT_NAME = "ESN"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String mSubredditName; private RulesRecyclerViewAdapter mAdapter; private ActivityRulesBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityRulesBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutRulesActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarRulesActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewRulesActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); /*adjustToolbar(binding.toolbarRulesActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { binding.recyclerViewRulesActivity.setPadding(0, 0, 0, navBarHeight); }*/ } } binding.appbarLayoutRulesActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimary()); setSupportActionBar(binding.toolbarRulesActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mSubredditName = getIntent().getExtras().getString(EXTRA_SUBREDDIT_NAME); mAdapter = new RulesRecyclerViewAdapter(this, mCustomThemeWrapper, mSliderPanel, mSubredditName); binding.recyclerViewRulesActivity.setAdapter(mAdapter); FetchRules.fetchRules(mExecutor, new Handler(), accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, accessToken, accountName, mSubredditName, new FetchRules.FetchRulesListener() { @Override public void success(ArrayList rules) { binding.progressBarRulesActivity.setVisibility(View.GONE); if (rules == null || rules.size() == 0) { binding.errorTextViewRulesActivity.setVisibility(View.VISIBLE); binding.errorTextViewRulesActivity.setText(R.string.no_rule); binding.errorTextViewRulesActivity.setOnClickListener(view -> { }); } mAdapter.changeDataset(rules); } @Override public void failed() { displayError(); } }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutRulesActivity, binding.collapsingToolbarLayoutRulesActivity, binding.toolbarRulesActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutRulesActivity); binding.progressBarRulesActivity.setIndicatorColor(mCustomThemeWrapper.getColorAccent()); binding.errorTextViewRulesActivity.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (typeface != null) { binding.errorTextViewRulesActivity.setTypeface(typeface); } } private void displayError() { binding.progressBarRulesActivity.setVisibility(View.GONE); binding.errorTextViewRulesActivity.setVisibility(View.VISIBLE); binding.errorTextViewRulesActivity.setText(R.string.error_loading_rules); binding.errorTextViewRulesActivity.setOnClickListener(view -> { binding.progressBarRulesActivity.setVisibility(View.VISIBLE); binding.errorTextViewRulesActivity.setVisibility(View.GONE); FetchRules.fetchRules(mExecutor, new Handler(), accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, accessToken, accountName, mSubredditName, new FetchRules.FetchRulesListener() { @Override public void success(ArrayList rules) { binding.progressBarRulesActivity.setVisibility(View.GONE); if (rules == null || rules.size() == 0) { binding.errorTextViewRulesActivity.setVisibility(View.VISIBLE); binding.errorTextViewRulesActivity.setText(R.string.no_rule); binding.errorTextViewRulesActivity.setOnClickListener(view -> { }); } mAdapter.changeDataset(rules); } @Override public void failed() { displayError(); } }); }); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { String dataSavingMode = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (mAdapter != null && dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { mAdapter.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SearchActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.SearchActivityRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.adapters.SubredditAutocompleteRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySearchBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.recentsearchquery.RecentSearchQuery; import ml.docilealligator.infinityforreddit.recentsearchquery.RecentSearchQueryViewModel; import ml.docilealligator.infinityforreddit.subreddit.ParseSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class SearchActivity extends BaseActivity { public static final String EXTRA_QUERY = "EQ"; public static final String EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME = "ESISOUN"; public static final String EXTRA_SEARCH_IN_MULTIREDDIT = "ESIM"; public static final String EXTRA_SEARCH_IN_THING_TYPE = "ESITY"; public static final String EXTRA_SEARCH_ONLY_SUBREDDITS = "ESOS"; public static final String EXTRA_SEARCH_ONLY_USERS = "ESOU"; public static final String EXTRA_SEARCH_SUBREDDITS_AND_USERS = "ESSAU"; public static final String RETURN_EXTRA_SELECTED_SUBREDDITS = "RESS"; public static final String RETURN_EXTRA_SELECTED_USERNAMES = "RESU"; public static final String EXTRA_IS_MULTI_SELECTION = "EIMS"; public static final int SUICIDE_PREVENTION_ACTIVITY_REQUEST_CODE = 101; private static final String SEARCH_IN_SUBREDDIT_OR_NAME_STATE = "SNS"; private static final String SEARCH_IN_THING_TYPE_STATE = "SITTS"; private static final String SEARCH_IN_MULTIREDDIT_STATE = "SIMS"; private static final int SUBREDDIT_SEARCH_REQUEST_CODE = 1; private static final int USER_SEARCH_REQUEST_CODE = 2; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor executor; private String query; private String searchInSubredditOrUserName; private MultiReddit searchInMultiReddit; @SelectThingReturnKey.THING_TYPE private int searchInThingType; private boolean searchOnlySubreddits; private boolean searchOnlyUsers; private boolean searchSubredditsAndUsers; private SearchActivityRecyclerViewAdapter adapter; private SubredditAutocompleteRecyclerViewAdapter subredditAutocompleteRecyclerViewAdapter; private Handler handler; private Runnable autoCompleteRunnable; private Call subredditAutocompleteCall; RecentSearchQueryViewModel mRecentSearchQueryViewModel; private ActivityResultLauncher requestThingSelectionForCurrentActivityLauncher; private ActivityResultLauncher requestThingSelectionForAnotherActivityLauncher; private ActivitySearchBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivitySearchBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSearchActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbar, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.nestedScrollViewSearchActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbar); binding.clearSearchEditViewSearchActivity.setVisibility(View.GONE); binding.viewAllSearchHistoryButtonSearchActivity.setVisibility(View.GONE); binding.deleteAllRecentSearchesButtonSearchActivity.setVisibility(View.GONE); searchOnlySubreddits = getIntent().getBooleanExtra(EXTRA_SEARCH_ONLY_SUBREDDITS, false); searchOnlyUsers = getIntent().getBooleanExtra(EXTRA_SEARCH_ONLY_USERS, false); searchSubredditsAndUsers = getIntent().getBooleanExtra(EXTRA_SEARCH_SUBREDDITS_AND_USERS, false); if (searchOnlySubreddits) { binding.searchEditTextSearchActivity.setHint(R.string.search_only_subreddits_hint); } else if (searchOnlyUsers) { binding.searchEditTextSearchActivity.setHint(R.string.search_only_users_hint); } else if (searchSubredditsAndUsers) { binding.searchEditTextSearchActivity.setHint(R.string.search_subreddits_and_users_hint); } boolean nsfw = mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); subredditAutocompleteRecyclerViewAdapter = new SubredditAutocompleteRecyclerViewAdapter(this, mCustomThemeWrapper, subredditData -> { if (searchOnlySubreddits || searchSubredditsAndUsers) { Intent returnIntent = new Intent(); if (getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)) { ArrayList subredditList = new ArrayList<>(); subredditList.add(subredditData); returnIntent.putParcelableArrayListExtra(RETURN_EXTRA_SELECTED_SUBREDDITS, subredditList); } else { returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME, subredditData.getName()); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON, subredditData.getIconUrl()); } setResult(Activity.RESULT_OK, returnIntent); } else { Intent intent = new Intent(SearchActivity.this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditData.getName()); startActivity(intent); } finish(); }); if (accountName.equals(Account.ANONYMOUS_ACCOUNT) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binding.searchEditTextSearchActivity.setImeOptions(binding.searchEditTextSearchActivity.getImeOptions() | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING); } handler = new Handler(); binding.searchEditTextSearchActivity.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) { if (subredditAutocompleteCall != null && subredditAutocompleteCall.isExecuted()) { subredditAutocompleteCall.cancel(); } if (autoCompleteRunnable != null) { handler.removeCallbacks(autoCompleteRunnable); } } @Override public void afterTextChanged(Editable s) { String currentQuery = s.toString().trim(); if (!currentQuery.isEmpty()) { binding.clearSearchEditViewSearchActivity.setVisibility(View.VISIBLE); if (!searchOnlyUsers) { autoCompleteRunnable = () -> { subredditAutocompleteCall = mOauthRetrofit.create(RedditAPI.class).subredditAutocomplete(APIUtils.getOAuthHeader(accessToken), currentQuery, nsfw); subredditAutocompleteCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { subredditAutocompleteCall = null; if (response.isSuccessful() && !call.isCanceled()) { ParseSubredditData.parseSubredditListingData(executor, handler, response.body(), nsfw, new ParseSubredditData.ParseSubredditListingDataListener() { @Override public void onParseSubredditListingDataSuccess(ArrayList subredditData, String after) { binding.recentSearchQueryRecyclerViewSearchActivity.setVisibility(View.GONE); binding.subredditAutocompleteRecyclerViewSearchActivity.setVisibility(View.VISIBLE); subredditAutocompleteRecyclerViewAdapter.setSubreddits(subredditData); } @Override public void onParseSubredditListingDataFail() { } }); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { subredditAutocompleteCall = null; } }); }; handler.postDelayed(autoCompleteRunnable, 500); } } else { binding.recentSearchQueryRecyclerViewSearchActivity.setVisibility(View.VISIBLE); binding.subredditAutocompleteRecyclerViewSearchActivity.setVisibility(View.GONE); binding.clearSearchEditViewSearchActivity.setVisibility(View.GONE); } } }); binding.searchEditTextSearchActivity.setOnEditorActionListener((v, actionId, event) -> { if ((actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_SEARCH) || (event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN)) { if (!binding.searchEditTextSearchActivity.getText().toString().isEmpty()) { search(binding.searchEditTextSearchActivity.getText().toString()); return true; } } return false; }); binding.clearSearchEditViewSearchActivity.setOnClickListener(view -> { binding.searchEditTextSearchActivity.getText().clear(); }); binding.linkHandlerImageViewSearchActivity.setOnClickListener(view -> { if (!binding.searchEditTextSearchActivity.getText().toString().equals("")) { Intent intent = new Intent(this, LinkResolverActivity.class); intent.setData(Uri.parse(binding.searchEditTextSearchActivity.getText().toString())); startActivity(intent); finish(); } }); binding.viewAllSearchHistoryButtonSearchActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SearchHistoryActivity.class); startActivity(intent); }); binding.deleteAllRecentSearchesButtonSearchActivity.setOnClickListener(view -> { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.confirm) .setMessage(R.string.confirm_delete_all_recent_searches) .setPositiveButton(R.string.yes, (dialogInterface, i) -> { Utils.hideKeyboard(SearchActivity.this); executor.execute(() -> { List deletedQueries = mRedditDataRoomDatabase.recentSearchQueryDao().getAllRecentSearchQueries(accountName); mRedditDataRoomDatabase.recentSearchQueryDao().deleteAllRecentSearchQueries(accountName); handler.post(() -> Snackbar.make(view, R.string.all_recent_searches_deleted, Snackbar.LENGTH_LONG) .setAction(R.string.undo, v -> executor.execute(() -> mRedditDataRoomDatabase.recentSearchQueryDao().insertAll(deletedQueries))) .addCallback(new Snackbar.Callback() { @Override public void onDismissed(Snackbar transientBottomBar, int event) { Utils.showKeyboard(SearchActivity.this, handler, binding.searchEditTextSearchActivity); } }) .show()); }); }) .setNegativeButton(R.string.no, null) .show(); }); if (savedInstanceState != null) { searchInSubredditOrUserName = savedInstanceState.getString(SEARCH_IN_SUBREDDIT_OR_NAME_STATE); searchInThingType = savedInstanceState.getInt(SEARCH_IN_THING_TYPE_STATE); searchInMultiReddit = savedInstanceState.getParcelable(SEARCH_IN_MULTIREDDIT_STATE); setSearchInThingText(); } else { query = getIntent().getStringExtra(EXTRA_QUERY); searchInSubredditOrUserName = getIntent().getStringExtra(EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME); searchInMultiReddit = getIntent().getParcelableExtra(EXTRA_SEARCH_IN_MULTIREDDIT); searchInThingType = getIntent().getIntExtra(EXTRA_SEARCH_IN_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT); if (searchInSubredditOrUserName != null) { binding.subredditNameTextViewSearchActivity.setText(searchInSubredditOrUserName); } else if (searchInMultiReddit != null) { binding.subredditNameTextViewSearchActivity.setText(searchInMultiReddit.getDisplayName()); } } bindView(); requestThingSelectionForCurrentActivityLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { Intent returnIntent = result.getData(); if (returnIntent == null || returnIntent.getExtras() == null) { return; } searchInSubredditOrUserName = returnIntent.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); searchInMultiReddit = returnIntent.getParcelableExtra(SelectThingReturnKey.RETRUN_EXTRA_MULTIREDDIT); searchInThingType = returnIntent.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT); setSearchInThingText(); }); requestThingSelectionForAnotherActivityLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { setResult(RESULT_OK, result.getData()); finish(); }); if (searchOnlySubreddits || searchOnlyUsers || searchSubredditsAndUsers) { binding.subredditNameRelativeLayoutSearchActivity.setVisibility(View.GONE); binding.dividerSearchActivity.setVisibility(View.GONE); } else { binding.subredditNameRelativeLayoutSearchActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_EXTRA_CLEAR_SELECTION, true); requestThingSelectionForCurrentActivityLauncher.launch(intent); }); } } private void bindView() { if (!accountName.equals(Account.ANONYMOUS_ACCOUNT)) { adapter = new SearchActivityRecyclerViewAdapter(this, mCustomThemeWrapper, new SearchActivityRecyclerViewAdapter.ItemOnClickListener() { @Override public void onClick(RecentSearchQuery recentSearchQuery, boolean searchImmediately) { if (searchImmediately) { searchInSubredditOrUserName = recentSearchQuery.getSearchInSubredditOrUserName(); searchInMultiReddit = MultiReddit.getDummyMultiReddit(recentSearchQuery.getMultiRedditPath()); if (searchInMultiReddit != null && recentSearchQuery.getMultiRedditDisplayName() != null) { searchInMultiReddit.setDisplayName(recentSearchQuery.getMultiRedditDisplayName()); } searchInThingType = recentSearchQuery.getSearchInThingType(); search(recentSearchQuery.getSearchQuery()); } else { binding.searchEditTextSearchActivity.setText(recentSearchQuery.getSearchQuery()); binding.searchEditTextSearchActivity.setSelection(recentSearchQuery.getSearchQuery().length()); } } @Override public void onDelete(RecentSearchQuery recentSearchQuery) { Utils.hideKeyboard(SearchActivity.this); executor.execute(() -> { mRedditDataRoomDatabase.recentSearchQueryDao().deleteRecentSearchQueries(recentSearchQuery); Snackbar.make(binding.getRoot(), R.string.recent_search_deleted, Snackbar.LENGTH_SHORT) .setAction(R.string.undo, v -> executor.execute(() -> mRedditDataRoomDatabase.recentSearchQueryDao().insert(recentSearchQuery))) .addCallback(new Snackbar.Callback() { @Override public void onDismissed(Snackbar transientBottomBar, int event) { Utils.showKeyboard(SearchActivity.this, handler, binding.searchEditTextSearchActivity); } }) .show(); }); } }); binding.recentSearchQueryRecyclerViewSearchActivity.setVisibility(View.VISIBLE); binding.recentSearchQueryRecyclerViewSearchActivity.setNestedScrollingEnabled(false); binding.recentSearchQueryRecyclerViewSearchActivity.setAdapter(adapter); binding.recentSearchQueryRecyclerViewSearchActivity.addItemDecoration(new RecyclerView.ItemDecoration() { final int spacing = (int) Utils.convertDpToPixel(16, SearchActivity.this); final int halfSpacing = spacing / 2; @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int column = ((GridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); boolean toTheLeft = column == 0; if (toTheLeft) { outRect.left = spacing; outRect.right = halfSpacing; } else { outRect.left = halfSpacing; outRect.right = spacing; } outRect.top = spacing; } }); binding.subredditAutocompleteRecyclerViewSearchActivity.setAdapter(subredditAutocompleteRecyclerViewAdapter); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SEARCH_HISTORY, true)) { mRecentSearchQueryViewModel = new ViewModelProvider(this, new RecentSearchQueryViewModel.Factory(mRedditDataRoomDatabase, accountName)) .get(RecentSearchQueryViewModel.class); mRecentSearchQueryViewModel.getLimitedRecentSearchQueries().observe(this, recentSearchQueries -> { if (recentSearchQueries != null && !recentSearchQueries.isEmpty()) { binding.dividerSearchActivity.setVisibility(View.VISIBLE); binding.viewAllSearchHistoryButtonSearchActivity.setVisibility(View.VISIBLE); binding.deleteAllRecentSearchesButtonSearchActivity.setVisibility(View.VISIBLE); } else { binding.dividerSearchActivity.setVisibility(View.GONE); binding.viewAllSearchHistoryButtonSearchActivity.setVisibility(View.GONE); binding.deleteAllRecentSearchesButtonSearchActivity.setVisibility(View.GONE); } adapter.setRecentSearchQueries(recentSearchQueries); }); } } } private void search(String query) { if (query.equalsIgnoreCase("suicide") && mSharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_SUICIDE_PREVENTION_ACTIVITY, true)) { Intent intent = new Intent(this, SuicidePreventionActivity.class); intent.putExtra(SuicidePreventionActivity.EXTRA_QUERY, query); startActivityForResult(intent, SUICIDE_PREVENTION_ACTIVITY_REQUEST_CODE); } else { openSearchResult(query); } } private void openSearchResult(String query) { if (searchOnlySubreddits) { Intent intent = new Intent(SearchActivity.this, SearchSubredditsResultActivity.class); intent.putExtra(SearchSubredditsResultActivity.EXTRA_QUERY, query); intent.putExtra(SearchSubredditsResultActivity.EXTRA_IS_MULTI_SELECTION, getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)); startActivityForResult(intent, SUBREDDIT_SEARCH_REQUEST_CODE); } else if (searchOnlyUsers) { Intent intent = new Intent(this, SearchUsersResultActivity.class); intent.putExtra(SearchUsersResultActivity.EXTRA_QUERY, query); intent.putExtra(SearchUsersResultActivity.EXTRA_IS_MULTI_SELECTION, getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)); startActivityForResult(intent, USER_SEARCH_REQUEST_CODE); } else if (searchSubredditsAndUsers) { Intent intent = new Intent(this, SearchResultActivity.class); intent.putExtra(SearchResultActivity.EXTRA_QUERY, query); intent.putExtra(SearchResultActivity.EXTRA_SHOULD_RETURN_SUBREDDIT_AND_USER_NAME, true); requestThingSelectionForAnotherActivityLauncher.launch(intent); } else { Intent intent = new Intent(SearchActivity.this, SearchResultActivity.class); intent.putExtra(SearchResultActivity.EXTRA_QUERY, query); intent.putExtra(SearchResultActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, searchInSubredditOrUserName); intent.putExtra(SearchResultActivity.EXTRA_SEARCH_IN_MULTIREDDIT, searchInMultiReddit); intent.putExtra(SearchResultActivity.EXTRA_SEARCH_IN_THING_TYPE, searchInThingType); startActivity(intent); finish(); } } private void setSearchInThingText() { switch (searchInThingType) { case SelectThingReturnKey.THING_TYPE.SUBREDDIT: case SelectThingReturnKey.THING_TYPE.USER: if (searchInSubredditOrUserName == null) { binding.subredditNameTextViewSearchActivity.setText(R.string.all_subreddits); } else { binding.subredditNameTextViewSearchActivity.setText(searchInSubredditOrUserName); } break; case SelectThingReturnKey.THING_TYPE.MULTIREDDIT: binding.subredditNameTextViewSearchActivity.setText(searchInMultiReddit.getDisplayName()); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSearchActivity, null, binding.toolbar); int toolbarPrimaryTextAndIconColorColor = mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor(); binding.searchEditTextSearchActivity.setTextColor(toolbarPrimaryTextAndIconColorColor); binding.searchEditTextSearchActivity.setHintTextColor(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor()); binding.clearSearchEditViewSearchActivity.setColorFilter(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); binding.linkHandlerImageViewSearchActivity.setColorFilter(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); int colorAccent = mCustomThemeWrapper.getColorAccent(); binding.searchInTextViewSearchActivity.setTextColor(colorAccent); binding.subredditNameTextViewSearchActivity.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); binding.viewAllSearchHistoryButtonSearchActivity.setIconTint(ColorStateList.valueOf(mCustomThemeWrapper.getPrimaryIconColor())); binding.deleteAllRecentSearchesButtonSearchActivity.setIconTint(ColorStateList.valueOf(mCustomThemeWrapper.getPrimaryIconColor())); binding.dividerSearchActivity.setBackgroundColor(mCustomThemeWrapper.getDividerColor()); if (typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), typeface); } } @Override protected void onStart() { super.onStart(); binding.searchEditTextSearchActivity.requestFocus(); if (query != null) { binding.searchEditTextSearchActivity.setText(query); binding.searchEditTextSearchActivity.setSelection(query.length()); query = null; } Utils.showKeyboard(this, new Handler(), binding.searchEditTextSearchActivity); } @Override protected void onPause() { super.onPause(); Utils.hideKeyboard(this); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (resultCode == RESULT_OK && data != null) { if (requestCode == SUBREDDIT_SEARCH_REQUEST_CODE) { Intent returnIntent = new Intent(); if (getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)) { returnIntent.putParcelableArrayListExtra(RETURN_EXTRA_SELECTED_SUBREDDITS, data.getParcelableArrayListExtra(SearchSubredditsResultActivity.RETURN_EXTRA_SELECTED_SUBREDDITS)); } else { returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME, data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME)); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON, data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON)); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT); } setResult(Activity.RESULT_OK, returnIntent); finish(); } else if (requestCode == USER_SEARCH_REQUEST_CODE) { Intent returnIntent = new Intent(); if (getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)) { returnIntent.putStringArrayListExtra(RETURN_EXTRA_SELECTED_USERNAMES, data.getStringArrayListExtra(SearchUsersResultActivity.RETURN_EXTRA_SELECTED_USERNAMES)); } else { returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME, data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME)); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON, data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON)); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.USER); } setResult(Activity.RESULT_OK, returnIntent); finish(); } else if (requestCode == SUICIDE_PREVENTION_ACTIVITY_REQUEST_CODE) { openSearchResult(data.getStringExtra(SuicidePreventionActivity.EXTRA_RETURN_QUERY)); } } super.onActivityResult(requestCode, resultCode, data); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putString(SEARCH_IN_SUBREDDIT_OR_NAME_STATE, searchInSubredditOrUserName); outState.putInt(SEARCH_IN_THING_TYPE_STATE, searchInThingType); outState.putParcelable(SEARCH_IN_MULTIREDDIT_STATE, searchInMultiReddit); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SearchHistoryActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.SearchActivityRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySearchHistoryBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.recentsearchquery.RecentSearchQuery; import ml.docilealligator.infinityforreddit.recentsearchquery.RecentSearchQueryViewModel; import ml.docilealligator.infinityforreddit.utils.Utils; public class SearchHistoryActivity extends BaseActivity { public static final String EXTRA_ACCOUNT_NAME = "EAN"; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private ActivitySearchHistoryBinding binding; private SearchActivityRecyclerViewAdapter mAdapter; private Handler mHandler; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySearchHistoryBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSearchHistoryActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSearchHistoryActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewSearchHistoryActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } } binding.appbarLayoutSearchHistoryActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimary()); setSupportActionBar(binding.toolbarSearchHistoryActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setTitle(R.string.search_history); mHandler = new Handler(); mAdapter = new SearchActivityRecyclerViewAdapter(this, mCustomThemeWrapper, new SearchActivityRecyclerViewAdapter.ItemOnClickListener() { @Override public void onClick(RecentSearchQuery recentSearchQuery, boolean searchImmediately) { Intent intent = new Intent(SearchHistoryActivity.this, SearchResultActivity.class); intent.putExtra(SearchResultActivity.EXTRA_QUERY, recentSearchQuery.getSearchQuery()); intent.putExtra(SearchResultActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, recentSearchQuery.getSearchInSubredditOrUserName()); if (recentSearchQuery.getMultiRedditPath() != null) { MultiReddit multiReddit = MultiReddit.getDummyMultiReddit(recentSearchQuery.getMultiRedditPath()); if (multiReddit != null && recentSearchQuery.getMultiRedditDisplayName() != null) { multiReddit.setDisplayName(recentSearchQuery.getMultiRedditDisplayName()); } intent.putExtra(SearchResultActivity.EXTRA_SEARCH_IN_MULTIREDDIT, multiReddit); } intent.putExtra(SearchResultActivity.EXTRA_SEARCH_IN_THING_TYPE, recentSearchQuery.getSearchInThingType()); startActivity(intent); } @Override public void onDelete(RecentSearchQuery recentSearchQuery) { mExecutor.execute(() -> { mRedditDataRoomDatabase.recentSearchQueryDao().deleteRecentSearchQueries(recentSearchQuery); mHandler.post(() -> Snackbar.make(binding.getRoot(), R.string.recent_search_deleted, Snackbar.LENGTH_SHORT) .setAction(R.string.undo, v -> mExecutor.execute(() -> mRedditDataRoomDatabase.recentSearchQueryDao().insert(recentSearchQuery))) .show()); }); } }); binding.recyclerViewSearchHistoryActivity.setAdapter(mAdapter); binding.recyclerViewSearchHistoryActivity.addItemDecoration(new RecyclerView.ItemDecoration() { final int spacing = (int) Utils.convertDpToPixel(16, SearchHistoryActivity.this); final int halfSpacing = spacing / 2; @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int column = ((GridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); boolean toTheLeft = column == 0; if (toTheLeft) { outRect.left = spacing; outRect.right = halfSpacing; } else { outRect.left = halfSpacing; outRect.right = spacing; } outRect.top = spacing; } }); RecentSearchQueryViewModel viewModel = new ViewModelProvider(this, new RecentSearchQueryViewModel.Factory(mRedditDataRoomDatabase, accountName)) .get(RecentSearchQueryViewModel.class); viewModel.getAllRecentSearchQueries().observe(this, recentSearchQueries -> { if (recentSearchQueries == null || recentSearchQueries.isEmpty()) { binding.emptyTextViewSearchHistoryActivity.setVisibility(View.VISIBLE); binding.recyclerViewSearchHistoryActivity.setVisibility(View.GONE); } else { binding.emptyTextViewSearchHistoryActivity.setVisibility(View.GONE); binding.recyclerViewSearchHistoryActivity.setVisibility(View.VISIBLE); } mAdapter.setRecentSearchQueries(recentSearchQueries); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSearchHistoryActivity, binding.collapsingToolbarLayoutSearchHistoryActivity, binding.toolbarSearchHistoryActivity); binding.emptyTextViewSearchHistoryActivity.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (typeface != null) { binding.emptyTextViewSearchHistoryActivity.setTypeface(typeface); } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SearchResultActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.textfield.TextInputEditText; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.SubredditAutocompleteRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FABMoreOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SearchPostSortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SearchUserAndSubredditSortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTimeBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySearchResultBinding; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.FragmentCommunicator; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.fragments.SubredditListingFragment; import ml.docilealligator.infinityforreddit.fragments.UserListingFragment; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import ml.docilealligator.infinityforreddit.recentsearchquery.InsertRecentSearchQuery; import ml.docilealligator.infinityforreddit.subreddit.ParseSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class SearchResultActivity extends BaseActivity implements SortTypeSelectionCallback, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, ActivityToolbarInterface, FABMoreOptionsBottomSheetFragment.FABOptionSelectionCallback, PostTypeBottomSheetFragment.PostTypeSelectionCallback, RecyclerViewContentScrollingInterface, MarkPostAsReadInterface { public static final String EXTRA_QUERY = "EQ"; public static final String EXTRA_TRENDING_SOURCE = "ETS"; public static final String EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME = "ESISOUN"; public static final String EXTRA_SEARCH_IN_MULTIREDDIT = "ESIM"; public static final String EXTRA_SEARCH_IN_THING_TYPE = "ESITT"; public static final String EXTRA_SHOULD_RETURN_SUBREDDIT_AND_USER_NAME = "ESRSAUN"; private static final String INSERT_SEARCH_QUERY_SUCCESS_STATE = "ISQSS"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("bottom_app_bar") SharedPreferences bottomAppBarSharedPreference; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Runnable autoCompleteRunnable; private Call subredditAutocompleteCall; private String mQuery; private String mSearchInSubredditOrUserName; private MultiReddit mSearchInMultiReddit; @SelectThingReturnKey.THING_TYPE private int mSearchInThingType; private boolean mInsertSearchQuerySuccess; private boolean mReturnSubredditAndUserName; private FragmentManager fragmentManager; private SectionsPagerAdapter sectionsPagerAdapter; private int fabOption; private ActivitySearchResultBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySearchResultBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); mViewPager2 = binding.viewPagerSearchResultActivity; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSearchResultActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSearchResultActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.viewPagerSearchResultActivity.setPadding(allInsets.left, 0, allInsets.right, 0); setMargins(binding.fabSearchResultActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, SearchResultActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, SearchResultActivity.this) + allInsets.bottom); setMargins(binding.tabLayoutSearchResultActivity, allInsets.left, BaseActivity.IGNORE_MARGIN, allInsets.right, BaseActivity.IGNORE_MARGIN); return insets; } }); /*adjustToolbar(binding.toolbarSearchResultActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.fabSearchResultActivity.getLayoutParams(); params.bottomMargin += navBarHeight; binding.fabSearchResultActivity.setLayoutParams(params); }*/ } } setSupportActionBar(binding.toolbarSearchResultActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarSearchResultActivity); // Get the intent, verify the action and get the query Intent intent = getIntent(); String query = intent.getStringExtra(EXTRA_QUERY); mSearchInSubredditOrUserName = intent.getStringExtra(EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME); mSearchInMultiReddit = intent.getParcelableExtra(EXTRA_SEARCH_IN_MULTIREDDIT); mSearchInThingType = intent.getIntExtra(EXTRA_SEARCH_IN_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT); mReturnSubredditAndUserName = intent.getBooleanExtra(EXTRA_SHOULD_RETURN_SUBREDDIT_AND_USER_NAME, false); if (query != null) { mQuery = query; setTitle(query); } fragmentManager = getSupportFragmentManager(); if (savedInstanceState != null) { mInsertSearchQuerySuccess = savedInstanceState.getBoolean(INSERT_SEARCH_QUERY_SUCCESS_STATE); } bindView(savedInstanceState); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (sectionsPagerAdapter != null) { return sectionsPagerAdapter.handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSearchResultActivity, binding.collapsingToolbarLayoutSearchResultActivity, binding.toolbarSearchResultActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutSearchResultActivity); applyTabLayoutTheme(binding.tabLayoutSearchResultActivity); applyFABTheme(binding.fabSearchResultActivity); } private void bindView(Bundle savedInstanceState) { sectionsPagerAdapter = new SectionsPagerAdapter(this); binding.viewPagerSearchResultActivity.setAdapter(sectionsPagerAdapter); binding.viewPagerSearchResultActivity.setUserInputEnabled(!mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false)); binding.viewPagerSearchResultActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { binding.fabSearchResultActivity.show(); sectionsPagerAdapter.displaySortTypeInToolbar(); if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } } }); new TabLayoutMediator(binding.tabLayoutSearchResultActivity, binding.viewPagerSearchResultActivity, (tab, position) -> { if (mReturnSubredditAndUserName) { if (position == 0) { Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.subreddits)); } else { Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.users)); } } else { switch (position) { case 0: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.posts)); break; case 1: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.subreddits)); break; case 2: Utils.setTitleWithCustomFontToTab(typeface, tab, getString(R.string.users)); break; } } }).attach(); fixViewPager2Sensitivity(binding.viewPagerSearchResultActivity); if (savedInstanceState == null) { binding.viewPagerSearchResultActivity.setCurrentItem(Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.DEFAULT_SEARCH_RESULT_TAB, "0")), false); } fabOption = bottomAppBarSharedPreference.getInt(SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS); switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_refresh_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_sort_toolbar_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_post_layout_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_search_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_subreddit_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_user_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.fabSearchResultActivity.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { binding.fabSearchResultActivity.setImageResource(R.drawable.ic_hide_read_posts_day_night_24dp); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_filter_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: binding.fabSearchResultActivity.setImageResource(R.drawable.ic_keyboard_double_arrow_up_day_night_24dp); break; default: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.fabSearchResultActivity.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { binding.fabSearchResultActivity.setImageResource(R.drawable.ic_add_day_night_24dp); } break; } setOtherActivitiesFabContentDescription(binding.fabSearchResultActivity, fabOption); binding.fabSearchResultActivity.setOnClickListener(view -> { switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: { Fragment fragment = sectionsPagerAdapter.getCurrentFragment(); if (fragment instanceof PostFragment) { SearchPostSortTypeBottomSheetFragment searchPostSortTypeBottomSheetFragment = SearchPostSortTypeBottomSheetFragment.getNewInstance(((PostFragment) fragment).getSortType()); searchPostSortTypeBottomSheetFragment.show(getSupportFragmentManager(), searchPostSortTypeBottomSheetFragment.getTag()); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, mSearchInSubredditOrUserName); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_MULTIREDDIT, mSearchInMultiReddit); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, mSearchInThingType); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; default: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } }); binding.fabSearchResultActivity.setOnLongClickListener(view -> { FABMoreOptionsBottomSheetFragment fabMoreOptionsBottomSheetFragment = new FABMoreOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(FABMoreOptionsBottomSheetFragment.EXTRA_ANONYMOUS_MODE, accountName.equals(Account.ANONYMOUS_ACCOUNT)); fabMoreOptionsBottomSheetFragment.setArguments(bundle); fabMoreOptionsBottomSheetFragment.show(getSupportFragmentManager(), fabMoreOptionsBottomSheetFragment.getTag()); return true; }); if (!accountName.equals(Account.ANONYMOUS_ACCOUNT) && mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SEARCH_HISTORY, true) && !mInsertSearchQuerySuccess && mQuery != null) { InsertRecentSearchQuery.insertRecentSearchQueryListener(mExecutor, new Handler(getMainLooper()), mRedditDataRoomDatabase, accountName, mQuery, mSearchInSubredditOrUserName, mSearchInMultiReddit, mSearchInThingType, () -> mInsertSearchQuerySuccess = true); } } private void displaySortTypeBottomSheetFragment() { Fragment fragment = sectionsPagerAdapter.getCurrentFragment(); if (fragment instanceof PostFragment) { SearchPostSortTypeBottomSheetFragment searchPostSortTypeBottomSheetFragment = SearchPostSortTypeBottomSheetFragment.getNewInstance(((PostFragment) fragment).getSortType()); searchPostSortTypeBottomSheetFragment.show(getSupportFragmentManager(), searchPostSortTypeBottomSheetFragment.getTag()); } else { if (fragment instanceof SubredditListingFragment) { SearchUserAndSubredditSortTypeBottomSheetFragment searchUserAndSubredditSortTypeBottomSheetFragment = SearchUserAndSubredditSortTypeBottomSheetFragment.getNewInstance(binding.viewPagerSearchResultActivity.getCurrentItem(), ((SubredditListingFragment) fragment).getSortType()); searchUserAndSubredditSortTypeBottomSheetFragment.show(getSupportFragmentManager(), searchUserAndSubredditSortTypeBottomSheetFragment.getTag()); } else if (fragment instanceof UserListingFragment) { SearchUserAndSubredditSortTypeBottomSheetFragment searchUserAndSubredditSortTypeBottomSheetFragment = SearchUserAndSubredditSortTypeBottomSheetFragment.getNewInstance(binding.viewPagerSearchResultActivity.getCurrentItem(), ((UserListingFragment) fragment).getSortType()); searchUserAndSubredditSortTypeBottomSheetFragment.show(getSupportFragmentManager(), searchUserAndSubredditSortTypeBottomSheetFragment.getTag()); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.search_result_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { onBackPressed(); return true; } else if (itemId == R.id.action_sort_search_result_activity) { displaySortTypeBottomSheetFragment(); return true; } else if (itemId == R.id.action_search_search_result_activity) { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, mSearchInSubredditOrUserName); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_MULTIREDDIT, mSearchInMultiReddit); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, mSearchInThingType); intent.putExtra(SearchActivity.EXTRA_QUERY, mQuery); finish(); startActivity(intent); return true; } else if (itemId == R.id.action_refresh_search_result_activity) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } return true; } else if (itemId == R.id.action_change_post_layout_search_result_activity) { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } return super.onOptionsItemSelected(item); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(INSERT_SEARCH_QUERY_SUCCESS_STATE, mInsertSearchQuerySuccess); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Override public void sortTypeSelected(SortType sortType) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.changeSortType(sortType); } } @Override public void sortTypeSelected(String sortType) { Bundle bundle = new Bundle(); bundle.putString(SortTimeBottomSheetFragment.EXTRA_SORT_TYPE, sortType); SortTimeBottomSheetFragment sortTimeBottomSheetFragment = new SortTimeBottomSheetFragment(); sortTimeBottomSheetFragment.setArguments(bundle); sortTimeBottomSheetFragment.show(getSupportFragmentManager(), sortTimeBottomSheetFragment.getTag()); } @Override public void searchUserAndSubredditSortTypeSelected(SortType sortType, int fragmentPosition) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.changeSortType(sortType, fragmentPosition); } } @Override public void postLayoutSelected(int postLayout) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.changePostLayout(postLayout); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onChangeNSFWEvent(ChangeNSFWEvent changeNSFWEvent) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.changeNSFW(changeNSFWEvent.nsfw); } } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void displaySortType() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.displaySortTypeInToolbar(); } } @Override public void fabOptionSelected(int option) { switch (option) { case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SUBMIT_POST: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_REFRESH: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_SORT_TYPE: displaySortTypeBottomSheetFragment(); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_POST_LAYOUT: PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SEARCH: Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, mSearchInSubredditOrUserName); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_MULTIREDDIT, mSearchInMultiReddit); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, mSearchInThingType); startActivity(intent); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_SUBREDDIT: { goToSubreddit(); break; } case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_USER: { goToUser(); break; } case FABMoreOptionsBottomSheetFragment.FAB_HIDE_READ_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_FILTER_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_GO_TO_TOP: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; } } } private void goToSubreddit() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); RecyclerView recyclerView = rootView.findViewById(R.id.recycler_view_go_to_thing_edit_text); thingEditText.requestFocus(); SubredditAutocompleteRecyclerViewAdapter adapter = new SubredditAutocompleteRecyclerViewAdapter( this, mCustomThemeWrapper, subredditData -> { Utils.hideKeyboard(this); Intent intent = new Intent(SearchResultActivity.this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditData.getName()); startActivity(intent); }); recyclerView.setAdapter(adapter); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); return true; } return false; }); Handler handler = new Handler(); boolean nsfw = mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); thingEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { if (subredditAutocompleteCall != null && subredditAutocompleteCall.isExecuted()) { subredditAutocompleteCall.cancel(); } if (autoCompleteRunnable != null) { handler.removeCallbacks(autoCompleteRunnable); } } @Override public void afterTextChanged(Editable editable) { String currentQuery = editable.toString().trim(); if (!currentQuery.isEmpty()) { autoCompleteRunnable = () -> { subredditAutocompleteCall = mOauthRetrofit.create(RedditAPI.class).subredditAutocomplete(APIUtils.getOAuthHeader(accessToken), currentQuery, nsfw); subredditAutocompleteCall.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { subredditAutocompleteCall = null; if (response.isSuccessful()) { ParseSubredditData.parseSubredditListingData(mExecutor, handler, response.body(), nsfw, new ParseSubredditData.ParseSubredditListingDataListener() { @Override public void onParseSubredditListingDataSuccess(ArrayList subredditData, String after) { adapter.setSubreddits(subredditData); } @Override public void onParseSubredditListingDataFail() { } }); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { subredditAutocompleteCall = null; } }); }; handler.postDelayed(autoCompleteRunnable, 500); } } }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_subreddit) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } private void goToUser() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); thingEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); return true; } return false; }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_user) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } @Override public void postTypeSelected(int postType) { Intent intent; switch (postType) { case PostTypeBottomSheetFragment.TYPE_TEXT: intent = new Intent(this, PostTextActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_LINK: intent = new Intent(this, PostLinkActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_IMAGE: intent = new Intent(this, PostImageActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_VIDEO: intent = new Intent(this, PostVideoActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_GALLERY: intent = new Intent(this, PostGalleryActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_POLL: intent = new Intent(this, PostPollActivity.class); startActivity(intent); } } @Override public void contentScrollUp() { binding.fabSearchResultActivity.show(); } @Override public void contentScrollDown() { binding.fabSearchResultActivity.hide(); } @Override public void markPostAsRead(Post post) { int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, accountName, post.getId(), readPostsLimit); } private class SectionsPagerAdapter extends FragmentStateAdapter { public SectionsPagerAdapter(FragmentActivity fa) { super(fa); } @NonNull @Override public Fragment createFragment(int position) { if (mReturnSubredditAndUserName) { if (position == 0) { return createSubredditListingFragment(true); } return createUserListingFragment(true); } else { switch (position) { case 0: { return createPostFragment(); } case 1: { return createSubredditListingFragment(false); } default: { return createUserListingFragment(false); } } } } private Fragment createPostFragment() { PostFragment mFragment = new PostFragment(); Bundle bundle = new Bundle(); switch (mSearchInThingType) { case SelectThingReturnKey.THING_TYPE.SUBREDDIT: bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_SEARCH); bundle.putString(PostFragment.EXTRA_NAME, mSearchInSubredditOrUserName); break; case SelectThingReturnKey.THING_TYPE.USER: bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_SEARCH); bundle.putString(PostFragment.EXTRA_NAME, "u_" + mSearchInSubredditOrUserName); break; case SelectThingReturnKey.THING_TYPE.MULTIREDDIT: bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_MULTI_REDDIT); bundle.putString(PostFragment.EXTRA_NAME, mSearchInMultiReddit.getPath()); } bundle.putString(PostFragment.EXTRA_QUERY, mQuery); bundle.putString(PostFragment.EXTRA_TRENDING_SOURCE, getIntent().getStringExtra(EXTRA_TRENDING_SOURCE)); mFragment.setArguments(bundle); return mFragment; } private Fragment createSubredditListingFragment(boolean returnSubredditName) { SubredditListingFragment mFragment = new SubredditListingFragment(); Bundle bundle = new Bundle(); bundle.putString(SubredditListingFragment.EXTRA_QUERY, mQuery); bundle.putBoolean(SubredditListingFragment.EXTRA_IS_GETTING_SUBREDDIT_INFO, returnSubredditName); mFragment.setArguments(bundle); return mFragment; } private Fragment createUserListingFragment(boolean returnUsername) { UserListingFragment mFragment = new UserListingFragment(); Bundle bundle = new Bundle(); bundle.putString(UserListingFragment.EXTRA_QUERY, mQuery); bundle.putBoolean(UserListingFragment.EXTRA_IS_GETTING_USER_INFO, returnUsername); mFragment.setArguments(bundle); return mFragment; } @Nullable private Fragment getCurrentFragment() { if (fragmentManager == null) { return null; } return fragmentManager.findFragmentByTag("f" + binding.viewPagerSearchResultActivity.getCurrentItem()); } public boolean handleKeyDown(int keyCode) { if (binding.viewPagerSearchResultActivity.getCurrentItem() == 0) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { return ((PostFragment) fragment).handleKeyDown(keyCode); } } return false; } void changeSortType(SortType sortType) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeSortType(sortType); displaySortTypeInToolbar(); } } void changeSortType(SortType sortType, int fragmentPosition) { Fragment fragment = fragmentManager.findFragmentByTag("f" + fragmentPosition); if (fragment instanceof SubredditListingFragment) { ((SubredditListingFragment) fragment).changeSortType(sortType); } else if (fragment instanceof UserListingFragment) { ((UserListingFragment) fragment).changeSortType(sortType); } displaySortTypeInToolbar(); } public void refresh() { Fragment fragment = getCurrentFragment(); if (fragment instanceof FragmentCommunicator) { ((FragmentCommunicator) fragment).refresh(); } } void changeNSFW(boolean nsfw) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeNSFW(nsfw); } } void changePostLayout(int postLayout) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changePostLayout(postLayout); } } void goBackToTop() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).goBackToTop(); } else if (fragment instanceof SubredditListingFragment) { ((SubredditListingFragment) fragment).goBackToTop(); } else if (fragment instanceof UserListingFragment) { ((UserListingFragment) fragment).goBackToTop(); } } void displaySortTypeInToolbar() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { SortType sortType = ((PostFragment) fragment).getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.toolbarSearchResultActivity); } else if (fragment instanceof SubredditListingFragment) { SortType sortType = ((SubredditListingFragment) fragment).getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.toolbarSearchResultActivity); } else if (fragment instanceof UserListingFragment) { SortType sortType = ((UserListingFragment) fragment).getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.toolbarSearchResultActivity); } } void filterPosts() { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { ((PostFragment) fragment).filterPosts(); } } void hideReadPosts() { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { ((PostFragment) fragment).hideReadPosts(); } } @Override public int getItemCount() { if (mReturnSubredditAndUserName) { return 2; } return 3; } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SearchSubredditsResultActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySearchSubredditsResultBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.SubredditListingFragment; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.utils.Utils; public class SearchSubredditsResultActivity extends BaseActivity implements ActivityToolbarInterface { static final String EXTRA_QUERY = "EQ"; static final String EXTRA_IS_MULTI_SELECTION = "EIMS"; static final String RETURN_EXTRA_SELECTED_SUBREDDITS = "RESS"; private static final String FRAGMENT_OUT_STATE = "FOS"; Fragment mFragment; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private ActivitySearchSubredditsResultBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySearchSubredditsResultBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSearchSubredditsResultActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSearchSubredditsResultActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.frameLayoutSearchSubredditsResultActivity.setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return insets; } }); //adjustToolbar(binding.toolbarSearchSubredditsResultActivity); } } setSupportActionBar(binding.toolbarSearchSubredditsResultActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarSearchSubredditsResultActivity); String query = getIntent().getExtras().getString(EXTRA_QUERY); if (savedInstanceState == null) { mFragment = new SubredditListingFragment(); Bundle bundle = new Bundle(); bundle.putString(SubredditListingFragment.EXTRA_QUERY, query); bundle.putBoolean(SubredditListingFragment.EXTRA_IS_GETTING_SUBREDDIT_INFO, true); bundle.putBoolean(SubredditListingFragment.EXTRA_IS_MULTI_SELECTION, getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)); mFragment.setArguments(bundle); } else { mFragment = getSupportFragmentManager().getFragment(savedInstanceState, FRAGMENT_OUT_STATE); } getSupportFragmentManager().beginTransaction() .replace(R.id.frame_layout_search_subreddits_result_activity, mFragment) .commit(); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSearchSubredditsResultActivity, null, binding.toolbarSearchSubredditsResultActivity); } @Override public boolean onCreateOptionsMenu(Menu menu) { if (getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)) { getMenuInflater().inflate(R.menu.search_subreddits_result_activity, menu); applyMenuItemTheme(menu); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_save_search_subreddits_result_activity) { if (mFragment != null) { ArrayList selectedSubreddits = ((SubredditListingFragment) mFragment).getSelectedSubredditNames(); Intent returnIntent = new Intent(); returnIntent.putParcelableArrayListExtra(RETURN_EXTRA_SELECTED_SUBREDDITS, selectedSubreddits); setResult(Activity.RESULT_OK, returnIntent); finish(); } } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); getSupportFragmentManager().putFragment(outState, FRAGMENT_OUT_STATE, mFragment); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Override public void onLongPress() { if (mFragment != null) { ((SubredditListingFragment) mFragment).goBackToTop(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SearchUsersResultActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySearchUsersResultBinding; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.UserListingFragment; import ml.docilealligator.infinityforreddit.utils.Utils; public class SearchUsersResultActivity extends BaseActivity implements ActivityToolbarInterface { static final String EXTRA_QUERY = "EQ"; static final String EXTRA_IS_MULTI_SELECTION = "EIMS"; static final String RETURN_EXTRA_SELECTED_USERNAMES = "RESU"; private static final String FRAGMENT_OUT_STATE = "FOS"; Fragment mFragment; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private ActivitySearchUsersResultBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySearchUsersResultBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSearchUsersResultActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSearchUsersResultActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.frameLayoutSearchUsersResultActivity.setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return insets; } }); //adjustToolbar(binding.toolbarSearchUsersResultActivity); } } setSupportActionBar(binding.toolbarSearchUsersResultActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarSearchUsersResultActivity); String query = getIntent().getExtras().getString(EXTRA_QUERY); if (savedInstanceState == null) { mFragment = new UserListingFragment(); Bundle bundle = new Bundle(); bundle.putString(UserListingFragment.EXTRA_QUERY, query); bundle.putBoolean(UserListingFragment.EXTRA_IS_GETTING_USER_INFO, true); bundle.putBoolean(UserListingFragment.EXTRA_IS_MULTI_SELECTION, getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)); mFragment.setArguments(bundle); } else { mFragment = getSupportFragmentManager().getFragment(savedInstanceState, FRAGMENT_OUT_STATE); } getSupportFragmentManager().beginTransaction() .replace(R.id.frame_layout_search_users_result_activity, mFragment) .commit(); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSearchUsersResultActivity, null, binding.toolbarSearchUsersResultActivity); } @Override public boolean onCreateOptionsMenu(Menu menu) { if (getIntent().getBooleanExtra(EXTRA_IS_MULTI_SELECTION, false)) { getMenuInflater().inflate(R.menu.search_users_result_activity, menu); applyMenuItemTheme(menu); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_save_search_users_result_activity) { if (mFragment != null) { ArrayList selectedUsernames = ((UserListingFragment) mFragment).getSelectedUsernames(); Intent returnIntent = new Intent(); returnIntent.putStringArrayListExtra(RETURN_EXTRA_SELECTED_USERNAMES, selectedUsernames); setResult(Activity.RESULT_OK, returnIntent); finish(); } return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); getSupportFragmentManager().putFragment(outState, FRAGMENT_OUT_STATE, mFragment); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Override public void onLongPress() { if (mFragment != null) { ((UserListingFragment) mFragment).goBackToTop(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SelectUserFlairActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.adapters.UserFlairRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivitySelectUserFlairBinding; import ml.docilealligator.infinityforreddit.user.FetchUserFlairs; import ml.docilealligator.infinityforreddit.user.SelectUserFlair; import ml.docilealligator.infinityforreddit.user.UserFlair; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class SelectUserFlairActivity extends BaseActivity implements ActivityToolbarInterface { public static final String EXTRA_SUBREDDIT_NAME = "ESN"; private static final String USER_FLAIRS_STATE = "UFS"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private LinearLayoutManagerBugFixed mLinearLayoutManager; private ArrayList mUserFlairs; private String mSubredditName; private UserFlairRecyclerViewAdapter mAdapter; private ActivitySelectUserFlairBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivitySelectUserFlairBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSelectUserFlairActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSelectUserFlairActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewSelectUserFlairActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } attachSliderPanelIfApplicable(); setSupportActionBar(binding.toolbarSelectUserFlairActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarSelectUserFlairActivity); mSubredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); setTitle(mSubredditName); if (savedInstanceState != null) { mUserFlairs = savedInstanceState.getParcelableArrayList(USER_FLAIRS_STATE); } bindView(); } private void bindView() { if (mUserFlairs == null) { FetchUserFlairs.fetchUserFlairsInSubreddit(mExecutor, mHandler, mOauthRetrofit, accessToken, mSubredditName, new FetchUserFlairs.FetchUserFlairsInSubredditListener() { @Override public void fetchSuccessful(ArrayList userFlairs) { mUserFlairs = userFlairs; instantiateRecyclerView(); } @Override public void fetchFailed() { } }); } else { instantiateRecyclerView(); } } private void instantiateRecyclerView() { mAdapter = new UserFlairRecyclerViewAdapter(this, mCustomThemeWrapper, mUserFlairs, (userFlair, editUserFlair) -> { if (editUserFlair) { View dialogView = getLayoutInflater().inflate(R.layout.dialog_edit_flair, null); EditText flairEditText = dialogView.findViewById(R.id.flair_edit_text_edit_flair_dialog); flairEditText.setText(userFlair.getText()); flairEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), flairEditText); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.edit_flair) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); userFlair.setText(flairEditText.getText().toString()); selectUserFlair(userFlair); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } else { if (userFlair == null) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.clear_user_flair) .setPositiveButton(R.string.yes, (dialogInterface, i) -> selectUserFlair(userFlair)) .setNegativeButton(R.string.no, null) .show(); } else { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.select_this_user_flair) .setMessage(userFlair.getText()) .setPositiveButton(R.string.yes, (dialogInterface, i) -> selectUserFlair(userFlair)) .setNegativeButton(R.string.no, null) .show(); } } }); mLinearLayoutManager = new LinearLayoutManagerBugFixed(SelectUserFlairActivity.this); binding.recyclerViewSelectUserFlairActivity.setLayoutManager(mLinearLayoutManager); binding.recyclerViewSelectUserFlairActivity.setAdapter(mAdapter); } private void selectUserFlair(@Nullable UserFlair userFlair) { SelectUserFlair.selectUserFlair(mExecutor, mHandler, mOauthRetrofit, accessToken, userFlair, mSubredditName, accountName, new SelectUserFlair.SelectUserFlairListener() { @Override public void success() { if (userFlair == null) { Toast.makeText(SelectUserFlairActivity.this, R.string.clear_user_flair_success, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(SelectUserFlairActivity.this, R.string.select_user_flair_success, Toast.LENGTH_SHORT).show(); } finish(); } @Override public void failed(String errorMessage) { if (errorMessage == null || errorMessage.equals("")) { if (userFlair == null) { Snackbar.make(binding.getRoot(), R.string.clear_user_flair_success, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.getRoot(), R.string.select_user_flair_success, Snackbar.LENGTH_SHORT).show(); } } else { Snackbar.make(binding.getRoot(), errorMessage, Snackbar.LENGTH_SHORT).show(); } } }); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(USER_FLAIRS_STATE, mUserFlairs); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSelectUserFlairActivity, null, binding.toolbarSelectUserFlairActivity); } @Override public void onLongPress() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SelectedSubredditsAndUsersActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.adapters.SelectedSubredditsRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SelectSubredditsOrUsersOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivitySelectedSubredditsBinding; import ml.docilealligator.infinityforreddit.multireddit.ExpandedSubredditInMultiReddit; import ml.docilealligator.infinityforreddit.subreddit.SubredditWithSelection; import ml.docilealligator.infinityforreddit.utils.Utils; public class SelectedSubredditsAndUsersActivity extends BaseActivity implements ActivityToolbarInterface { public static final String EXTRA_SELECTED_SUBREDDITS = "ESS"; public static final String EXTRA_RETURN_SELECTED_SUBREDDITS = "ERSS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 1; private static final int USER_SELECTION_REQUEST_CODE = 2; private static final String SELECTED_SUBREDDITS_STATE = "SSS"; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private LinearLayoutManagerBugFixed linearLayoutManager; private SelectedSubredditsRecyclerViewAdapter adapter; private ArrayList subreddits; private ActivitySelectedSubredditsBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivitySelectedSubredditsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); attachSliderPanelIfApplicable(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSelectedSubredditsAndUsersActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSelectedSubredditsAndUsersActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewSelectedSubredditsAndUsersActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom); setMargins(binding.fabSelectedSubredditsAndUsersActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, SelectedSubredditsAndUsersActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, SelectedSubredditsAndUsersActivity.this) + allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarSelectedSubredditsAndUsersActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarSelectedSubredditsAndUsersActivity); if (savedInstanceState != null) { subreddits = savedInstanceState.getParcelableArrayList(SELECTED_SUBREDDITS_STATE); } else { subreddits = getIntent().getParcelableArrayListExtra(EXTRA_SELECTED_SUBREDDITS); } Collections.sort(subreddits, Comparator.comparing(ExpandedSubredditInMultiReddit::getName)); adapter = new SelectedSubredditsRecyclerViewAdapter(this, mCustomThemeWrapper, Glide.with(this), subreddits); linearLayoutManager = new LinearLayoutManagerBugFixed(this); binding.recyclerViewSelectedSubredditsAndUsersActivity.setLayoutManager(linearLayoutManager); binding.recyclerViewSelectedSubredditsAndUsersActivity.setAdapter(adapter); binding.recyclerViewSelectedSubredditsAndUsersActivity.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (dy > 0) { binding.fabSelectedSubredditsAndUsersActivity.hide(); } else { binding.fabSelectedSubredditsAndUsersActivity.show(); } } }); binding.fabSelectedSubredditsAndUsersActivity.setOnClickListener(view -> { SelectSubredditsOrUsersOptionsBottomSheetFragment selectSubredditsOrUsersOptionsBottomSheetFragment = new SelectSubredditsOrUsersOptionsBottomSheetFragment(); selectSubredditsOrUsersOptionsBottomSheetFragment.show(getSupportFragmentManager(), selectSubredditsOrUsersOptionsBottomSheetFragment.getTag()); }); } public void selectSubreddits() { Intent intent = new Intent(this, SubredditMultiselectionActivity.class); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); } public void selectUsers() { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); intent.putExtra(SearchActivity.EXTRA_IS_MULTI_SELECTION, true); startActivityForResult(intent, USER_SELECTION_REQUEST_CODE); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.selected_subreddits_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_save_selected_subreddits_activity) { if (adapter != null) { Intent returnIntent = new Intent(); returnIntent.putExtra(EXTRA_RETURN_SELECTED_SUBREDDITS, adapter.getSubreddits()); setResult(Activity.RESULT_OK, returnIntent); } finish(); } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { if (data != null) { ArrayList subredditWithSelections = data.getParcelableArrayListExtra(SubredditMultiselectionActivity.EXTRA_RETURN_SELECTED_SUBREDDITS); subreddits = new ArrayList<>(subredditWithSelections.stream().map( (subredditWithSelection) -> new ExpandedSubredditInMultiReddit(subredditWithSelection.getName(), subredditWithSelection.getIconUrl()) ).collect(Collectors.toList())); adapter.addSubreddits(subreddits); } } else if (requestCode == USER_SELECTION_REQUEST_CODE) { if (data != null) { if (subreddits == null) { subreddits = new ArrayList<>(); } ArrayList selectedUsernames = data.getStringArrayListExtra(SearchActivity.RETURN_EXTRA_SELECTED_USERNAMES); if (selectedUsernames != null) { for (String username : selectedUsernames) { adapter.addUserInSubredditType("u_" + username); } } } } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (adapter != null) { outState.putParcelableArrayList(SELECTED_SUBREDDITS_STATE, adapter.getSubreddits()); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSelectedSubredditsAndUsersActivity, binding.collapsingToolbarLayoutSelectedSubredditsAndUsersActivity, binding.toolbarSelectedSubredditsAndUsersActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutSelectedSubredditsAndUsersActivity); applyFABTheme(binding.fabSelectedSubredditsAndUsersActivity); } @Override public void onLongPress() { if (linearLayoutManager != null) { linearLayoutManager.scrollToPositionWithOffset(0, 0); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SendPrivateMessageActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.SharedPreferences; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.snackbar.Snackbar; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySendPrivateMessageBinding; import ml.docilealligator.infinityforreddit.message.ComposeMessage; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class SendPrivateMessageActivity extends BaseActivity { public static final String EXTRA_RECIPIENT_USERNAME = "ERU"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String mAccessToken; private boolean isSubmitting = false; private ActivitySendPrivateMessageBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivitySendPrivateMessageBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSendPrivateMessageActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarSendPrivateMessageActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.nestedScrollViewSendPrivateMesassgeActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } mAccessToken = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCESS_TOKEN, null); setSupportActionBar(binding.toolbarSendPrivateMessageActivity); String username = getIntent().getStringExtra(EXTRA_RECIPIENT_USERNAME); if (username != null) { binding.usernameEditTextSendPrivateMessageActivity.setText(username); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.send_private_message_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_send_send_private_message_activity) { if (!isSubmitting) { isSubmitting = true; if (binding.usernameEditTextSendPrivateMessageActivity.getText() == null || binding.usernameEditTextSendPrivateMessageActivity.getText().toString().equals("")) { isSubmitting = false; Snackbar.make(binding.getRoot(), R.string.message_username_required, Snackbar.LENGTH_LONG).show(); return true; } if (binding.subjetEditTextSendPrivateMessageActivity.getText() == null || binding.subjetEditTextSendPrivateMessageActivity.getText().toString().equals("")) { isSubmitting = false; Snackbar.make(binding.getRoot(), R.string.message_subject_required, Snackbar.LENGTH_LONG).show(); return true; } if (binding.contentEditTextSendPrivateMessageActivity.getText() == null || binding.contentEditTextSendPrivateMessageActivity.getText().toString().equals("")) { isSubmitting = false; Snackbar.make(binding.getRoot(), R.string.message_content_required, Snackbar.LENGTH_LONG).show(); return true; } item.setEnabled(false); item.getIcon().setAlpha(130); Snackbar sendingSnackbar = Snackbar.make(binding.getRoot(), R.string.sending_message, Snackbar.LENGTH_INDEFINITE); sendingSnackbar.show(); ComposeMessage.composeMessage(mExecutor, mHandler, mOauthRetrofit, mAccessToken, getResources().getConfiguration().locale, binding.usernameEditTextSendPrivateMessageActivity.getText().toString(), binding.subjetEditTextSendPrivateMessageActivity.getText().toString(), binding.contentEditTextSendPrivateMessageActivity.getText().toString(), new ComposeMessage.ComposeMessageListener() { @Override public void composeMessageSuccess() { isSubmitting = false; item.setEnabled(true); item.getIcon().setAlpha(255); Toast.makeText(SendPrivateMessageActivity.this, R.string.send_message_success, Toast.LENGTH_SHORT).show(); finish(); } @Override public void composeMessageFailed(String errorMessage) { isSubmitting = false; sendingSnackbar.dismiss(); item.setEnabled(true); item.getIcon().setAlpha(255); if (errorMessage == null || errorMessage.equals("")) { Snackbar.make(binding.getRoot(), R.string.send_message_failed, Snackbar.LENGTH_LONG).show(); } else { Snackbar.make(binding.getRoot(), errorMessage, Snackbar.LENGTH_LONG).show(); } } }); } } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSendPrivateMessageActivity, null, binding.toolbarSendPrivateMessageActivity); int primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.usernameEditTextSendPrivateMessageActivity.setTextColor(primaryTextColor); binding.subjetEditTextSendPrivateMessageActivity.setTextColor(primaryTextColor); binding.contentEditTextSendPrivateMessageActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.usernameEditTextSendPrivateMessageActivity.setHintTextColor(secondaryTextColor); binding.subjetEditTextSendPrivateMessageActivity.setHintTextColor(secondaryTextColor); binding.contentEditTextSendPrivateMessageActivity.setHintTextColor(secondaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1SendPrivateMessageActivity.setBackgroundColor(dividerColor); binding.divider2SendPrivateMessageActivity.setBackgroundColor(dividerColor); if (typeface != null) { binding.usernameEditTextSendPrivateMessageActivity.setTypeface(typeface); binding.subjetEditTextSendPrivateMessageActivity.setTypeface(typeface); binding.contentEditTextSendPrivateMessageActivity.setTypeface(typeface); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SettingsActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySettingsBinding; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.settings.AboutPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.AdvancedPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.DebugPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.APIKeysPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.FontPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.GesturesAndButtonsPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.InterfacePreferenceFragment; import ml.docilealligator.infinityforreddit.settings.MainPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.PostPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.SettingsSearchFragment; import ml.docilealligator.infinityforreddit.settings.SettingsSearchRegistry; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesLiveDataKt; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class SettingsActivity extends BaseActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { private static final String TITLE_STATE = "TS"; private ActivitySettingsBinding binding; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivitySettingsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSettingsActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSettingsActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); return insets; } }); } } setSupportActionBar(binding.toolbarSettingsActivity); SettingsSearchRegistry.getInstance().buildRegistry(getApplicationContext()); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() .replace(R.id.frame_layout_settings_activity, new MainPreferenceFragment()) .commit(); } else { setTitle(savedInstanceState.getCharSequence(TITLE_STATE)); } getSupportFragmentManager().addOnBackStackChangedListener(() -> { invalidateOptionsMenu(); if (getSupportFragmentManager().getBackStackEntryCount() == 0) { setTitle(R.string.settings_activity_label); setToolbarScrollLocked(mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_TOOLBAR, false)); return; } Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.frame_layout_settings_activity); if (fragment instanceof SettingsSearchFragment) { setTitle(R.string.settings_search_settings); setToolbarScrollLocked(true); } else { setToolbarScrollLocked(mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_TOOLBAR, false)); if (fragment instanceof AboutPreferenceFragment) { setTitle(R.string.settings_about_master_title); } else if (fragment instanceof InterfacePreferenceFragment) { setTitle(R.string.settings_interface_title); } else if (fragment instanceof FontPreferenceFragment) { setTitle(R.string.settings_font_title); } else if (fragment instanceof GesturesAndButtonsPreferenceFragment) { setTitle(R.string.settings_gestures_and_buttons_title); } else if (fragment instanceof PostPreferenceFragment) { setTitle(R.string.settings_category_post_title); } else if (fragment instanceof AdvancedPreferenceFragment) { setTitle(R.string.settings_advanced_master_title); } else if (fragment instanceof APIKeysPreferenceFragment) { setTitle(R.string.settings_api_keys_title); } else if (fragment instanceof DebugPreferenceFragment) { setTitle(R.string.settings_debug_title); } else if (fragment instanceof MainPreferenceFragment) { setTitle(R.string.settings_activity_label); } } }); SharedPreferencesLiveDataKt.booleanLiveData(mSharedPreferences, SharedPreferencesUtils.LOCK_TOOLBAR, false).observe(this, lock -> { Fragment current = getSupportFragmentManager().findFragmentById(R.id.frame_layout_settings_activity); setToolbarScrollLocked(lock || current instanceof SettingsSearchFragment); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSettingsActivity, binding.collapsingToolbarLayoutSettingsActivity, binding.toolbarSettingsActivity); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_settings, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { Fragment current = getSupportFragmentManager().findFragmentById(R.id.frame_layout_settings_activity); menu.findItem(R.id.action_search_settings).setVisible(!(current instanceof SettingsSearchFragment)); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { onBackPressed(); return true; } if (item.getItemId() == R.id.action_search_settings) { Fragment current = getSupportFragmentManager() .findFragmentById(R.id.frame_layout_settings_activity); if (!(current instanceof SettingsSearchFragment)) { SettingsSearchFragment searchFragment = new SettingsSearchFragment(); getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right) .replace(R.id.frame_layout_settings_activity, searchFragment) .addToBackStack(null) .commit(); binding.appbarLayoutSettingsActivity.setExpanded(true); setToolbarScrollLocked(true); setTitle(R.string.settings_search_settings); } return true; } return false; } public void navigateToSettingsFragment(Fragment fragment, int titleResId) { getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right) .replace(R.id.frame_layout_settings_activity, fragment) .addToBackStack(null) .commit(); binding.appbarLayoutSettingsActivity.setExpanded(true); setToolbarScrollLocked(mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_TOOLBAR, false)); setTitle(titleResId); } private void setToolbarScrollLocked(boolean locked) { AppBarLayout.LayoutParams p = (AppBarLayout.LayoutParams) binding.collapsingToolbarLayoutSettingsActivity.getLayoutParams(); p.setScrollFlags(locked ? SCROLL_FLAG_NO_SCROLL : SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS); binding.collapsingToolbarLayoutSettingsActivity.setLayoutParams(p); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putCharSequence(TITLE_STATE, getTitle()); } @Override public boolean onSupportNavigateUp() { if (getSupportFragmentManager().popBackStackImmediate()) { return true; } return super.onSupportNavigateUp(); } @Override public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat caller, Preference pref) { // Instantiate the new Fragment final Bundle args = pref.getExtras(); final Fragment fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment()); fragment.setArguments(args); fragment.setTargetFragment(caller, 0); getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right) .replace(R.id.frame_layout_settings_activity, fragment) .addToBackStack(null) .commit(); binding.appbarLayoutSettingsActivity.setExpanded(true); setTitle(pref.getTitle()); return true; } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } public void showSnackbar(int stringId, int actionStringId, View.OnClickListener onClickListener) { Snackbar.make(binding.getRoot(), stringId, BaseTransientBottomBar.LENGTH_SHORT).setAction(actionStringId, onClickListener).show(); } @Subscribe(threadMode = ThreadMode.MAIN) public void onRecreateActivityEvent(RecreateActivityEvent recreateActivityEvent) { ActivityCompat.recreate(this); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ShareDataResolverActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Patterns; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import ml.docilealligator.infinityforreddit.R; public class ShareDataResolverActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent receivedIntent = getIntent(); String action = receivedIntent.getAction(); String type = receivedIntent.getType(); if (Intent.ACTION_SEND.equals(action) && type != null) { if ("text/plain".equals(type)) { String text = receivedIntent.getStringExtra(Intent.EXTRA_TEXT); if (text != null) { if (Patterns.WEB_URL.matcher(text).matches()) { //It's a link Intent intent = new Intent(this, PostLinkActivity.class); intent.putExtra(PostLinkActivity.EXTRA_LINK, text); startActivity(intent); } else { Intent intent = new Intent(this, PostTextActivity.class); intent.putExtra(PostTextActivity.EXTRA_CONTENT, text); startActivity(intent); } } else { Toast.makeText(this, R.string.no_data_received, Toast.LENGTH_SHORT).show(); } } else if (type.equals("image/gif")) { Uri videoUri = receivedIntent.getParcelableExtra(Intent.EXTRA_STREAM); if (videoUri != null) { Intent intent = new Intent(this, PostVideoActivity.class); intent.setData(videoUri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); } else { Toast.makeText(this, R.string.no_video_path_received, Toast.LENGTH_SHORT).show(); } } else if (type.startsWith("image/")) { Uri imageUri = receivedIntent.getParcelableExtra(Intent.EXTRA_STREAM); if (imageUri != null) { Intent intent = new Intent(this, PostImageActivity.class); intent.setData(imageUri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); } else { Toast.makeText(this, R.string.no_image_path_received, Toast.LENGTH_SHORT).show(); } } else if (type.startsWith("video/")) { Uri videoUri = receivedIntent.getParcelableExtra(Intent.EXTRA_STREAM); if (videoUri != null) { Intent intent = new Intent(this, PostVideoActivity.class); intent.setData(videoUri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); } else { Toast.makeText(this, R.string.no_video_path_received, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(this, R.string.cannot_handle_intent, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(this, R.string.cannot_handle_intent, Toast.LENGTH_SHORT).show(); } finish(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SubmitCrosspostActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.PersistableBundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.davemorrissey.labs.subscaleview.ImageSource; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.bottomsheetfragments.AccountChooserBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySubmitCrosspostBinding; import ml.docilealligator.infinityforreddit.events.SubmitCrosspostEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class SubmitCrosspostActivity extends BaseActivity implements FlairBottomSheetFragment.FlairSelectionCallback, AccountChooserBottomSheetFragment.AccountChooserListener { public static final String EXTRA_POST = "EP"; private static final String SELECTED_ACCOUNT_STATE = "SAS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String SUBREDDIT_ICON_STATE = "SIS"; private static final String SUBREDDIT_SELECTED_STATE = "SSS"; private static final String SUBREDDIT_IS_USER_STATE = "SIUS"; private static final String LOAD_SUBREDDIT_ICON_STATE = "LSIS"; private static final String IS_POSTING_STATE = "IPS"; private static final String FLAIR_STATE = "FS"; private static final String IS_SPOILER_STATE = "ISS"; private static final String IS_NSFW_STATE = "INS"; private static final int SUBREDDIT_SELECTION_REQUEST_CODE = 0; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Account selectedAccount; private Post post; private String iconUrl; private String subredditName; private boolean subredditSelected = false; private boolean subredditIsUser; private boolean loadSubredditIconSuccessful = true; private boolean isPosting; private int primaryTextColor; private int flairBackgroundColor; private int flairTextColor; private int spoilerBackgroundColor; private int spoilerTextColor; private int nsfwBackgroundColor; private int nsfwTextColor; private Flair flair; private boolean isSpoiler = false; private boolean isNSFW = false; private Resources resources; private Menu mMenu; private RequestManager mGlide; private FlairBottomSheetFragment flairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private ActivitySubmitCrosspostBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivitySubmitCrosspostBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSubmitCrosspostActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarSubmitCrosspostActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.nestedScrollViewSubmitCrosspostActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarSubmitCrosspostActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); mPostingSnackbar = Snackbar.make(binding.getRoot(), R.string.posting, Snackbar.LENGTH_INDEFINITE); resources = getResources(); post = getIntent().getParcelableExtra(EXTRA_POST); if (savedInstanceState != null) { selectedAccount = savedInstanceState.getParcelable(SELECTED_ACCOUNT_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); iconUrl = savedInstanceState.getString(SUBREDDIT_ICON_STATE); subredditSelected = savedInstanceState.getBoolean(SUBREDDIT_SELECTED_STATE); subredditIsUser = savedInstanceState.getBoolean(SUBREDDIT_IS_USER_STATE); loadSubredditIconSuccessful = savedInstanceState.getBoolean(LOAD_SUBREDDIT_ICON_STATE); isPosting = savedInstanceState.getBoolean(IS_POSTING_STATE); flair = savedInstanceState.getParcelable(FLAIR_STATE); isSpoiler = savedInstanceState.getBoolean(IS_SPOILER_STATE); isNSFW = savedInstanceState.getBoolean(IS_NSFW_STATE); if (selectedAccount != null) { mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewSubmitCrosspostActivity); binding.accountNameTextViewSubmitCrosspostActivity.setText(selectedAccount.getAccountName()); } else { loadCurrentAccount(); } if (subredditName != null) { binding.subredditNameTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewSubmitCrosspostActivity.setText(subredditName); binding.flairCustomTextViewSubmitCrosspostActivity.setVisibility(View.VISIBLE); if (!loadSubredditIconSuccessful) { loadSubredditIcon(); } } displaySubredditIcon(); if (isPosting) { mPostingSnackbar.show(); } if (flair != null) { binding.flairCustomTextViewSubmitCrosspostActivity.setText(flair.getText()); binding.flairCustomTextViewSubmitCrosspostActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewSubmitCrosspostActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewSubmitCrosspostActivity.setTextColor(flairTextColor); } if (isSpoiler) { binding.spoilerCustomTextViewSubmitCrosspostActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewSubmitCrosspostActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewSubmitCrosspostActivity.setTextColor(spoilerTextColor); } if (isNSFW) { binding.nsfwCustomTextViewSubmitCrosspostActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewSubmitCrosspostActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewSubmitCrosspostActivity.setTextColor(nsfwTextColor); } } else { isPosting = false; loadCurrentAccount(); mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewSubmitCrosspostActivity); if (post.isSpoiler()) { binding.spoilerCustomTextViewSubmitCrosspostActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewSubmitCrosspostActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewSubmitCrosspostActivity.setTextColor(spoilerTextColor); } if (post.isNSFW()) { binding.nsfwCustomTextViewSubmitCrosspostActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewSubmitCrosspostActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewSubmitCrosspostActivity.setTextColor(nsfwTextColor); } binding.postTitleEditTextSubmitCrosspostActivity.setText(post.getTitle()); } if (post.getPostType() == Post.TEXT_TYPE) { binding.postContentTextViewSubmitCrosspostActivity.setVisibility(View.VISIBLE); binding.postContentTextViewSubmitCrosspostActivity.setText(post.getSelfTextPlain()); } else if (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { binding.postContentTextViewSubmitCrosspostActivity.setVisibility(View.VISIBLE); binding.postContentTextViewSubmitCrosspostActivity.setText(post.getUrl()); } else { Post.Preview preview = getPreview(post); if (preview != null) { binding.frameLayoutSubmitCrosspostActivity.setVisibility(View.VISIBLE); mGlide.asBitmap().load(preview.getPreviewUrl()).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { binding.imageViewSubmitCrosspostActivity.setImage(ImageSource.bitmap(resource)); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); if (post.getPostType() == Post.VIDEO_TYPE || post.getPostType() == Post.GIF_TYPE) { binding.playButtonImageViewSubmitCrosspostActivity.setVisibility(View.VISIBLE); binding.playButtonImageViewSubmitCrosspostActivity.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_play_circle_36dp)); } else if (post.getPostType() == Post.GALLERY_TYPE) { binding.playButtonImageViewSubmitCrosspostActivity.setVisibility(View.VISIBLE); binding.playButtonImageViewSubmitCrosspostActivity.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_gallery_day_night_24dp)); } } } binding.accountLinearLayoutSubmitCrosspostActivity.setOnClickListener(view -> { AccountChooserBottomSheetFragment fragment = new AccountChooserBottomSheetFragment(); fragment.show(getSupportFragmentManager(), fragment.getTag()); }); binding.subredditIconGifImageViewSubmitCrosspostActivity.setOnClickListener(view -> { binding.subredditNameTextViewSubmitCrosspostActivity.performClick(); }); binding.subredditNameTextViewSubmitCrosspostActivity.setOnClickListener(view -> { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SPECIFIED_ACCOUNT, selectedAccount); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, SUBREDDIT_SELECTION_REQUEST_CODE); }); binding.rulesButtonSubmitCrosspostActivity.setOnClickListener(view -> { if (subredditName == null) { Snackbar.make(binding.getRoot(), R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, RulesActivity.class); if (subredditIsUser) { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { intent.putExtra(RulesActivity.EXTRA_SUBREDDIT_NAME, subredditName); } startActivity(intent); } }); binding.flairCustomTextViewSubmitCrosspostActivity.setOnClickListener(view -> { if (flair == null) { flairSelectionBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); if (subredditIsUser) { bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, "u_" + subredditName); } else { bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, subredditName); } flairSelectionBottomSheetFragment.setArguments(bundle); flairSelectionBottomSheetFragment.show(getSupportFragmentManager(), flairSelectionBottomSheetFragment.getTag()); } else { binding.flairCustomTextViewSubmitCrosspostActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewSubmitCrosspostActivity.setText(getString(R.string.flair)); flair = null; } }); binding.spoilerCustomTextViewSubmitCrosspostActivity.setOnClickListener(view -> { if (!isSpoiler) { binding.spoilerCustomTextViewSubmitCrosspostActivity.setBackgroundColor(spoilerBackgroundColor); binding.spoilerCustomTextViewSubmitCrosspostActivity.setBorderColor(spoilerBackgroundColor); binding.spoilerCustomTextViewSubmitCrosspostActivity.setTextColor(spoilerTextColor); isSpoiler = true; } else { binding.spoilerCustomTextViewSubmitCrosspostActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.spoilerCustomTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); isSpoiler = false; } }); binding.nsfwCustomTextViewSubmitCrosspostActivity.setOnClickListener(view -> { if (!isNSFW) { binding.nsfwCustomTextViewSubmitCrosspostActivity.setBackgroundColor(nsfwBackgroundColor); binding.nsfwCustomTextViewSubmitCrosspostActivity.setBorderColor(nsfwBackgroundColor); binding.nsfwCustomTextViewSubmitCrosspostActivity.setTextColor(nsfwTextColor); isNSFW = true; } else { binding.nsfwCustomTextViewSubmitCrosspostActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.nsfwCustomTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); isNSFW = false; } }); binding.receivePostReplyNotificationsLinearLayoutSubmitCrosspostActivity.setOnClickListener(view -> { binding.receivePostReplyNotificationsSwitchMaterialSubmitCrosspostActivity.performClick(); }); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (isPosting) { promptAlertDialog(R.string.exit_when_submit, R.string.exit_when_submit_post_detail); } else { if (!binding.postTitleEditTextSubmitCrosspostActivity.getText().toString().equals("")) { promptAlertDialog(R.string.discard, R.string.discard_detail); } else { setEnabled(false); triggerBackPress(); } } } }); } private void loadCurrentAccount() { Handler handler = new Handler(); mExecutor.execute(() -> { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); selectedAccount = account; handler.post(() -> { if (!isFinishing() && !isDestroyed() && account != null) { mGlide.load(account.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewSubmitCrosspostActivity); binding.accountNameTextViewSubmitCrosspostActivity.setText(account.getAccountName()); } }); }); } @Nullable private Post.Preview getPreview(Post post) { ArrayList previews = post.getPreviews(); if (previews != null && !previews.isEmpty()) { return previews.get(0); } return null; } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSubmitCrosspostActivity, null, binding.toolbarSubmitCrosspostActivity); primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.accountNameTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); int secondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.subredditNameTextViewSubmitCrosspostActivity.setTextColor(secondaryTextColor); binding.rulesButtonSubmitCrosspostActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); binding.rulesButtonSubmitCrosspostActivity.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); binding.receivePostReplyNotificationsTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); int dividerColor = mCustomThemeWrapper.getDividerColor(); binding.divider1SubmitCrosspostActivity.setBackgroundColor(dividerColor); binding.divider2SubmitCrosspostActivity.setBackgroundColor(dividerColor); binding.divider3SubmitCrosspostActivity.setBackgroundColor(dividerColor); binding.divider4SubmitCrosspostActivity.setBackgroundColor(dividerColor); flairBackgroundColor = mCustomThemeWrapper.getFlairBackgroundColor(); flairTextColor = mCustomThemeWrapper.getFlairTextColor(); spoilerBackgroundColor = mCustomThemeWrapper.getSpoilerBackgroundColor(); spoilerTextColor = mCustomThemeWrapper.getSpoilerTextColor(); nsfwBackgroundColor = mCustomThemeWrapper.getNsfwBackgroundColor(); nsfwTextColor = mCustomThemeWrapper.getNsfwTextColor(); binding.flairCustomTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.spoilerCustomTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.nsfwCustomTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.postTitleEditTextSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.postTitleEditTextSubmitCrosspostActivity.setHintTextColor(secondaryTextColor); binding.postContentTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.postContentTextViewSubmitCrosspostActivity.setHintTextColor(secondaryTextColor); binding.playButtonImageViewSubmitCrosspostActivity.setColorFilter(mCustomThemeWrapper.getMediaIndicatorIconColor(), PorterDuff.Mode.SRC_IN); binding.playButtonImageViewSubmitCrosspostActivity.setBackgroundTintList(ColorStateList.valueOf(mCustomThemeWrapper.getMediaIndicatorBackgroundColor())); if (typeface != null) { binding.subredditNameTextViewSubmitCrosspostActivity.setTypeface(typeface); binding.rulesButtonSubmitCrosspostActivity.setTypeface(typeface); binding.receivePostReplyNotificationsTextViewSubmitCrosspostActivity.setTypeface(typeface); binding.flairCustomTextViewSubmitCrosspostActivity.setTypeface(typeface); binding.spoilerCustomTextViewSubmitCrosspostActivity.setTypeface(typeface); binding.nsfwCustomTextViewSubmitCrosspostActivity.setTypeface(typeface); binding.postTitleEditTextSubmitCrosspostActivity.setTypeface(typeface); } if (contentTypeface != null) { binding.postContentTextViewSubmitCrosspostActivity.setTypeface(contentTypeface); } } private void displaySubredditIcon() { if (iconUrl != null && !iconUrl.equals("")) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.subredditIconGifImageViewSubmitCrosspostActivity); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.subredditIconGifImageViewSubmitCrosspostActivity); } } private void loadSubredditIcon() { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accessToken, accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { iconUrl = iconImageUrl; displaySubredditIcon(); loadSubredditIconSuccessful = true; }); } private void promptAlertDialog(int titleResId, int messageResId) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton(R.string.yes, (dialogInterface, i) -> finish()) .setNegativeButton(R.string.no, null) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.submit_crosspost_activity, menu); applyMenuItemTheme(menu); mMenu = menu; if (isPosting) { mMenu.findItem(R.id.action_send_submit_crosspost_activity).setEnabled(false); mMenu.findItem(R.id.action_send_submit_crosspost_activity).getIcon().setAlpha(130); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { triggerBackPress(); return true; } else if (itemId == R.id.action_send_submit_crosspost_activity) { if (!subredditSelected) { Snackbar.make(binding.getRoot(), R.string.select_a_subreddit, Snackbar.LENGTH_SHORT).show(); return true; } if (binding.postTitleEditTextSubmitCrosspostActivity.getText() == null || binding.postTitleEditTextSubmitCrosspostActivity.getText().toString().equals("")) { Snackbar.make(binding.getRoot(), R.string.title_required, Snackbar.LENGTH_SHORT).show(); return true; } isPosting = true; item.setEnabled(false); item.getIcon().setAlpha(130); mPostingSnackbar.show(); String subredditName; if (subredditIsUser) { subredditName = "u_" + binding.subredditNameTextViewSubmitCrosspostActivity.getText().toString(); } else { subredditName = binding.subredditNameTextViewSubmitCrosspostActivity.getText().toString(); } /*Intent intent = new Intent(this, SubmitPostService.class); intent.putExtra(SubmitPostService.EXTRA_ACCOUNT, selectedAccount); intent.putExtra(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); intent.putExtra(SubmitPostService.EXTRA_TITLE, binding.postTitleEditTextSubmitCrosspostActivity.getText().toString()); if (post.isCrosspost()) { intent.putExtra(SubmitPostService.EXTRA_CONTENT, "t3_" + post.getCrosspostParentId()); } else { intent.putExtra(SubmitPostService.EXTRA_CONTENT, post.getFullName()); } intent.putExtra(SubmitPostService.EXTRA_KIND, APIUtils.KIND_CROSSPOST); intent.putExtra(SubmitPostService.EXTRA_FLAIR, flair); intent.putExtra(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler); intent.putExtra(SubmitPostService.EXTRA_IS_NSFW, isNSFW); intent.putExtra(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialSubmitCrosspostActivity.isChecked()); intent.putExtra(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_CROSSPOST); ContextCompat.startForegroundService(this, intent);*/ PersistableBundle extras = new PersistableBundle(); extras.putString(SubmitPostService.EXTRA_ACCOUNT, selectedAccount.getJSONModel()); extras.putString(SubmitPostService.EXTRA_SUBREDDIT_NAME, subredditName); String title = binding.postTitleEditTextSubmitCrosspostActivity.getText().toString(); extras.putString(SubmitPostService.EXTRA_TITLE, title); if (post.isCrosspost()) { extras.putString(SubmitPostService.EXTRA_CONTENT, "t3_" + post.getCrosspostParentId()); } else { extras.putString(SubmitPostService.EXTRA_CONTENT, post.getFullName()); } extras.putString(SubmitPostService.EXTRA_KIND, APIUtils.KIND_CROSSPOST); if (flair != null) { extras.putString(SubmitPostService.EXTRA_FLAIR, flair.getJSONModel()); } extras.putInt(SubmitPostService.EXTRA_IS_SPOILER, isSpoiler ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_IS_NSFW, isNSFW ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, binding.receivePostReplyNotificationsSwitchMaterialSubmitCrosspostActivity.isChecked() ? 1 : 0); extras.putInt(SubmitPostService.EXTRA_POST_TYPE, SubmitPostService.EXTRA_POST_TYPE_CROSSPOST); // TODO: contentEstimatedBytes JobInfo jobInfo = SubmitPostService.constructJobInfo(this, title.length() * 2L + 20000, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SELECTED_ACCOUNT_STATE, selectedAccount); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(SUBREDDIT_ICON_STATE, iconUrl); outState.putBoolean(SUBREDDIT_SELECTED_STATE, subredditSelected); outState.putBoolean(SUBREDDIT_IS_USER_STATE, subredditIsUser); outState.putBoolean(LOAD_SUBREDDIT_ICON_STATE, loadSubredditIconSuccessful); outState.putBoolean(IS_POSTING_STATE, isPosting); outState.putParcelable(FLAIR_STATE, flair); outState.putBoolean(IS_SPOILER_STATE, isSpoiler); outState.putBoolean(IS_NSFW_STATE, isNSFW); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBREDDIT_SELECTION_REQUEST_CODE) { if (resultCode == RESULT_OK && data != null) { subredditName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); iconUrl = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON); subredditSelected = true; subredditIsUser = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT) == SelectThingReturnKey.THING_TYPE.USER; binding.subredditNameTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.subredditNameTextViewSubmitCrosspostActivity.setText(subredditName); displaySubredditIcon(); binding.flairCustomTextViewSubmitCrosspostActivity.setVisibility(View.VISIBLE); binding.flairCustomTextViewSubmitCrosspostActivity.setBackgroundColor(resources.getColor(android.R.color.transparent)); binding.flairCustomTextViewSubmitCrosspostActivity.setTextColor(primaryTextColor); binding.flairCustomTextViewSubmitCrosspostActivity.setText(getString(R.string.flair)); flair = null; } } } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void flairSelected(Flair flair) { this.flair = flair; binding.flairCustomTextViewSubmitCrosspostActivity.setText(flair.getText()); binding.flairCustomTextViewSubmitCrosspostActivity.setBackgroundColor(flairBackgroundColor); binding.flairCustomTextViewSubmitCrosspostActivity.setBorderColor(flairBackgroundColor); binding.flairCustomTextViewSubmitCrosspostActivity.setTextColor(flairTextColor); } @Override public void onAccountSelected(Account account) { if (account != null) { selectedAccount = account; mGlide.load(selectedAccount.getProfileImageUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(binding.accountIconGifImageViewSubmitCrosspostActivity); binding.accountNameTextViewSubmitCrosspostActivity.setText(selectedAccount.getAccountName()); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void onSubmitCrosspostEvent(SubmitCrosspostEvent submitCrosspostEvent) { isPosting = false; mPostingSnackbar.dismiss(); if (submitCrosspostEvent.postSuccess) { Intent intent = new Intent(this, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_DATA, submitCrosspostEvent.post); startActivity(intent); finish(); } else { mMenu.findItem(R.id.action_send_submit_crosspost_activity).setEnabled(true); mMenu.findItem(R.id.action_send_submit_crosspost_activity).getIcon().setAlpha(255); if (submitCrosspostEvent.errorMessage == null || submitCrosspostEvent.errorMessage.equals("")) { Snackbar.make(binding.getRoot(), R.string.post_failed, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(binding.getRoot(), submitCrosspostEvent.errorMessage.substring(0, 1).toUpperCase() + submitCrosspostEvent.errorMessage.substring(1), Snackbar.LENGTH_SHORT).show(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SubredditMultiselectionActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.SubredditMultiselectionRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivitySubscribedSubredditsMultiselectionBinding; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditWithSelection; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditViewModel; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class SubredditMultiselectionActivity extends BaseActivity implements ActivityToolbarInterface { static final String EXTRA_RETURN_SELECTED_SUBREDDITS = "ERSS"; public static final String EXTRA_GET_SELECTED_SUBREDDITS = "EGSS"; private static final int SUBREDDIT_SEARCH_REQUEST_CODE = 1; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; public SubscribedSubredditViewModel mSubscribedSubredditViewModel; private LinearLayoutManagerBugFixed mLinearLayoutManager; private SubredditMultiselectionRecyclerViewAdapter mAdapter; private RequestManager mGlide; private ActivitySubscribedSubredditsMultiselectionBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySubscribedSubredditsMultiselectionBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSubredditsMultiselectionActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarSubscribedSubredditsMultiselectionActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewSubscribedSubscribedSubredditsMultiselectionActivity.setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); /*adjustToolbar(binding.toolbarSubscribedSubredditsMultiselectionActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { binding.recyclerViewSubscribedSubscribedSubredditsMultiselectionActivity.setPadding(0, 0, 0, navBarHeight); }*/ } } setSupportActionBar(binding.toolbarSubscribedSubredditsMultiselectionActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); binding.swipeRefreshLayoutSubscribedSubscribedSubredditsMultiselectionActivity.setEnabled(false); bindView(); } private void bindView() { mLinearLayoutManager = new LinearLayoutManagerBugFixed(this); binding.recyclerViewSubscribedSubscribedSubredditsMultiselectionActivity.setLayoutManager(mLinearLayoutManager); mAdapter = new SubredditMultiselectionRecyclerViewAdapter(this, mCustomThemeWrapper); binding.recyclerViewSubscribedSubscribedSubredditsMultiselectionActivity.setAdapter(mAdapter); mSubscribedSubredditViewModel = new ViewModelProvider(this, new SubscribedSubredditViewModel.Factory(mRedditDataRoomDatabase, accountName)) .get(SubscribedSubredditViewModel.class); mSubscribedSubredditViewModel.getAllSubscribedSubreddits().observe(this, subscribedSubredditData -> { binding.swipeRefreshLayoutSubscribedSubscribedSubredditsMultiselectionActivity.setRefreshing(false); if (subscribedSubredditData == null || subscribedSubredditData.size() == 0) { binding.recyclerViewSubscribedSubscribedSubredditsMultiselectionActivity.setVisibility(View.GONE); binding.noSubscriptionsLinearLayoutSubscribedSubredditsMultiselectionActivity.setVisibility(View.VISIBLE); } else { binding.noSubscriptionsLinearLayoutSubscribedSubredditsMultiselectionActivity.setVisibility(View.GONE); binding.recyclerViewSubscribedSubscribedSubredditsMultiselectionActivity.setVisibility(View.VISIBLE); mGlide.clear(binding.noSubscriptionsImageViewSubscribedSubredditsMultiselectionActivity); } mAdapter.setSubscribedSubreddits(subscribedSubredditData, getIntent().getStringExtra(EXTRA_GET_SELECTED_SUBREDDITS)); }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.subreddit_multiselection_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_save_subreddit_multiselection_activity) { if (mAdapter != null) { Intent returnIntent = new Intent(); returnIntent.putParcelableArrayListExtra(EXTRA_RETURN_SELECTED_SUBREDDITS, mAdapter.getAllSelectedSubreddits()); setResult(RESULT_OK, returnIntent); } finish(); return true; } else if (itemId == R.id.action_search_subreddit_multiselection_activity) { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_SUBREDDITS, true); intent.putExtra(SearchActivity.EXTRA_IS_MULTI_SELECTION, true); startActivityForResult(intent, SUBREDDIT_SEARCH_REQUEST_CODE); } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBREDDIT_SEARCH_REQUEST_CODE && resultCode == RESULT_OK && data != null && mAdapter != null) { Intent returnIntent = new Intent(); ArrayList selectedSubreddits = mAdapter.getAllSelectedSubreddits(); ArrayList searchedSubreddits = data.getParcelableArrayListExtra(SearchActivity.RETURN_EXTRA_SELECTED_SUBREDDITS); if (searchedSubreddits != null) { selectedSubreddits.addAll(searchedSubreddits.stream().map( subredditData -> new SubredditWithSelection(subredditData.getName(), subredditData.getIconUrl()) ).collect(Collectors.toList())); } returnIntent.putParcelableArrayListExtra(EXTRA_RETURN_SELECTED_SUBREDDITS, selectedSubreddits); setResult(RESULT_OK, returnIntent); finish(); } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSubredditsMultiselectionActivity, binding.collapsingToolbarLayoutSubscribedSubredditsMultiselectionActivity, binding.toolbarSubscribedSubredditsMultiselectionActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutSubscribedSubredditsMultiselectionActivity); binding.errorTextViewSubscribedSubredditsMultiselectionActivity.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (typeface != null) { binding.errorTextViewSubscribedSubredditsMultiselectionActivity.setTypeface(typeface); } } @Override public void onLongPress() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SubscribedThingListingActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.asynctasks.DeleteMultiredditInDatabase; import ml.docilealligator.infinityforreddit.asynctasks.InsertMultireddit; import ml.docilealligator.infinityforreddit.asynctasks.InsertSubscribedThings; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySubscribedThingListingBinding; import ml.docilealligator.infinityforreddit.events.GoBackToMainPageEvent; import ml.docilealligator.infinityforreddit.events.RefreshMultiRedditsEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.FollowedUsersListingFragment; import ml.docilealligator.infinityforreddit.fragments.FragmentCommunicator; import ml.docilealligator.infinityforreddit.fragments.MultiRedditListingFragment; import ml.docilealligator.infinityforreddit.fragments.SubscribedSubredditsListingFragment; import ml.docilealligator.infinityforreddit.multireddit.DeleteMultiReddit; import ml.docilealligator.infinityforreddit.multireddit.FetchMyMultiReddits; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.network.AnyAccountAccessTokenAuthenticator; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.thing.FetchSubscribedThing; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import retrofit2.Retrofit; public class SubscribedThingListingActivity extends BaseActivity implements ActivityToolbarInterface { public static final String EXTRA_SHOW_MULTIREDDITS = "ESM"; public static final String EXTRA_THING_SELECTION_MODE = "ETSM"; public static final String EXTRA_THING_SELECTION_TYPE = "ETST"; public static final String EXTRA_SPECIFIED_ACCOUNT = "ESA"; public static final String EXTRA_EXTRA_CLEAR_SELECTION = "EECS"; public static final int EXTRA_THING_SELECTION_TYPE_ALL = 0; public static final int EXTRA_THING_SELECTION_TYPE_SUBREDDIT = 1; public static final int EXTRA_THING_SELECTION_TYPE_USER = 2; public static final int EXTRA_THING_SELECTION_TYPE_MULTIREDDIT = 3; private static final String INSERT_SUBSCRIBED_SUBREDDIT_STATE = "ISSS"; private static final String INSERT_MULTIREDDIT_STATE = "IMS"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private boolean mInsertSuccess; private boolean mInsertMultiredditSuccess; private boolean showMultiReddits; private boolean isThingSelectionMode; private int thingSelectionType; private String mAccountProfileImageUrl; private SectionsPagerAdapter sectionsPagerAdapter; private Menu mMenu; private ActivityResultLauncher requestSearchThingLauncher; private ActivitySubscribedThingListingBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySubscribedThingListingBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutSubscribedThingListingActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarSubscribedThingListingActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.viewPagerSubscribedThingListingActivity.setPadding(allInsets.left, 0, allInsets.right, 0); setMargins(binding.fabSubscribedThingListingActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, SubscribedThingListingActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, SubscribedThingListingActivity.this) + allInsets.bottom); return insets; } }); /*adjustToolbar(binding.toolbarSubscribedThingListingActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.fabSubscribedThingListingActivity.getLayoutParams(); params.bottomMargin += navBarHeight; binding.fabSubscribedThingListingActivity.setLayoutParams(params); }*/ } } setSupportActionBar(binding.toolbarSubscribedThingListingActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarSubscribedThingListingActivity); if (getIntent().hasExtra(EXTRA_SPECIFIED_ACCOUNT)) { Account specifiedAccount = getIntent().getParcelableExtra(EXTRA_SPECIFIED_ACCOUNT); if (specifiedAccount != null) { accessToken = specifiedAccount.getAccessToken(); accountName = specifiedAccount.getAccountName(); mAccountProfileImageUrl = specifiedAccount.getProfileImageUrl(); mOauthRetrofit = mOauthRetrofit.newBuilder().client(new OkHttpClient.Builder().authenticator(new AnyAccountAccessTokenAuthenticator(APIUtils.getClientId(getApplicationContext()), mRetrofit, mRedditDataRoomDatabase, specifiedAccount, mCurrentAccountSharedPreferences)) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build()) .build(); } else { mAccountProfileImageUrl = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCOUNT_IMAGE_URL, null); } } else { mAccountProfileImageUrl = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCOUNT_IMAGE_URL, null); } if (savedInstanceState != null) { mInsertSuccess = savedInstanceState.getBoolean(INSERT_SUBSCRIBED_SUBREDDIT_STATE); mInsertMultiredditSuccess = savedInstanceState.getBoolean(INSERT_MULTIREDDIT_STATE); } else { showMultiReddits = getIntent().getBooleanExtra(EXTRA_SHOW_MULTIREDDITS, false); } isThingSelectionMode = getIntent().getBooleanExtra(EXTRA_THING_SELECTION_MODE, false); thingSelectionType = getIntent().getIntExtra(EXTRA_THING_SELECTION_TYPE, EXTRA_THING_SELECTION_TYPE_ALL); if (isThingSelectionMode) { if (thingSelectionType == EXTRA_THING_SELECTION_TYPE_SUBREDDIT) { getSupportActionBar().setTitle(R.string.subreddit_selection_activity_label); } else if (thingSelectionType == EXTRA_THING_SELECTION_TYPE_MULTIREDDIT) { getSupportActionBar().setTitle(R.string.multireddit_selection_activity_label); } } if (isThingSelectionMode && thingSelectionType != EXTRA_THING_SELECTION_TYPE_ALL) { binding.tabLayoutSubscribedThingListingActivity.setVisibility(View.GONE); } if (accountName.equals(Account.ANONYMOUS_ACCOUNT) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binding.searchEditTextSubscribedThingListingActivity.setImeOptions(binding.searchEditTextSubscribedThingListingActivity.getImeOptions() | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING); } binding.searchEditTextSubscribedThingListingActivity.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override public void afterTextChanged(Editable editable) { sectionsPagerAdapter.changeSearchQuery(editable.toString()); } }); requestSearchThingLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { setResult(RESULT_OK, result.getData()); finish(); }); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (binding.searchEditTextSubscribedThingListingActivity.getVisibility() == View.VISIBLE) { Utils.hideKeyboard(SubscribedThingListingActivity.this); binding.searchEditTextSubscribedThingListingActivity.setVisibility(View.GONE); binding.searchEditTextSubscribedThingListingActivity.setText(""); mMenu.findItem(R.id.action_search_subscribed_thing_listing_activity).setVisible(true); sectionsPagerAdapter.changeSearchQuery(""); } else { setEnabled(false); triggerBackPress(); } } }); initializeViewPagerAndLoadSubscriptions(); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutSubscribedThingListingActivity, binding.collapsingToolbarLayoutSubscribedThingListingActivity, binding.toolbarSubscribedThingListingActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutSubscribedThingListingActivity); applyTabLayoutTheme(binding.tabLayoutSubscribedThingListingActivity); applyFABTheme(binding.fabSubscribedThingListingActivity); binding.searchEditTextSubscribedThingListingActivity.setTextColor(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor()); binding.searchEditTextSubscribedThingListingActivity.setHintTextColor(mCustomThemeWrapper.getToolbarSecondaryTextColor()); } private void initializeViewPagerAndLoadSubscriptions() { binding.fabSubscribedThingListingActivity.setOnClickListener(view -> { Intent intent = new Intent(this, CreateMultiRedditActivity.class); startActivity(intent); }); sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); binding.viewPagerSubscribedThingListingActivity.setAdapter(sectionsPagerAdapter); binding.viewPagerSubscribedThingListingActivity.setOffscreenPageLimit(3); if (binding.viewPagerSubscribedThingListingActivity.getCurrentItem() != 2) { binding.fabSubscribedThingListingActivity.hide(); } binding.viewPagerSubscribedThingListingActivity.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { if (position == 0) { unlockSwipeRightToGoBack(); binding.fabSubscribedThingListingActivity.hide(); } else { lockSwipeRightToGoBack(); if (position != 2) { binding.fabSubscribedThingListingActivity.hide(); } else { binding.fabSubscribedThingListingActivity.show(); } } } }); binding.tabLayoutSubscribedThingListingActivity.setupWithViewPager(binding.viewPagerSubscribedThingListingActivity); if (showMultiReddits) { binding.viewPagerSubscribedThingListingActivity.setCurrentItem(2, false); } loadSubscriptions(false); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.subscribed_thing_listing_activity, menu); mMenu = menu; applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_search_subscribed_thing_listing_activity) { if (isThingSelectionMode) { Intent intent = new Intent(this, SearchActivity.class); if (thingSelectionType == EXTRA_THING_SELECTION_TYPE_SUBREDDIT) { intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_SUBREDDITS, true); } else if (thingSelectionType == EXTRA_THING_SELECTION_TYPE_USER) { intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); } else if (thingSelectionType == EXTRA_THING_SELECTION_TYPE_MULTIREDDIT) { item.setVisible(false); binding.searchEditTextSubscribedThingListingActivity.setVisibility(View.VISIBLE); binding.searchEditTextSubscribedThingListingActivity.requestFocus(); if (binding.searchEditTextSubscribedThingListingActivity.requestFocus()) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(binding.searchEditTextSubscribedThingListingActivity, InputMethodManager.SHOW_IMPLICIT); } return true; } else { intent.putExtra(SearchActivity.EXTRA_SEARCH_SUBREDDITS_AND_USERS, true); } requestSearchThingLauncher.launch(intent); return true; } item.setVisible(false); binding.searchEditTextSubscribedThingListingActivity.setVisibility(View.VISIBLE); binding.searchEditTextSubscribedThingListingActivity.requestFocus(); if (binding.searchEditTextSubscribedThingListingActivity.requestFocus()) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(binding.searchEditTextSubscribedThingListingActivity, InputMethodManager.SHOW_IMPLICIT); } return true; } else if (item.getItemId() == android.R.id.home) { triggerBackPress(); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(INSERT_SUBSCRIBED_SUBREDDIT_STATE, mInsertSuccess); outState.putBoolean(INSERT_MULTIREDDIT_STATE, mInsertMultiredditSuccess); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } public void loadSubscriptions(boolean forceLoad) { if (!forceLoad && System.currentTimeMillis() - mCurrentAccountSharedPreferences.getLong(SharedPreferencesUtils.SUBSCRIBED_THINGS_SYNC_TIME, 0L) < 24 * 60 * 60 * 1000) { return; } if (!accountName.equals(Account.ANONYMOUS_ACCOUNT) && !(!forceLoad && mInsertSuccess)) { FetchSubscribedThing.fetchSubscribedThing(mExecutor, mHandler, mOauthRetrofit, accessToken, accountName, null, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new FetchSubscribedThing.FetchSubscribedThingListener() { @Override public void onFetchSubscribedThingSuccess(ArrayList subscribedSubredditData, ArrayList subscribedUserData, ArrayList subredditData) { mCurrentAccountSharedPreferences.edit().putLong(SharedPreferencesUtils.SUBSCRIBED_THINGS_SYNC_TIME, System.currentTimeMillis()).apply(); InsertSubscribedThings.insertSubscribedThings( mExecutor, new Handler(), mRedditDataRoomDatabase, accountName, subscribedSubredditData, subscribedUserData, subredditData, () -> { mInsertSuccess = true; sectionsPagerAdapter.stopRefreshProgressbar(); }); } @Override public void onFetchSubscribedThingFail() { mInsertSuccess = false; sectionsPagerAdapter.stopRefreshProgressbar(); Toast.makeText(SubscribedThingListingActivity.this, R.string.error_loading_subscriptions, Toast.LENGTH_SHORT).show(); } }); } if (!(!forceLoad && mInsertMultiredditSuccess)) { loadMultiReddits(); } } public void showFabInMultiredditTab() { if (binding.viewPagerSubscribedThingListingActivity.getCurrentItem() == 2) { binding.fabSubscribedThingListingActivity.show(); } } public void hideFabInMultiredditTab() { if (binding.viewPagerSubscribedThingListingActivity.getCurrentItem() == 2) { binding.fabSubscribedThingListingActivity.hide(); } } private void loadMultiReddits() { if (!accountName.equals(Account.ANONYMOUS_ACCOUNT)) { FetchMyMultiReddits.fetchMyMultiReddits(mExecutor, mHandler, mOauthRetrofit, accessToken, new FetchMyMultiReddits.FetchMyMultiRedditsListener() { @Override public void success(ArrayList multiReddits) { InsertMultireddit.insertMultireddits(mExecutor, new Handler(), mRedditDataRoomDatabase, multiReddits, accountName, () -> { mInsertMultiredditSuccess = true; sectionsPagerAdapter.stopMultiRedditRefreshProgressbar(); }); } @Override public void failed() { mInsertMultiredditSuccess = false; sectionsPagerAdapter.stopMultiRedditRefreshProgressbar(); Toast.makeText(SubscribedThingListingActivity.this, R.string.error_loading_multi_reddit_list, Toast.LENGTH_SHORT).show(); } }); } } public void deleteMultiReddit(MultiReddit multiReddit) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.delete) .setMessage(R.string.delete_multi_reddit_dialog_message) .setPositiveButton(R.string.delete, (dialogInterface, i) -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { DeleteMultiredditInDatabase.deleteMultiredditInDatabase(mExecutor, new Handler(), mRedditDataRoomDatabase, accountName, multiReddit.getPath(), () -> Toast.makeText(SubscribedThingListingActivity.this, R.string.delete_multi_reddit_success, Toast.LENGTH_SHORT).show()); } else { DeleteMultiReddit.deleteMultiReddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, accessToken, accountName, multiReddit.getPath(), new DeleteMultiReddit.DeleteMultiRedditListener() { @Override public void success() { Toast.makeText(SubscribedThingListingActivity.this, R.string.delete_multi_reddit_success, Toast.LENGTH_SHORT).show(); loadMultiReddits(); } @Override public void failed() { Toast.makeText(SubscribedThingListingActivity.this, R.string.delete_multi_reddit_failed, Toast.LENGTH_SHORT).show(); } }); } }) .setNegativeButton(R.string.cancel, null) .show(); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { finish(); } @Subscribe public void goBackToMainPageEvent(GoBackToMainPageEvent event) { finish(); } @Subscribe public void onRefreshMultiRedditsEvent(RefreshMultiRedditsEvent event) { loadMultiReddits(); } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } private class SectionsPagerAdapter extends FragmentPagerAdapter { @Nullable private SubscribedSubredditsListingFragment subscribedSubredditsListingFragment; @Nullable private FollowedUsersListingFragment followedUsersListingFragment; @Nullable private MultiRedditListingFragment multiRedditListingFragment; public SectionsPagerAdapter(FragmentManager fm) { super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } @NonNull @Override public Fragment getItem(int position) { if (isThingSelectionMode) { switch (thingSelectionType) { case EXTRA_THING_SELECTION_TYPE_SUBREDDIT: return getSubscribedSubredditListingFragment(); case EXTRA_THING_SELECTION_TYPE_USER: return getFollowedUserFragment(); case EXTRA_THING_SELECTION_TYPE_MULTIREDDIT: return getMultiRedditListingFragment(); default: switch (position) { case 0: return getSubscribedSubredditListingFragment(); case 1: return getFollowedUserFragment(); default: return getMultiRedditListingFragment(); } } } switch (position) { case 0: return getSubscribedSubredditListingFragment(); case 1: return getFollowedUserFragment(); default: return getMultiRedditListingFragment(); } } @NonNull private Fragment getSubscribedSubredditListingFragment() { SubscribedSubredditsListingFragment fragment = new SubscribedSubredditsListingFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(SubscribedSubredditsListingFragment.EXTRA_IS_SUBREDDIT_SELECTION, isThingSelectionMode); bundle.putBoolean(SubscribedSubredditsListingFragment.EXTRA_EXTRA_CLEAR_SELECTION, isThingSelectionMode && getIntent().getBooleanExtra(EXTRA_EXTRA_CLEAR_SELECTION, false)); bundle.putString(SubscribedSubredditsListingFragment.EXTRA_ACCOUNT_PROFILE_IMAGE_URL, mAccountProfileImageUrl); fragment.setArguments(bundle); return fragment; } @NonNull private Fragment getFollowedUserFragment() { FollowedUsersListingFragment fragment = new FollowedUsersListingFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(FollowedUsersListingFragment.EXTRA_IS_USER_SELECTION, isThingSelectionMode); fragment.setArguments(bundle); return fragment; } @NonNull private Fragment getMultiRedditListingFragment() { MultiRedditListingFragment fragment = new MultiRedditListingFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(MultiRedditListingFragment.EXTRA_IS_MULTIREDDIT_SELECTION, isThingSelectionMode); fragment.setArguments(bundle); return fragment; } @Override public int getCount() { if (isThingSelectionMode) { switch (thingSelectionType) { case EXTRA_THING_SELECTION_TYPE_ALL: return Account.ANONYMOUS_ACCOUNT.equals(accountName) ? 2 : 3; case EXTRA_THING_SELECTION_TYPE_SUBREDDIT: case EXTRA_THING_SELECTION_TYPE_USER: case EXTRA_THING_SELECTION_TYPE_MULTIREDDIT: return 1; } } return 3; } @Override public CharSequence getPageTitle(int position) { if (isThingSelectionMode) { switch (thingSelectionType) { case EXTRA_THING_SELECTION_TYPE_ALL: switch (position) { case 0: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.subreddits)); case 1: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.users)); case 2: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.multi_reddits)); } case EXTRA_THING_SELECTION_TYPE_SUBREDDIT: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.subreddits)); case EXTRA_THING_SELECTION_TYPE_USER: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.users)); case EXTRA_THING_SELECTION_TYPE_MULTIREDDIT: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.multi_reddits)); } } switch (position) { case 0: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.subreddits)); case 1: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.users)); case 2: return Utils.getTabTextWithCustomFont(typeface, getString(R.string.multi_reddits)); } return null; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { Fragment fragment = (Fragment) super.instantiateItem(container, position); if (fragment instanceof SubscribedSubredditsListingFragment) { subscribedSubredditsListingFragment = (SubscribedSubredditsListingFragment) fragment; } else if (fragment instanceof FollowedUsersListingFragment) { followedUsersListingFragment = (FollowedUsersListingFragment) fragment; } else if (fragment instanceof MultiRedditListingFragment) { multiRedditListingFragment = (MultiRedditListingFragment) fragment; } return fragment; } void stopRefreshProgressbar() { if (subscribedSubredditsListingFragment != null) { ((FragmentCommunicator) subscribedSubredditsListingFragment).stopRefreshProgressbar(); } if (followedUsersListingFragment != null) { ((FragmentCommunicator) followedUsersListingFragment).stopRefreshProgressbar(); } } void stopMultiRedditRefreshProgressbar() { if (multiRedditListingFragment != null) { ((FragmentCommunicator) multiRedditListingFragment).stopRefreshProgressbar(); } } @Nullable Fragment getCurrentFragment() { List fragments = getSupportFragmentManager().getFragments(); if (binding.viewPagerSubscribedThingListingActivity.getCurrentItem() < fragments.size()) { return fragments.get(binding.viewPagerSubscribedThingListingActivity.getCurrentItem()); } return null; } void goBackToTop() { Fragment fragment = getCurrentFragment(); if (fragment instanceof SubscribedSubredditsListingFragment) { ((SubscribedSubredditsListingFragment) fragment).goBackToTop(); } else if (fragment instanceof FollowedUsersListingFragment) { ((FollowedUsersListingFragment) fragment).goBackToTop(); } else if (fragment instanceof MultiRedditListingFragment) { ((MultiRedditListingFragment) fragment).goBackToTop(); } } void changeSearchQuery(String searchQuery) { if (subscribedSubredditsListingFragment != null) { subscribedSubredditsListingFragment.changeSearchQuery(searchQuery); } if (followedUsersListingFragment != null) { followedUsersListingFragment.changeSearchQuery(searchQuery); } if (multiRedditListingFragment != null) { multiRedditListingFragment.changeSearchQuery(searchQuery); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/SuicidePreventionActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.os.Bundle; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivitySuicidePreventionBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class SuicidePreventionActivity extends BaseActivity { static final String EXTRA_QUERY = "EQ"; static final String EXTRA_RETURN_QUERY = "ERQ"; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private ActivitySuicidePreventionBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplicationContext()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySuicidePreventionBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); binding.linearLayoutCheckBoxWrapperSuicidePreventionActivity.setOnClickListener(view -> { binding.doNotShowThisAgainCheckBox.performClick(); }); binding.continueButtonSuicidePreventionActivity.setOnClickListener(view -> { if (binding.doNotShowThisAgainCheckBox.isChecked()) { mSharedPreferences.edit().putBoolean(SharedPreferencesUtils.SHOW_SUICIDE_PREVENTION_ACTIVITY, false).apply(); } Intent returnIntent = new Intent(); returnIntent.putExtra(EXTRA_RETURN_QUERY, getIntent().getStringExtra(EXTRA_QUERY)); setResult(RESULT_OK, returnIntent); finish(); }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); binding.quoteTextViewSuicidePreventionActivity.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); binding.doNotShowThisAgainTextView.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); binding.continueButtonSuicidePreventionActivity.setBackgroundTintList(ColorStateList.valueOf(mCustomThemeWrapper.getColorPrimaryLightTheme())); binding.continueButtonSuicidePreventionActivity.setTextColor(mCustomThemeWrapper.getButtonTextColor()); if (typeface != null) { binding.quoteTextViewSuicidePreventionActivity.setTypeface(typeface); binding.doNotShowThisAgainTextView.setTypeface(typeface); binding.continueButtonSuicidePreventionActivity.setTypeface(typeface); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/UploadImageEnabledActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import ml.docilealligator.infinityforreddit.thing.UploadedImage; public interface UploadImageEnabledActivity { void uploadImage(); void captureImage(); void insertImageUrl(UploadedImage uploadedImage); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/UserMultiselectionActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.UserMultiselectionRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivitySubscribedUsersMultiselectionBinding; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserViewModel; import retrofit2.Retrofit; public class UserMultiselectionActivity extends BaseActivity implements ActivityToolbarInterface { static final String EXTRA_RETURN_SELECTED_USERNAMES = "ERSU"; public static final String EXTRA_GET_SELECTED_USERS = "EGSU"; private static final int USERNAME_SEARCH_REQUEST_CODE = 3; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; public SubscribedUserViewModel mSubscribedUSerViewModel; private LinearLayoutManagerBugFixed mLinearLayoutManager; private UserMultiselectionRecyclerViewAdapter mAdapter; private RequestManager mGlide; private ActivitySubscribedUsersMultiselectionBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivitySubscribedUsersMultiselectionBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); applyCustomTheme(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutUsersMultiselectionActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = insets.getInsets( WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() ); setMargins(binding.toolbarSubscribedUsersMultiselectionActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.recyclerViewSubscribedSubscribedUsersMultiselectionActivity.setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); //adjustToolbar(binding.toolbarSubscribedUsersMultiselectionActivity); //int navBarHeight = getNavBarHeight(); //if (navBarHeight > 0) { // binding.recyclerViewSubscribedSubscribedUsersMultiselectionActivity.setPadding(0, 0, 0, navBarHeight); //} } } setSupportActionBar(binding.toolbarSubscribedUsersMultiselectionActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mGlide = Glide.with(this); binding.swipeRefreshLayoutSubscribedSubscribedUsersMultiselectionActivity.setEnabled(false); bindView(); } private void bindView() { mLinearLayoutManager = new LinearLayoutManagerBugFixed(this); binding.recyclerViewSubscribedSubscribedUsersMultiselectionActivity.setLayoutManager(mLinearLayoutManager); mAdapter = new UserMultiselectionRecyclerViewAdapter(this, mCustomThemeWrapper); binding.recyclerViewSubscribedSubscribedUsersMultiselectionActivity.setAdapter(mAdapter); mSubscribedUSerViewModel = new ViewModelProvider(this, new SubscribedUserViewModel.Factory(mRedditDataRoomDatabase, accountName)) .get(SubscribedUserViewModel.class); mSubscribedUSerViewModel.getAllSubscribedUsers().observe(this, subscribedUserData -> { binding.swipeRefreshLayoutSubscribedSubscribedUsersMultiselectionActivity.setRefreshing(false); if (subscribedUserData == null || subscribedUserData.size() == 0) { binding.recyclerViewSubscribedSubscribedUsersMultiselectionActivity.setVisibility(View.GONE); binding.noSubscriptionsLinearLayoutSubscribedUsersMultiselectionActivity.setVisibility(View.VISIBLE); } else { binding.noSubscriptionsLinearLayoutSubscribedUsersMultiselectionActivity.setVisibility(View.GONE); binding.recyclerViewSubscribedSubscribedUsersMultiselectionActivity.setVisibility(View.VISIBLE); mGlide.clear(binding.noSubscriptionsImageViewSubscribedUsersMultiselectionActivity); } mAdapter.setSubscribedUsers(subscribedUserData, getIntent().getStringExtra(EXTRA_GET_SELECTED_USERS)); }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.user_multiselection_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_save_user_multiselection_activity) { if (mAdapter != null) { Intent returnIntent = new Intent(); returnIntent.putStringArrayListExtra(EXTRA_RETURN_SELECTED_USERNAMES, mAdapter.getAllSelectedUsers()); setResult(RESULT_OK, returnIntent); } finish(); return true; } else if (itemId == R.id.action_search_user_multiselection_activity) { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); intent.putExtra(SearchActivity.EXTRA_IS_MULTI_SELECTION, true); startActivityForResult(intent, USERNAME_SEARCH_REQUEST_CODE); } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == USERNAME_SEARCH_REQUEST_CODE && resultCode == RESULT_OK && data != null && mAdapter != null) { Intent returnIntent = new Intent(); ArrayList selectedUsers = mAdapter.getAllSelectedUsers(); ArrayList searchedUsers = data.getStringArrayListExtra(SearchActivity.RETURN_EXTRA_SELECTED_USERNAMES); if (searchedUsers != null) { selectedUsers.addAll(searchedUsers); } returnIntent.putStringArrayListExtra(EXTRA_RETURN_SELECTED_USERNAMES, selectedUsers); setResult(RESULT_OK, returnIntent); finish(); } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutUsersMultiselectionActivity, binding.collapsingToolbarLayoutSubscribedUsersMultiselectionActivity, binding.toolbarSubscribedUsersMultiselectionActivity); binding.errorTextViewSubscribedUsersMultiselectionActivity.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (typeface != null) { binding.errorTextViewSubscribedUsersMultiselectionActivity.setTypeface(typeface); } } @Override public void onLongPress() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewImageOrGifActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.Manifest; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.PersistableBundle; import android.text.Html; import android.text.Spanned; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.FrameLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import com.github.piasy.biv.BigImageViewer; import com.github.piasy.biv.loader.ImageLoader; import com.github.piasy.biv.loader.glide.GlideImageLoader; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.io.File; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; import ml.docilealligator.infinityforreddit.WallpaperSetter; import ml.docilealligator.infinityforreddit.asynctasks.SaveBitmapImageToFile; import ml.docilealligator.infinityforreddit.asynctasks.SaveGIFToFile; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.customviews.GlideGifImageViewFactory; import ml.docilealligator.infinityforreddit.customviews.slidr.Slidr; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrConfig; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrPosition; import ml.docilealligator.infinityforreddit.databinding.ActivityViewImageOrGifBinding; import ml.docilealligator.infinityforreddit.events.FinishViewMediaActivityEvent; import ml.docilealligator.infinityforreddit.font.ContentFontFamily; import ml.docilealligator.infinityforreddit.font.ContentFontStyle; import ml.docilealligator.infinityforreddit.font.FontFamily; import ml.docilealligator.infinityforreddit.font.FontStyle; import ml.docilealligator.infinityforreddit.font.TitleFontFamily; import ml.docilealligator.infinityforreddit.font.TitleFontStyle; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ViewImageOrGifActivity extends AppCompatActivity implements SetAsWallpaperCallback, CustomFontReceiver { public static final String EXTRA_IMAGE_URL_KEY = "EIUK"; public static final String EXTRA_GIF_URL_KEY = "EGUK"; public static final String EXTRA_FILE_NAME_KEY = "EFNK"; public static final String EXTRA_SUBREDDIT_OR_USERNAME_KEY = "ESOUK"; public static final String EXTRA_POST_TITLE_KEY = "EPTK"; public static final String EXTRA_POST_ID_KEY = "EPIK"; public static final String EXTRA_COMMENT_ID_KEY = "ECIK"; public static final String EXTRA_IS_NSFW = "EIN"; private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject Executor mExecutor; private boolean isActionBarHidden = false; private boolean isDownloading = false; private RequestManager glide; private String mImageUrl; private String mImageFileName; private String mSubredditName; private boolean isGif = true; private boolean isApng = false; private boolean isNsfw; private Typeface typeface; private Handler handler; private ActivityViewImageOrGifBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((Infinity) getApplication()).getAppComponent().inject(this); getTheme().applyStyle(R.style.Theme_Normal, true); getTheme().applyStyle(FontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.FONT_SIZE_KEY, FontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(TitleFontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_SIZE_KEY, TitleFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(ContentFontStyle.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_SIZE_KEY, ContentFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(FontFamily.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.FONT_FAMILY_KEY, FontFamily.Default.name())).getResId(), true); getTheme().applyStyle(TitleFontFamily.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY, TitleFontFamily.Default.name())).getResId(), true); getTheme().applyStyle(ContentFontFamily.valueOf(mSharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY, ContentFontFamily.Default.name())).getResId(), true); BigImageViewer.initialize(GlideImageLoader.with(this.getApplicationContext())); binding = ActivityViewImageOrGifBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_VERTICALLY_TO_GO_BACK_FROM_MEDIA, true)) { Slidr.attach(this, new SlidrConfig.Builder().position(SlidrPosition.VERTICAL).distanceThreshold(0.125f).build()); } glide = Glide.with(this); handler = new Handler(Looper.getMainLooper()); Intent intent = getIntent(); mImageUrl = intent.getStringExtra(EXTRA_GIF_URL_KEY); if (mImageUrl == null) { isGif = false; mImageUrl = intent.getStringExtra(EXTRA_IMAGE_URL_KEY); } mImageFileName = intent.getStringExtra(EXTRA_FILE_NAME_KEY); String postTitle = intent.getStringExtra(EXTRA_POST_TITLE_KEY); mSubredditName = intent.getStringExtra(EXTRA_SUBREDDIT_OR_USERNAME_KEY); isNsfw = intent.getBooleanExtra(EXTRA_IS_NSFW, false); // Detect APNG/avatar images - check URL extension or if it's an icon/avatar // Use animated-capable view for avatars since they may be APNG if (mImageUrl != null && (mImageUrl.toLowerCase().endsWith(".apng") || mImageUrl.toLowerCase().contains(".apng?") || mImageUrl.toLowerCase().contains(".apng&"))) { isApng = true; } else if (mImageFileName != null && (mImageFileName.toLowerCase().contains("-icon.") || mImageFileName.toLowerCase().contains("avatar"))) { // Avatar/icon images - treat as potentially animated isApng = true; } boolean useBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.USE_BOTTOM_TOOLBAR_IN_MEDIA_VIEWER, false); if (postTitle != null) { Spanned title = Html.fromHtml(String.format("%s", postTitle)); if (useBottomAppBar) { binding.titleTextViewViewImageOrGifActivity.setText(title); } else { setTitle(Utils.getTabTextWithCustomFont(typeface, title)); } } else { if (!useBottomAppBar) { setTitle(""); } } if (useBottomAppBar) { getSupportActionBar().hide(); binding.bottomNavigationViewImageOrGifActivity.setVisibility(View.VISIBLE); binding.downloadImageViewViewImageOrGifActivity.setOnClickListener(view -> { if (isDownloading) { return; } isDownloading = true; requestPermissionAndDownload(); }); binding.shareImageViewViewImageOrGifActivity.setOnClickListener(view -> { if (isGif || isApng) shareGif(); else shareImage(); }); binding.wallpaperImageViewViewImageOrGifActivity.setOnClickListener(view -> { setWallpaper(); }); } else { ActionBar actionBar = getSupportActionBar(); Drawable upArrow = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); actionBar.setHomeAsUpIndicator(upArrow); actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.transparentActionBarAndExoPlayerControllerColor))); } binding.loadImageErrorLinearLayoutViewImageOrGifActivity.setOnClickListener(view -> { binding.progressBarViewImageOrGifActivity.setVisibility(View.VISIBLE); binding.loadImageErrorLinearLayoutViewImageOrGifActivity.setVisibility(View.GONE); loadImage(); }); binding.imageViewViewImageOrGifActivity.setOnClickListener(view -> { if (isActionBarHidden) { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); isActionBarHidden = false; if (useBottomAppBar) { binding.bottomNavigationViewImageOrGifActivity.setVisibility(View.VISIBLE); } } else { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); isActionBarHidden = true; if (useBottomAppBar) { binding.bottomNavigationViewImageOrGifActivity.setVisibility(View.GONE); } } }); binding.imageViewViewImageOrGifActivity.setImageViewFactory(new GlideGifImageViewFactory(new SaveMemoryCenterInisdeDownsampleStrategy(Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"))))); binding.imageViewViewImageOrGifActivity.setImageLoaderCallback(new ImageLoader.Callback() { @Override public void onCacheHit(int imageType, File image) { } @Override public void onCacheMiss(int imageType, File image) { } @Override public void onStart() { } @Override public void onProgress(int progress) { } @Override public void onFinish() { } @Override public void onSuccess(File image) { binding.progressBarViewImageOrGifActivity.setVisibility(View.GONE); final SubsamplingScaleImageView view = binding.imageViewViewImageOrGifActivity.getSSIV(); if (view != null) { view.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() { @Override public void onImageLoaded() { view.setMinimumDpi(80); view.setDoubleTapZoomDpi(240); view.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED); view.setQuickScaleEnabled(true); view.resetScaleAndCenter(); } }); } } @Override public void onFail(Exception error) { binding.progressBarViewImageOrGifActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImageOrGifActivity.setVisibility(View.VISIBLE); } }); loadImage(); // Fixes #383 // Not having a background will cause visual glitches on some devices. FrameLayout slidablePanel = findViewById(R.id.slidable_panel); if (slidablePanel != null) { slidablePanel.setBackgroundColor(getResources().getColor(android.R.color.black)); } } private void loadImage() { if (isApng) { // Use GifImageView for APNG files, which Glide with APNG4Android plugin will animate binding.imageViewViewImageOrGifActivity.setVisibility(View.GONE); binding.apngImageViewViewImageOrGifActivity.setVisibility(View.VISIBLE); boolean disableAnimation = mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_PROFILE_AVATAR_ANIMATION, false); if (disableAnimation) { // Use asBitmap() to load only the first frame and prevent animation glide.asBitmap().load(mImageUrl).into(binding.apngImageViewViewImageOrGifActivity); } else { glide.load(mImageUrl).into(binding.apngImageViewViewImageOrGifActivity); } binding.progressBarViewImageOrGifActivity.setVisibility(View.GONE); binding.apngImageViewViewImageOrGifActivity.setOnClickListener(view -> { if (isActionBarHidden) { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); isActionBarHidden = false; if (mSharedPreferences.getBoolean(SharedPreferencesUtils.USE_BOTTOM_TOOLBAR_IN_MEDIA_VIEWER, false)) { binding.bottomNavigationViewImageOrGifActivity.setVisibility(View.VISIBLE); } } else { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); isActionBarHidden = true; if (mSharedPreferences.getBoolean(SharedPreferencesUtils.USE_BOTTOM_TOOLBAR_IN_MEDIA_VIEWER, false)) { binding.bottomNavigationViewImageOrGifActivity.setVisibility(View.GONE); } } }); } else { binding.imageViewViewImageOrGifActivity.showImage(Uri.parse(mImageUrl)); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_image_or_gif_activity, menu); for (int i = 0; i < menu.size(); i++) { Utils.setTitleWithCustomFontToMenuItem(typeface, menu.getItem(i), null); } if (!isGif && !isApng) { menu.findItem(R.id.action_set_wallpaper_view_image_or_gif_activity).setVisible(true); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_download_view_image_or_gif_activity) { if (isDownloading) { return false; } isDownloading = true; requestPermissionAndDownload(); return true; } else if (itemId == R.id.action_share_view_image_or_gif_activity) { if (isGif || isApng) shareGif(); else shareImage(); return true; } else if (itemId == R.id.action_set_wallpaper_view_image_or_gif_activity) { setWallpaper(); return true; } return false; } private void requestPermissionAndDownload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted download(); } } else { download(); } } private void download() { isDownloading = false; // Check if download location is set String downloadLocation; int mediaType = (isGif || isApng) ? DownloadMediaService.EXTRA_MEDIA_TYPE_GIF : DownloadMediaService.EXTRA_MEDIA_TYPE_IMAGE; if (isNsfw && mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); } else { if (isGif || isApng) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); } else { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); } } if (downloadLocation == null || downloadLocation.isEmpty()) { Toast.makeText(this, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return; } PersistableBundle extras = new PersistableBundle(); extras.putString(DownloadMediaService.EXTRA_URL, mImageUrl); extras.putInt(DownloadMediaService.EXTRA_MEDIA_TYPE, mediaType); extras.putString(DownloadMediaService.EXTRA_SUBREDDIT_NAME, mSubredditName); extras.putInt(DownloadMediaService.EXTRA_IS_NSFW, isNsfw ? 1 : 0); // Reconstruct filename using post title passed in intent String postTitle = getIntent().getStringExtra(EXTRA_POST_TITLE_KEY); String commentId = getIntent().getStringExtra(EXTRA_COMMENT_ID_KEY); String postId = getIntent().getStringExtra(EXTRA_POST_ID_KEY); String title = (postTitle != null && !postTitle.isEmpty()) ? postTitle : ((isGif || isApng) ? "reddit_gif" : "reddit_image"); if (postId != null && !postId.isEmpty()) { title = title + "_" + postId; } if (commentId != null && !commentId.isEmpty()) { title = title + "_" + commentId; } // Basic sanitization (similar to DownloadMediaService) String sanitizedTitle = title.replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("[\\s_]+", "_").replaceAll("^_+|_+$", ""); if (sanitizedTitle.length() > 100) sanitizedTitle = sanitizedTitle.substring(0, 100).replaceAll("_+$", ""); if (sanitizedTitle.isEmpty()) sanitizedTitle = ((isGif || isApng) ? "reddit_gif_" : "reddit_image_") + System.currentTimeMillis(); // Basic extension determination String extension = ".unknown"; if (mImageUrl != null) { String urlExt = org.apache.commons.io.FilenameUtils.getExtension(mImageUrl); if (urlExt != null && !urlExt.isEmpty() && urlExt.matches("(?i)(jpg|jpeg|png|apng|gif|mp4|webm|mov|avi)")) { extension = "." + urlExt.toLowerCase().substring(0, Math.min(urlExt.length(), 5)); } else if (isApng) { extension = ".apng"; } else if (isGif) { extension = ".gif"; } else { extension = ".jpg"; // Default for images if URL extension is weird } } else if (isApng) { extension = ".apng"; } else if (isGif) { extension = ".gif"; } else { extension = ".jpg"; } String finalFileName = sanitizedTitle + extension; extras.putString(DownloadMediaService.EXTRA_FILE_NAME, finalFileName); //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadMediaService.constructJobInfo(this, 5000000, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); Toast.makeText(this, R.string.download_started, Toast.LENGTH_SHORT).show(); } private void shareImage() { glide.asBitmap().load(mImageUrl).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { File cacheDir = Utils.getCacheDir(ViewImageOrGifActivity.this); if (cacheDir != null) { Toast.makeText(ViewImageOrGifActivity.this, R.string.save_image_first, Toast.LENGTH_SHORT).show(); SaveBitmapImageToFile.SaveBitmapImageToFile(mExecutor, handler, resource, cacheDir.getPath(), mImageFileName, new SaveBitmapImageToFile.SaveBitmapImageToFileListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(ViewImageOrGifActivity.this, BuildConfig.APPLICATION_ID + ".provider", imageFile); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.setType("image/*"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); } @Override public void saveFailed() { Toast.makeText(ViewImageOrGifActivity.this, R.string.cannot_save_image, Toast.LENGTH_SHORT).show(); } }); } else { Toast.makeText(ViewImageOrGifActivity.this, R.string.cannot_get_storage, Toast.LENGTH_SHORT).show(); } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); } private void shareGif() { Toast.makeText(ViewImageOrGifActivity.this, R.string.save_gif_first, Toast.LENGTH_SHORT).show(); glide.asGif().load(mImageUrl).listener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady(GifDrawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { File cacheDir = Utils.getCacheDir(ViewImageOrGifActivity.this); if (cacheDir != null) { SaveGIFToFile.saveGifToFile(mExecutor, handler, resource, cacheDir.getPath(), mImageFileName, new SaveGIFToFile.SaveGIFToFileListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(ViewImageOrGifActivity.this, BuildConfig.APPLICATION_ID + ".provider", imageFile); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.setType("image/gif"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); } @Override public void saveFailed() { Toast.makeText(ViewImageOrGifActivity.this, R.string.cannot_save_gif, Toast.LENGTH_SHORT).show(); } }); } else { Toast.makeText(ViewImageOrGifActivity.this, R.string.cannot_get_storage, Toast.LENGTH_SHORT).show(); } return false; } }).submit(); } private void setWallpaper() { if (!isGif && !isApng) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { SetAsWallpaperBottomSheetFragment setAsWallpaperBottomSheetFragment = new SetAsWallpaperBottomSheetFragment(); setAsWallpaperBottomSheetFragment.show(getSupportFragmentManager(), setAsWallpaperBottomSheetFragment.getTag()); } else { WallpaperSetter.set(mExecutor, handler, mImageUrl, WallpaperSetter.BOTH_SCREENS, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewImageOrGifActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewImageOrGifActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); isDownloading = false; } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED && isDownloading) { download(); } } } @Override public void setToHomeScreen(int viewPagerPosition) { WallpaperSetter.set(mExecutor, handler, mImageUrl, WallpaperSetter.HOME_SCREEN, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewImageOrGifActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewImageOrGifActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } @Override public void setToLockScreen(int viewPagerPosition) { WallpaperSetter.set(mExecutor, handler, mImageUrl, WallpaperSetter.LOCK_SCREEN, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewImageOrGifActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewImageOrGifActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } @Override public void setToBoth(int viewPagerPosition) { WallpaperSetter.set(mExecutor, handler, mImageUrl, WallpaperSetter.BOTH_SCREENS, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewImageOrGifActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewImageOrGifActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } @Override public void onDestroy() { EventBus.getDefault().unregister(this); BigImageViewer.imageLoader().cancelAll(); super.onDestroy(); } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } @Subscribe public void onFinishViewMediaActivityEvent(FinishViewMediaActivityEvent e) { finish(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewImgurMediaActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Html; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.viewpager.widget.ViewPager; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import app.futured.hauler.DragDirection; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; import ml.docilealligator.infinityforreddit.WallpaperSetter; import ml.docilealligator.infinityforreddit.apis.ImgurAPI; import ml.docilealligator.infinityforreddit.databinding.ActivityViewImgurMediaBinding; import ml.docilealligator.infinityforreddit.font.ContentFontFamily; import ml.docilealligator.infinityforreddit.font.ContentFontStyle; import ml.docilealligator.infinityforreddit.font.FontFamily; import ml.docilealligator.infinityforreddit.font.FontStyle; import ml.docilealligator.infinityforreddit.font.TitleFontFamily; import ml.docilealligator.infinityforreddit.font.TitleFontStyle; import ml.docilealligator.infinityforreddit.fragments.ViewImgurImageFragment; import ml.docilealligator.infinityforreddit.fragments.ViewImgurVideoFragment; import ml.docilealligator.infinityforreddit.post.ImgurMedia; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ViewImgurMediaActivity extends AppCompatActivity implements SetAsWallpaperCallback, CustomFontReceiver { public static final String EXTRA_IMGUR_TYPE = "EIT"; public static final String EXTRA_IMGUR_ID = "EII"; public static final String EXTRA_SUBREDDIT_NAME = "ESN_VIMA"; public static final String EXTRA_POST_TITLE_KEY = "ET_VIMA"; public static final String EXTRA_IS_NSFW = "EIN_VIMA"; public static final int IMGUR_TYPE_GALLERY = 0; public static final int IMGUR_TYPE_ALBUM = 1; public static final int IMGUR_TYPE_IMAGE = 2; private static final String IMGUR_IMAGES_STATE = "IIS"; public Typeface typeface; private SectionsPagerAdapter sectionsPagerAdapter; private ArrayList mImages; private boolean useBottomAppBar; private String subredditName; private String postTitle; private boolean isNsfw; private String title; @Inject @Named("imgur") Retrofit imgurRetrofit; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject Executor executor; private Handler handler; private ActivityViewImgurMediaBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((Infinity) getApplication()).getAppComponent().inject(this); getTheme().applyStyle(R.style.Theme_Normal, true); getTheme().applyStyle(FontStyle.valueOf(sharedPreferences .getString(SharedPreferencesUtils.FONT_SIZE_KEY, FontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(TitleFontStyle.valueOf(sharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_SIZE_KEY, TitleFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(ContentFontStyle.valueOf(sharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_SIZE_KEY, ContentFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(FontFamily.valueOf(sharedPreferences .getString(SharedPreferencesUtils.FONT_FAMILY_KEY, FontFamily.Default.name())).getResId(), true); getTheme().applyStyle(TitleFontFamily.valueOf(sharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY, TitleFontFamily.Default.name())).getResId(), true); getTheme().applyStyle(ContentFontFamily.valueOf(sharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY, ContentFontFamily.Default.name())).getResId(), true); binding = ActivityViewImgurMediaBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); handler = new Handler(Looper.getMainLooper()); useBottomAppBar = sharedPreferences.getBoolean(SharedPreferencesUtils.USE_BOTTOM_TOOLBAR_IN_MEDIA_VIEWER, false); if (!useBottomAppBar) { ActionBar actionBar = getSupportActionBar(); Drawable upArrow = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); actionBar.setHomeAsUpIndicator(upArrow); actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.transparentActionBarAndExoPlayerControllerColor))); setTitle(" "); } else { getSupportActionBar().hide(); } String imgurId = getIntent().getStringExtra(EXTRA_IMGUR_ID); if (imgurId == null) { finish(); return; } subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); isNsfw = getIntent().getBooleanExtra(EXTRA_IS_NSFW, false); postTitle = getIntent().getStringExtra(EXTRA_POST_TITLE_KEY); title = getIntent().getStringExtra(EXTRA_POST_TITLE_KEY); if (savedInstanceState != null) { mImages = savedInstanceState.getParcelableArrayList(IMGUR_IMAGES_STATE); } if (sharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_VERTICALLY_TO_GO_BACK_FROM_MEDIA, true)) { binding.getRoot().setOnDragDismissedListener(dragDirection -> { int slide = dragDirection == DragDirection.UP ? R.anim.slide_out_up : R.anim.slide_out_down; finish(); overridePendingTransition(0, slide); }); } else { binding.getRoot().setDragEnabled(false); } if (mImages == null) { fetchImgurMedia(imgurId); } else { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); setupViewPager(); } binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setOnClickListener(view -> fetchImgurMedia(imgurId)); } public boolean isUseBottomAppBar() { return useBottomAppBar; } private void fetchImgurMedia(String imgurId) { binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.GONE); binding.progressBarViewImgurMediaActivity.setVisibility(View.VISIBLE); switch (getIntent().getIntExtra(EXTRA_IMGUR_TYPE, IMGUR_TYPE_IMAGE)) { case IMGUR_TYPE_GALLERY: imgurRetrofit.create(ImgurAPI.class).getGalleryImages(APIUtils.IMGUR_CLIENT_ID, imgurId) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { ArrayList images = parseImgurImages(response.body()); handler.post(() -> { if (images != null) { mImages = images; binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.GONE); setupViewPager(); } else { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } }); }); } else { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } }); break; case IMGUR_TYPE_ALBUM: imgurRetrofit.create(ImgurAPI.class).getAlbumImages(APIUtils.IMGUR_CLIENT_ID, imgurId) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { ArrayList images = parseImgurImages(response.body()); handler.post(() -> { if (images != null) { mImages = images; binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.GONE); setupViewPager(); } else { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } }); }); } else { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } }); break; case IMGUR_TYPE_IMAGE: imgurRetrofit.create(ImgurAPI.class).getImage(APIUtils.IMGUR_CLIENT_ID, imgurId) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { ImgurMedia image = parseImgurImage(response.body()); handler.post(() -> { if (image != null) { mImages = new ArrayList<>(); mImages.add(image); binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.GONE); setupViewPager(); } else { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } }); }); } else { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { binding.progressBarViewImgurMediaActivity.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurMediaActivity.setVisibility(View.VISIBLE); } }); break; } } private void setupViewPager() { if (!useBottomAppBar) { setToolbarTitle(0); binding.viewPagerViewImgurMediaActivity.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { setToolbarTitle(position); } }); } sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); binding.viewPagerViewImgurMediaActivity.setAdapter(sectionsPagerAdapter); binding.viewPagerViewImgurMediaActivity.setOffscreenPageLimit(3); } private void setToolbarTitle(int position) { if (mImages != null && position >= 0 && position < mImages.size()) { if (mImages.get(position).getType() == ImgurMedia.TYPE_VIDEO) { setTitle(Utils.getTabTextWithCustomFont(typeface, Html.fromHtml("" + getString(R.string.view_imgur_media_activity_video_label, position + 1, mImages.size()) + ""))); } else { setTitle(Utils.getTabTextWithCustomFont(typeface, Html.fromHtml("" + getString(R.string.view_imgur_media_activity_image_label, position + 1, mImages.size()) + ""))); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_imgur_media_activity, menu); for (int i = 0; i < menu.size(); i++) { Utils.setTitleWithCustomFontToMenuItem(typeface, menu.getItem(i), null); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_download_all_imgur_album_media_view_imgur_media_activity) { // Check if download locations are set for all media types // Imgur album can contain images and videos String imageDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String videoDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); String nsfwDownloadLocation = ""; boolean needsNsfwLocation = isNsfw && sharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false); Log.d("ImgurDownload", "ViewImgurMediaActivity - Starting download of album with " + mImages.size() + " items, isNsfw=" + isNsfw + ", needsNsfwLocation=" + needsNsfwLocation); Log.d("ImgurDownload", "Download location prefs - IMAGE: " + (imageDownloadLocation.isEmpty() ? "EMPTY" : "SET") + ", VIDEO: " + (videoDownloadLocation.isEmpty() ? "EMPTY" : "SET")); if (needsNsfwLocation) { nsfwDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); Log.d("ImgurDownload", "NSFW location: " + (nsfwDownloadLocation.isEmpty() ? "EMPTY" : "SET")); if (nsfwDownloadLocation == null || nsfwDownloadLocation.isEmpty()) { Log.e("ImgurDownload", "NSFW download location not set but required"); Toast.makeText(this, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return true; } } else { // Check for required download locations based on the album content boolean hasImage = false; boolean hasVideo = false; for (ImgurMedia media : mImages) { if (media.getType() == ImgurMedia.TYPE_VIDEO) { hasVideo = true; } else { hasImage = true; } } Log.d("ImgurDownload", "Album content - hasImage: " + hasImage + ", hasVideo: " + hasVideo); if ((hasImage && (imageDownloadLocation == null || imageDownloadLocation.isEmpty())) || (hasVideo && (videoDownloadLocation == null || videoDownloadLocation.isEmpty()))) { Log.e("ImgurDownload", "Required download location not set - " + (hasImage && (imageDownloadLocation == null || imageDownloadLocation.isEmpty()) ? "IMAGE missing" : "") + (hasVideo && (videoDownloadLocation == null || videoDownloadLocation.isEmpty()) ? "VIDEO missing" : "")); Toast.makeText(this, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return true; } } JobInfo jobInfo = DownloadMediaService.constructImgurAlbumDownloadAllMediaJobInfo(this, 5000000L * mImages.size(), mImages, subredditName, isNsfw, title); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); Log.d("ImgurDownload", "Download job scheduled successfully"); Toast.makeText(this, R.string.download_started, Toast.LENGTH_SHORT).show(); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(IMGUR_IMAGES_STATE, mImages); } @Override public void setToHomeScreen(int viewPagerPosition) { if (mImages != null && viewPagerPosition >= 0 && viewPagerPosition < mImages.size()) { WallpaperSetter.set(executor, handler, mImages.get(viewPagerPosition).getLink(), WallpaperSetter.HOME_SCREEN, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewImgurMediaActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewImgurMediaActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } } @Override public void setToLockScreen(int viewPagerPosition) { if (mImages != null && viewPagerPosition >= 0 && viewPagerPosition < mImages.size()) { WallpaperSetter.set(executor, handler, mImages.get(viewPagerPosition).getLink(), WallpaperSetter.LOCK_SCREEN, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewImgurMediaActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewImgurMediaActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } } @Override public void setToBoth(int viewPagerPosition) { if (mImages != null && viewPagerPosition >= 0 && viewPagerPosition < mImages.size()) { WallpaperSetter.set(executor, handler, mImages.get(viewPagerPosition).getLink(), WallpaperSetter.BOTH_SCREENS, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewImgurMediaActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewImgurMediaActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } } public int getCurrentPagePosition() { return binding.viewPagerViewImgurMediaActivity.getCurrentItem(); } @WorkerThread @Nullable private static ArrayList parseImgurImages(String response) { try { JSONArray jsonArray = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.IMAGES_KEY); ArrayList images = new ArrayList<>(); for (int i = 0; i < jsonArray.length(); i++) { try { JSONObject image = jsonArray.getJSONObject(i); String type = image.getString(JSONUtils.TYPE_KEY); if (type.contains("gif")) { images.add(new ImgurMedia(image.getString(JSONUtils.ID_KEY), image.getString(JSONUtils.TITLE_KEY), image.getString(JSONUtils.DESCRIPTION_KEY), "video/mp4", image.getString(JSONUtils.MP4_KEY))); } else { images.add(new ImgurMedia(image.getString(JSONUtils.ID_KEY), image.getString(JSONUtils.TITLE_KEY), image.getString(JSONUtils.DESCRIPTION_KEY), type, image.getString(JSONUtils.LINK_KEY))); } } catch (JSONException e) { e.printStackTrace(); } } return images; } catch (JSONException e) { e.printStackTrace(); } return null; } @WorkerThread @Nullable private static ImgurMedia parseImgurImage(String response) { try { JSONObject image = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY); String type = image.getString(JSONUtils.TYPE_KEY); if (type.contains("gif")) { return new ImgurMedia(image.getString(JSONUtils.ID_KEY), image.getString(JSONUtils.TITLE_KEY), image.getString(JSONUtils.DESCRIPTION_KEY), "video/mp4", image.getString(JSONUtils.MP4_KEY)); } else { return new ImgurMedia(image.getString(JSONUtils.ID_KEY), image.getString(JSONUtils.TITLE_KEY), image.getString(JSONUtils.DESCRIPTION_KEY), type, image.getString(JSONUtils.LINK_KEY)); } } catch (JSONException e) { e.printStackTrace(); } return null; } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } private class SectionsPagerAdapter extends FragmentStatePagerAdapter { SectionsPagerAdapter(@NonNull FragmentManager fm) { super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } @NonNull @Override public Fragment getItem(int position) { ImgurMedia imgurMedia = mImages.get(position); if (imgurMedia.getType() == ImgurMedia.TYPE_VIDEO) { ViewImgurVideoFragment fragment = new ViewImgurVideoFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(ViewImgurVideoFragment.EXTRA_IMGUR_VIDEO, imgurMedia); bundle.putInt(ViewImgurVideoFragment.EXTRA_INDEX, position); bundle.putInt(ViewImgurVideoFragment.EXTRA_MEDIA_COUNT, mImages.size()); bundle.putString(EXTRA_SUBREDDIT_NAME, subredditName); bundle.putBoolean(EXTRA_IS_NSFW, isNsfw); bundle.putString(EXTRA_POST_TITLE_KEY, postTitle); fragment.setArguments(bundle); return fragment; } else { ViewImgurImageFragment fragment = new ViewImgurImageFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(ViewImgurImageFragment.EXTRA_IMGUR_IMAGES, imgurMedia); bundle.putInt(ViewImgurImageFragment.EXTRA_INDEX, position); bundle.putInt(ViewImgurImageFragment.EXTRA_MEDIA_COUNT, mImages.size()); bundle.putString(EXTRA_SUBREDDIT_NAME, subredditName); bundle.putBoolean(EXTRA_IS_NSFW, isNsfw); bundle.putString(EXTRA_POST_TITLE_KEY, postTitle); fragment.setArguments(bundle); return fragment; } } @Override public int getCount() { return mImages.size(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewMultiRedditDetailActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.badge.ExperimentalBadgeUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.SubredditAutocompleteRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.DeleteMultiredditInDatabase; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FABMoreOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTimeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.NavigationWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityViewMultiRedditDetailBinding; import ml.docilealligator.infinityforreddit.events.ChangeInboxCountEvent; import ml.docilealligator.infinityforreddit.events.GoBackToMainPageEvent; import ml.docilealligator.infinityforreddit.events.RefreshMultiRedditsEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.FragmentCommunicator; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.fragments.PostFragmentBase; import ml.docilealligator.infinityforreddit.multireddit.DeleteMultiReddit; import ml.docilealligator.infinityforreddit.multireddit.ExpandedSubredditInMultiReddit; import ml.docilealligator.infinityforreddit.multireddit.FetchMultiRedditInfo; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import ml.docilealligator.infinityforreddit.subreddit.ParseSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ViewMultiRedditDetailActivity extends BaseActivity implements SortTypeSelectionCallback, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, ActivityToolbarInterface, MarkPostAsReadInterface, PostTypeBottomSheetFragment.PostTypeSelectionCallback, FABMoreOptionsBottomSheetFragment.FABOptionSelectionCallback, RecyclerViewContentScrollingInterface { public static final String EXTRA_MULTIREDDIT_DATA = "EMD"; public static final String EXTRA_MULTIREDDIT_PATH = "EMP"; private static final String FRAGMENT_OUT_STATE_KEY = "FOSK"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject @Named("bottom_app_bar") SharedPreferences mBottomAppBarSharedPreference; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private MultiReddit multiReddit; private String multiPath; private Fragment mFragment; private int fabOption; private boolean hideFab; private boolean showBottomAppBar; private boolean lockBottomAppBar; private Runnable autoCompleteRunnable; private Call subredditAutocompleteCall; private NavigationWrapper navigationWrapper; private ActivityViewMultiRedditDetailBinding binding; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mFragment instanceof PostFragment) return ((PostFragment) mFragment).handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); return super.onKeyDown(keyCode, event); } @ExperimentalBadgeUtils @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityViewMultiRedditDetailBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); hideFab = mSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_FAB_IN_POST_FEED, false); showBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.BOTTOM_APP_BAR_KEY, false); navigationWrapper = new NavigationWrapper(findViewById(R.id.bottom_app_bar_bottom_app_bar), findViewById(R.id.linear_layout_bottom_app_bar), findViewById(R.id.option_1_bottom_app_bar), findViewById(R.id.option_2_bottom_app_bar), findViewById(R.id.option_3_bottom_app_bar), findViewById(R.id.option_4_bottom_app_bar), findViewById(R.id.fab_view_multi_reddit_detail_activity), findViewById(R.id.navigation_rail), customThemeWrapper, showBottomAppBar); applyCustomTheme(); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutViewMultiRedditDetailActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); if (navigationWrapper.navigationRailView == null) { if (navigationWrapper.bottomAppBar.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, ViewMultiRedditDetailActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, ViewMultiRedditDetailActivity.this) + allInsets.bottom); } else { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, allInsets.bottom); } } else { if (navigationWrapper.navigationRailView.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, ViewMultiRedditDetailActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, ViewMultiRedditDetailActivity.this) + allInsets.bottom); binding.frameLayoutViewMultiRedditDetailActivity.setPadding(allInsets.left, 0, allInsets.right, 0); } else { navigationWrapper.navigationRailView.setFitsSystemWindows(false); navigationWrapper.navigationRailView.setPadding(0, 0, 0, allInsets.bottom); setMargins(navigationWrapper.navigationRailView, allInsets.left, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN ); binding.frameLayoutViewMultiRedditDetailActivity.setPadding(0, 0, allInsets.right, 0); } } if (navigationWrapper.bottomAppBar != null) { navigationWrapper.linearLayoutBottomAppBar.setPadding( navigationWrapper.linearLayoutBottomAppBar.getPaddingLeft(), navigationWrapper.linearLayoutBottomAppBar.getPaddingTop(), navigationWrapper.linearLayoutBottomAppBar.getPaddingRight(), allInsets.bottom ); } setMargins(binding.toolbarViewMultiRedditDetailActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); return insets; } }); /*adjustToolbar(binding.toolbarViewMultiRedditDetailActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { if (navigationWrapper.navigationRailView == null) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); params.bottomMargin += navBarHeight; navigationWrapper.floatingActionButton.setLayoutParams(params); } }*/ } } multiReddit = getIntent().getParcelableExtra(EXTRA_MULTIREDDIT_DATA); if (multiReddit == null) { multiPath = getIntent().getStringExtra(EXTRA_MULTIREDDIT_PATH); if (multiPath != null) { binding.toolbarViewMultiRedditDetailActivity.setTitle(multiPath.substring(multiPath.lastIndexOf("/", multiPath.length() - 2) + 1)); } else { Toast.makeText(this, R.string.error_getting_multi_reddit_data, Toast.LENGTH_SHORT).show(); finish(); return; } } else { multiPath = multiReddit.getPath(); binding.toolbarViewMultiRedditDetailActivity.setTitle(multiReddit.getDisplayName()); } setSupportActionBar(binding.toolbarViewMultiRedditDetailActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setToolbarGoToTop(binding.toolbarViewMultiRedditDetailActivity); lockBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_BOTTOM_APP_BAR, false); if (savedInstanceState != null) { mFragment = getSupportFragmentManager().getFragment(savedInstanceState, FRAGMENT_OUT_STATE_KEY); getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout_view_multi_reddit_detail_activity, mFragment).commit(); } else { initializeFragment(); } navigationWrapper.floatingActionButton.setVisibility(hideFab ? View.GONE : View.VISIBLE); if (showBottomAppBar) { int optionCount = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_COUNT, 4); int option1 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_1, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME); int option2 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_2, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS); if (optionCount == 2) { navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2)); navigationWrapper.bindOptions(option1, option2); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option2BottomAppBar, option1); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option4BottomAppBar, option2); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } return false; }); } } else { int option3 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_3, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS : SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX); int option4 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_4, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH : SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE); navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2), getBottomAppBarOptionDrawableResource(option3), getBottomAppBarOptionDrawableResource(option4)); navigationWrapper.bindOptions(option1, option2, option3, option4); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option1BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); navigationWrapper.option3BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option3); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option4); }); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option1BottomAppBar, option1); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option2BottomAppBar, option2); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option3BottomAppBar, option3); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option4BottomAppBar, option4); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } else if (itemId == R.id.navigation_rail_option_3) { bottomAppBarOptionAction(option3); return true; } else if (itemId == R.id.navigation_rail_option_4) { bottomAppBarOptionAction(option4); return true; } return false; }); } } } else { CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); lp.setAnchorId(View.NO_ID); lp.gravity = Gravity.END | Gravity.BOTTOM; navigationWrapper.floatingActionButton.setLayoutParams(lp); } fabOption = mBottomAppBarSharedPreference.getInt(SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS); switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_refresh_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_sort_toolbar_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_post_layout_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_search_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_subreddit_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_user_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_hide_read_posts_day_night_24dp); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_keyboard_double_arrow_up_day_night_24dp); break; default: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_add_day_night_24dp); } break; } setOtherActivitiesFabContentDescription(navigationWrapper.floatingActionButton, fabOption); navigationWrapper.floatingActionButton.setOnClickListener(view -> { switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: { if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).refresh(); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: { showSortTypeBottomSheetFragment(); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: { showPostLayoutBottomSheetFragment(); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).hideReadPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).filterPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).goBackToTop(); } break; default: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } }); navigationWrapper.floatingActionButton.setOnLongClickListener(view -> { FABMoreOptionsBottomSheetFragment fabMoreOptionsBottomSheetFragment = new FABMoreOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(FABMoreOptionsBottomSheetFragment.EXTRA_ANONYMOUS_MODE, accountName.equals(Account.ANONYMOUS_ACCOUNT)); fabMoreOptionsBottomSheetFragment.setArguments(bundle); fabMoreOptionsBottomSheetFragment.show(getSupportFragmentManager(), fabMoreOptionsBottomSheetFragment.getTag()); return true; }); navigationWrapper.bottomAppBar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { navigationWrapper.bottomAppBar.getViewTreeObserver().removeOnGlobalLayoutListener(this); setInboxCount(mCurrentAccountSharedPreferences.getInt(SharedPreferencesUtils.INBOX_COUNT, 0)); } }); } private void initializeFragment() { mFragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putString(PostFragment.EXTRA_NAME, multiPath); bundle.putInt(PostFragment.EXTRA_POST_TYPE, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT : PostPagingSource.TYPE_MULTI_REDDIT); mFragment.setArguments(bundle); getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout_view_multi_reddit_detail_activity, mFragment).commit(); } private void bottomAppBarOptionAction(int option) { switch (option) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME: { EventBus.getDefault().post(new GoBackToMainPageEvent()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX: { Intent intent = new Intent(this, InboxActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE: { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SHOW_MULTIREDDITS, true); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: { PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH: { if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).refresh(); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: { showSortTypeBottomSheetFragment(); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: { showPostLayoutBottomSheetFragment(); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).hideReadPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).filterPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_UPVOTED); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_DOWNVOTED); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_HIDDEN); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED: { Intent intent = new Intent(ViewMultiRedditDetailActivity.this, AccountSavedThingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: { if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).goBackToTop(); } break; } } } private int getBottomAppBarOptionDrawableResource(int option) { switch (option) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME: return R.drawable.ic_home_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: return R.drawable.ic_subscriptions_bottom_app_bar_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX: return R.drawable.ic_inbox_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE: return R.drawable.ic_account_circle_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: return R.drawable.ic_multi_reddit_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: return R.drawable.ic_add_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH: return R.drawable.ic_refresh_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: return R.drawable.ic_sort_toolbar_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: return R.drawable.ic_post_layout_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH: return R.drawable.ic_search_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: return R.drawable.ic_subreddit_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER: return R.drawable.ic_user_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: return R.drawable.ic_hide_read_posts_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: return R.drawable.ic_filter_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED: return R.drawable.ic_arrow_upward_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED: return R.drawable.ic_arrow_downward_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN: return R.drawable.ic_lock_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED: return R.drawable.ic_bookmarks_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: return R.drawable.ic_keyboard_double_arrow_up_day_night_24dp; } } private void showSortTypeBottomSheetFragment() { if (mFragment instanceof PostFragment) { SortTypeBottomSheetFragment sortTypeBottomSheetFragment = SortTypeBottomSheetFragment.getNewInstance(true, ((PostFragment) mFragment).getSortType()); sortTypeBottomSheetFragment.show(getSupportFragmentManager(), sortTypeBottomSheetFragment.getTag()); } } private void showPostLayoutBottomSheetFragment() { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); } private void goToSubreddit() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); RecyclerView recyclerView = rootView.findViewById(R.id.recycler_view_go_to_thing_edit_text); thingEditText.requestFocus(); SubredditAutocompleteRecyclerViewAdapter adapter = new SubredditAutocompleteRecyclerViewAdapter( this, mCustomThemeWrapper, subredditData -> { Utils.hideKeyboard(this); Intent intent = new Intent(this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditData.getName()); startActivity(intent); }); recyclerView.setAdapter(adapter); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); return true; } return false; }); Handler handler = new Handler(); boolean nsfw = mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); thingEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { if (subredditAutocompleteCall != null && subredditAutocompleteCall.isExecuted()) { subredditAutocompleteCall.cancel(); } if (autoCompleteRunnable != null) { handler.removeCallbacks(autoCompleteRunnable); } } @Override public void afterTextChanged(Editable editable) { String currentQuery = editable.toString().trim(); if (!currentQuery.isEmpty()) { autoCompleteRunnable = () -> { subredditAutocompleteCall = mOauthRetrofit.create(RedditAPI.class).subredditAutocomplete(APIUtils.getOAuthHeader(accessToken), currentQuery, nsfw); subredditAutocompleteCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { subredditAutocompleteCall = null; if (response.isSuccessful()) { ParseSubredditData.parseSubredditListingData(mExecutor, handler, response.body(), nsfw, new ParseSubredditData.ParseSubredditListingDataListener() { @Override public void onParseSubredditListingDataSuccess(ArrayList subredditData, String after) { adapter.setSubreddits(subredditData); } @Override public void onParseSubredditListingDataFail() { } }); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { subredditAutocompleteCall = null; } }); }; handler.postDelayed(autoCompleteRunnable, 500); } } }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_subreddit) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } private void goToUser() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); thingEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); return true; } return false; }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_user) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_multi_reddit_detail_activity, menu); if (multiReddit == null && multiPath != null) { String[] segments = multiPath.split("/"); if (segments.length > 2 && !segments[1].equals(accountName)) { menu.findItem(R.id.action_edit_view_multi_reddit_detail_activity).setVisible(false); menu.findItem(R.id.action_delete_view_multi_reddit_detail_activity).setVisible(false); menu.findItem(R.id.action_copy_view_multi_reddit_detail_activity).setVisible(true); } } applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_sort_view_multi_reddit_detail_activity) { showSortTypeBottomSheetFragment(); return true; } else if (itemId == R.id.action_search_view_multi_reddit_detail_activity) { Intent intent = new Intent(this, SearchActivity.class); if (multiReddit == null) { intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_MULTIREDDIT, MultiReddit.getDummyMultiReddit(multiPath)); } else { intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_MULTIREDDIT, multiReddit); } intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, SelectThingReturnKey.THING_TYPE.MULTIREDDIT); startActivity(intent); return true; } else if (itemId == R.id.action_refresh_view_multi_reddit_detail_activity) { if (mFragment instanceof FragmentCommunicator) { ((FragmentCommunicator) mFragment).refresh(); } return true; } else if (itemId == R.id.action_change_post_layout_view_multi_reddit_detail_activity) { showPostLayoutBottomSheetFragment(); return true; } else if (itemId == R.id.action_list_subreddits_view_multi_reddit_detail_activity) { if (multiReddit != null && multiReddit.getSubreddits() != null && !multiReddit.getSubreddits().isEmpty()) { showListSubredditsDialog(multiReddit.getSubreddits()); } else { FetchMultiRedditInfo.FetchMultiRedditInfoListener listener = new FetchMultiRedditInfo.FetchMultiRedditInfoListener() { @Override public void success(MultiReddit fetchedMultiReddit) { if (fetchedMultiReddit.getSubreddits() != null && !fetchedMultiReddit.getSubreddits().isEmpty()) { if (multiReddit != null) { multiReddit.setSubreddits(fetchedMultiReddit.getSubreddits()); } showListSubredditsDialog(fetchedMultiReddit.getSubreddits()); } else { Toast.makeText(ViewMultiRedditDetailActivity.this, R.string.error_getting_multi_reddit_data, Toast.LENGTH_SHORT).show(); } } @Override public void failed() { Toast.makeText(ViewMultiRedditDetailActivity.this, R.string.error_getting_multi_reddit_data, Toast.LENGTH_SHORT).show(); } }; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { FetchMultiRedditInfo.anonymousFetchMultiRedditInfo(mExecutor, new Handler(), mRedditDataRoomDatabase, multiPath, listener); } else { FetchMultiRedditInfo.fetchMultiRedditInfo(mExecutor, new Handler(), mOauthRetrofit, accessToken, multiPath, listener); } } return true; } else if (itemId == R.id.action_edit_view_multi_reddit_detail_activity) { Intent editIntent = new Intent(this, EditMultiRedditActivity.class); editIntent.putExtra(EditMultiRedditActivity.EXTRA_MULTI_PATH, multiPath); startActivity(editIntent); return true; } else if (itemId == R.id.action_delete_view_multi_reddit_detail_activity) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.delete) .setMessage(R.string.delete_multi_reddit_dialog_message) .setPositiveButton(R.string.delete, (dialogInterface, i) -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { DeleteMultiredditInDatabase.deleteMultiredditInDatabase(mExecutor, new Handler(), mRedditDataRoomDatabase, accountName, multiPath, () -> { Toast.makeText(this, R.string.delete_multi_reddit_success, Toast.LENGTH_SHORT).show(); finish(); }); } else { DeleteMultiReddit.deleteMultiReddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, accessToken, accountName, multiPath, new DeleteMultiReddit.DeleteMultiRedditListener() { @Override public void success() { Toast.makeText(ViewMultiRedditDetailActivity.this, R.string.delete_multi_reddit_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new RefreshMultiRedditsEvent()); finish(); } @Override public void failed() { Toast.makeText(ViewMultiRedditDetailActivity.this, R.string.delete_multi_reddit_failed, Toast.LENGTH_SHORT).show(); } }); } }) .setNegativeButton(R.string.cancel, null) .show(); return true; } else if (itemId == R.id.action_copy_view_multi_reddit_detail_activity) { CopyMultiRedditActivity.Companion.start(this, multiPath); return true; } return false; } private void showListSubredditsDialog(ArrayList subreddits) { String title; if (multiReddit != null) { title = multiReddit.getDisplayName() + "'s subreddits"; } else if (multiPath != null) { String[] segments = multiPath.split("/"); title = segments[segments.length - 1] + "'s subreddits"; } else { title = getString(R.string.action_list_subreddits); } String[] subredditNames = new String[subreddits.size()]; for (int i = 0; i < subreddits.size(); i++) { subredditNames[i] = subreddits.get(i).getName(); } new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(title) .setItems(subredditNames, (dialogInterface, i) -> { if (subredditNames[i].startsWith("u_")) { Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, subredditNames[i].substring(2)); startActivity(userIntent); } else { Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditNames[i]); startActivity(subredditIntent); } }) .show(); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); getSupportFragmentManager().putFragment(outState, FRAGMENT_OUT_STATE_KEY, mFragment); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Override public void sortTypeSelected(SortType sortType) { ((PostFragment) mFragment).changeSortType(sortType); displaySortType(); } @Override public void sortTypeSelected(String sortType) { SortTimeBottomSheetFragment sortTimeBottomSheetFragment = new SortTimeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(SortTimeBottomSheetFragment.EXTRA_SORT_TYPE, sortType); sortTimeBottomSheetFragment.setArguments(bundle); sortTimeBottomSheetFragment.show(getSupportFragmentManager(), sortTimeBottomSheetFragment.getTag()); } @Override public void postLayoutSelected(int postLayout) { if (mFragment != null) { mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_MULTI_REDDIT_POST_BASE + multiPath, postLayout).apply(); ((PostFragmentBase) mFragment).changePostLayout(postLayout); } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutViewMultiRedditDetailActivity, binding.collapsingToolbarLayoutViewMultiRedditDetailActivity, binding.toolbarViewMultiRedditDetailActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutViewMultiRedditDetailActivity); navigationWrapper.applyCustomTheme(mCustomThemeWrapper.getBottomAppBarIconColor(), mCustomThemeWrapper.getBottomAppBarBackgroundColor()); applyFABTheme(navigationWrapper.floatingActionButton); } @Override public void onLongPress() { if (mFragment != null) { ((PostFragment) mFragment).goBackToTop(); } } @Override public void displaySortType() { if (mFragment != null) { SortType sortType = ((PostFragment) mFragment).getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.toolbarViewMultiRedditDetailActivity); } } @Override public void markPostAsRead(Post post) { int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, accountName, post.getId(), readPostsLimit); } @Override public void postTypeSelected(int postType) { Intent intent; switch (postType) { case PostTypeBottomSheetFragment.TYPE_TEXT: intent = new Intent(this, PostTextActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_LINK: intent = new Intent(this, PostLinkActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_IMAGE: intent = new Intent(this, PostImageActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_VIDEO: intent = new Intent(this, PostVideoActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_GALLERY: intent = new Intent(this, PostGalleryActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_POLL: intent = new Intent(this, PostPollActivity.class); startActivity(intent); } } @Override public void fabOptionSelected(int option) { switch (option) { case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SUBMIT_POST: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_REFRESH: if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).refresh(); } break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_SORT_TYPE: showSortTypeBottomSheetFragment(); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_POST_LAYOUT: showPostLayoutBottomSheetFragment(); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SEARCH: Intent intent = new Intent(this, SearchActivity.class); startActivity(intent); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_SUBREDDIT: { goToSubreddit(); break; } case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_USER: { goToUser(); break; } case FABMoreOptionsBottomSheetFragment.FAB_HIDE_READ_POSTS: { if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).hideReadPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_FILTER_POSTS: { if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).filterPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_GO_TO_TOP: { if (mFragment instanceof PostFragment) { ((PostFragment) mFragment).goBackToTop(); } break; } } } @Override public void contentScrollUp() { if (showBottomAppBar && !lockBottomAppBar) { navigationWrapper.showNavigation(); } if (!(showBottomAppBar && lockBottomAppBar) && !hideFab) { navigationWrapper.showFab(); } } @Override public void contentScrollDown() { if (!(showBottomAppBar && lockBottomAppBar) && !hideFab) { navigationWrapper.hideFab(); } if (showBottomAppBar && !lockBottomAppBar) { navigationWrapper.hideNavigation(); } } @ExperimentalBadgeUtils private void setInboxCount(int inboxCount) { mHandler.post(() -> navigationWrapper.setInboxCount(this, inboxCount)); } @Subscribe public void goBackToMainPageEvent(GoBackToMainPageEvent event) { finish(); } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } @ExperimentalBadgeUtils @Subscribe public void onChangeInboxCountEvent(ChangeInboxCountEvent event) { setInboxCount(event.inboxCount); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewPostDetailActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static ml.docilealligator.infinityforreddit.activities.CommentActivity.RETURN_EXTRA_COMMENT_DATA_KEY; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.evernote.android.state.State; import com.github.piasy.biv.BigImageViewer; import com.github.piasy.biv.loader.glide.GlideImageLoader; import com.google.android.material.snackbar.Snackbar; import com.livefront.bridge.Bridge; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.AccountManagement; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityViewPostDetailBinding; import ml.docilealligator.infinityforreddit.events.NeedForPostListFromPostFragmentEvent; import ml.docilealligator.infinityforreddit.events.ProvidePostListToViewPostDetailActivityEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.MorePostsInfoFragment; import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment; import ml.docilealligator.infinityforreddit.post.HistoryPostPagingSource; import ml.docilealligator.infinityforreddit.post.LoadingMorePostsStatus; import ml.docilealligator.infinityforreddit.post.ParsePost; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.readpost.NullReadPostsList; import ml.docilealligator.infinityforreddit.readpost.ReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface; import ml.docilealligator.infinityforreddit.thing.SaveThing; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.viewmodels.ViewPostDetailActivityViewModel; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; public class ViewPostDetailActivity extends BaseActivity implements SortTypeSelectionCallback, ActivityToolbarInterface { public static final String EXTRA_POST_DATA = "EPD"; public static final String EXTRA_POST_ID = "EPI"; public static final String EXTRA_POST_LIST_POSITION = "EPLP"; public static final String EXTRA_SINGLE_COMMENT_ID = "ESCI"; public static final String EXTRA_CONTEXT_NUMBER = "ECN"; public static final String EXTRA_MESSAGE_FULLNAME = "ENI"; public static final String EXTRA_NEW_ACCOUNT_NAME = "ENAN"; public static final String EXTRA_POST_FRAGMENT_ID = "EPFI"; public static final String EXTRA_IS_NSFW_SUBREDDIT = "EINS"; public static final int EDIT_COMMENT_REQUEST_CODE = 3; @State String mNewAccountName; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("post_details") SharedPreferences mPostDetailsSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; @State ArrayList posts; @State int postType; @State String subredditName; @State String concatenatedSubredditNames; @State String username; @State String userWhere; @State String multiPath; @State String query; @State String trendingSource; @State PostFilter postFilter; @State SortType.Type sortType; @State SortType.Time sortTime; @State Post post; @State @LoadingMorePostsStatus int mLoadingMorePostsStatus = LoadingMorePostsStatus.NOT_LOADING; public ViewPostDetailActivityViewModel viewPostDetailActivityViewModel; private FragmentManager mFragmentManager; private SectionsPagerAdapter mSectionsPagerAdapter; private long mPostFragmentId; private int mPostListPosition; private boolean mVolumeKeysNavigateComments; private boolean mIsNsfwSubreddit; private boolean mHideFab; private ActivityViewPostDetailBinding binding; @Nullable private ReadPostsListInterface readPostsList; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); BigImageViewer.initialize(GlideImageLoader.with(this.getApplicationContext())); binding = ActivityViewPostDetailBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); Bridge.restoreInstanceState(this, savedInstanceState); EventBus.getDefault().register(this); applyCustomTheme(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutViewPostDetailActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarViewPostDetailActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.viewPager2ViewPostDetailActivity.setPadding(allInsets.left, 0, allInsets.right, 0); binding.searchPanelMaterialCardViewViewPostDetailActivity.setContentPadding(0, 0, 0, allInsets.bottom); setMargins(binding.fabViewPostDetailActivity, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, (int) Utils.convertDpToPixel(16, ViewPostDetailActivity.this) + allInsets.right, (int) Utils.convertDpToPixel(16, ViewPostDetailActivity.this) + allInsets.bottom); return insets; } }); /*adjustToolbar(binding.toolbarViewPostDetailActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.fabViewPostDetailActivity.getLayoutParams(); params.bottomMargin += navBarHeight; binding.fabViewPostDetailActivity.setLayoutParams(params); binding.searchPanelMaterialCardViewViewPostDetailActivity.setContentPadding(binding.searchPanelMaterialCardViewViewPostDetailActivity.getPaddingStart(), binding.searchPanelMaterialCardViewViewPostDetailActivity.getPaddingTop(), binding.searchPanelMaterialCardViewViewPostDetailActivity.getPaddingEnd(), binding.searchPanelMaterialCardViewViewPostDetailActivity.getPaddingBottom() + navBarHeight); }*/ } } boolean swipeBetweenPosts = mSharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_BETWEEN_POSTS, false); if (!swipeBetweenPosts) { attachSliderPanelIfApplicable(); binding.viewPager2ViewPostDetailActivity.setUserInputEnabled(false); } else { mViewPager2 = binding.viewPager2ViewPostDetailActivity; } mSectionsPagerAdapter = new SectionsPagerAdapter(this); binding.viewPager2ViewPostDetailActivity.setAdapter(mSectionsPagerAdapter); mPostFragmentId = getIntent().getLongExtra(EXTRA_POST_FRAGMENT_ID, -1); if (swipeBetweenPosts && posts == null && mPostFragmentId > 0) { EventBus.getDefault().post(new NeedForPostListFromPostFragmentEvent(mPostFragmentId)); } mPostListPosition = getIntent().getIntExtra(EXTRA_POST_LIST_POSITION, -1); mIsNsfwSubreddit = getIntent().getBooleanExtra(EXTRA_IS_NSFW_SUBREDDIT, false); mHideFab = mPostDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_FAB_IN_POST_DETAILS, false); if (mHideFab) { binding.fabViewPostDetailActivity.setVisibility(View.GONE); } mFragmentManager = getSupportFragmentManager(); if (savedInstanceState == null) { post = getIntent().getParcelableExtra(EXTRA_POST_DATA); } binding.toolbarViewPostDetailActivity.setTitle(""); setSupportActionBar(binding.toolbarViewPostDetailActivity); setToolbarGoToTop(binding.toolbarViewPostDetailActivity); if (savedInstanceState == null) { mNewAccountName = getIntent().getStringExtra(EXTRA_NEW_ACCOUNT_NAME); } mVolumeKeysNavigateComments = mSharedPreferences.getBoolean(SharedPreferencesUtils.VOLUME_KEYS_NAVIGATE_COMMENTS, false); binding.fabViewPostDetailActivity.setOnClickListener(view -> { scrollToNextParentComment(); }); binding.fabViewPostDetailActivity.setOnLongClickListener(view -> scrollToPreviousParentComment()); if (accountName.equals(Account.ANONYMOUS_ACCOUNT) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binding.searchTextInputEditTextViewPostDetailActivity.setImeOptions(binding.searchTextInputEditTextViewPostDetailActivity.getImeOptions() | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING); } if (mLoadingMorePostsStatus == LoadingMorePostsStatus.LOADING) { mLoadingMorePostsStatus = LoadingMorePostsStatus.NOT_LOADING; fetchMorePosts(false); } binding.fabViewPostDetailActivity.bindRequiredData( Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay() : null, mPostDetailsSharedPreferences, getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ); binding.fabViewPostDetailActivity.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { binding.fabViewPostDetailActivity.getViewTreeObserver().removeOnGlobalLayoutListener(this); binding.fabViewPostDetailActivity.setCoordinates(); } }); viewPostDetailActivityViewModel = new ViewModelProvider(this, new ViewPostDetailActivityViewModel.Factory(mExecutor, mHandler, mRedditDataRoomDatabase, mRetrofit)).get(ViewPostDetailActivityViewModel.class); checkNewAccountAndBindView(savedInstanceState); } public void setTitle(String title) { binding.toolbarViewPostDetailActivity.setTitle(title); } public void showFab() { if (!mHideFab) { binding.fabViewPostDetailActivity.show(); } } public void hideFab() { if (!mHideFab) { binding.fabViewPostDetailActivity.hide(); } } public void showSnackBar(int resId) { Snackbar.make(binding.getRoot(), resId, Snackbar.LENGTH_SHORT).show(); } public void scrollToNextParentComment() { if (mSectionsPagerAdapter != null) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.scrollToNextParentComment(); } } } public boolean scrollToPreviousParentComment() { if (mSectionsPagerAdapter != null) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.scrollToPreviousParentComment(); return true; } } return false; } public void scrollToParentComment(int position, int currentDepth) { if (mSectionsPagerAdapter != null) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.scrollToParentComment(position, currentDepth); } } } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutViewPostDetailActivity, binding.collapsingToolbarLayoutViewPostDetailActivity, binding.toolbarViewPostDetailActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutViewPostDetailActivity); applyFABTheme(binding.fabViewPostDetailActivity); binding.searchPanelMaterialCardViewViewPostDetailActivity.setBackgroundTintList(ColorStateList.valueOf(mCustomThemeWrapper.getColorPrimary())); int searchPanelTextAndIconColor = mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor(); binding.searchTextInputLayoutViewPostDetailActivity.setBoxStrokeColor(searchPanelTextAndIconColor); binding.searchTextInputLayoutViewPostDetailActivity.setDefaultHintTextColor(ColorStateList.valueOf(searchPanelTextAndIconColor)); binding.searchTextInputEditTextViewPostDetailActivity.setTextColor(searchPanelTextAndIconColor); binding.previousResultImageViewViewPostDetailActivity.setColorFilter(searchPanelTextAndIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.nextResultImageViewViewPostDetailActivity.setColorFilter(searchPanelTextAndIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.closeSearchPanelImageViewViewPostDetailActivity.setColorFilter(searchPanelTextAndIconColor, android.graphics.PorterDuff.Mode.SRC_IN); if (typeface != null) { binding.searchTextInputLayoutViewPostDetailActivity.setTypeface(typeface); binding.searchTextInputEditTextViewPostDetailActivity.setTypeface(typeface); } } private void checkNewAccountAndBindView(Bundle savedInstanceState) { if (mNewAccountName != null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT) || !accountName.equals(mNewAccountName)) { AccountManagement.switchAccount(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), mNewAccountName, newAccount -> { EventBus.getDefault().post(new SwitchAccountEvent(getClass().getName())); Toast.makeText(this, R.string.account_switched, Toast.LENGTH_SHORT).show(); mNewAccountName = null; if (newAccount != null) { accessToken = newAccount.getAccessToken(); accountName = newAccount.getAccountName(); } bindView(savedInstanceState); }); } else { bindView(savedInstanceState); } } else { bindView(savedInstanceState); } } private void bindView(Bundle savedInstanceState) { if (savedInstanceState == null) { binding.viewPager2ViewPostDetailActivity.setCurrentItem(getIntent().getIntExtra(EXTRA_POST_LIST_POSITION, 0), false); } binding.viewPager2ViewPostDetailActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (posts != null && position > posts.size() - 5) { fetchMorePosts(false); } } }); binding.searchPanelMaterialCardViewViewPostDetailActivity.setOnClickListener(null); binding.nextResultImageViewViewPostDetailActivity.setOnClickListener(view -> { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { searchComment(fragment, true); } }); binding.previousResultImageViewViewPostDetailActivity.setOnClickListener(view -> { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { searchComment(fragment, false); } }); binding.closeSearchPanelImageViewViewPostDetailActivity.setOnClickListener(view -> { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.resetSearchCommentIndex(); } binding.searchPanelMaterialCardViewViewPostDetailActivity.setVisibility(View.GONE); }); } public boolean isNsfwSubreddit() { return mIsNsfwSubreddit; } private void editComment(Comment comment, int position) { if (mSectionsPagerAdapter != null) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.editComment(comment, position); } } } private void editComment(String commentContentMarkdown, int position) { if (mSectionsPagerAdapter != null) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.editComment(commentContentMarkdown, position); } } } public void deleteComment(String fullName, int position) { if (mSectionsPagerAdapter != null) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.deleteComment(fullName, position); } } } public void toggleReplyNotifications(Comment comment, int position) { if (mSectionsPagerAdapter != null) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.toggleReplyNotifications(comment, position); } } } public void saveComment(@NonNull Comment comment, int position) { if (comment.isSaved()) { comment.setSaved(false); SaveThing.unsaveThing(mOauthRetrofit, accessToken, comment.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.saveComment(position, false); } Toast.makeText(ViewPostDetailActivity.this, R.string.comment_unsaved_success, Toast.LENGTH_SHORT).show(); } @Override public void failed() { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.saveComment(position, true); } Toast.makeText(ViewPostDetailActivity.this, R.string.comment_unsaved_failed, Toast.LENGTH_SHORT).show(); } }); } else { comment.setSaved(true); SaveThing.saveThing(mOauthRetrofit, accessToken, comment.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.saveComment(position, true); } Toast.makeText(ViewPostDetailActivity.this, R.string.comment_saved_success, Toast.LENGTH_SHORT).show(); } @Override public void failed() { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.saveComment(position, false); } Toast.makeText(ViewPostDetailActivity.this, R.string.comment_saved_failed, Toast.LENGTH_SHORT).show(); } }); } } public boolean toggleSearchPanelVisibility() { if (binding.searchPanelMaterialCardViewViewPostDetailActivity.getVisibility() == View.GONE) { binding.searchPanelMaterialCardViewViewPostDetailActivity.setVisibility(View.VISIBLE); return false; } else { binding.searchPanelMaterialCardViewViewPostDetailActivity.setVisibility(View.GONE); binding.searchTextInputEditTextViewPostDetailActivity.setText(""); return true; } } public void searchComment(ViewPostDetailFragment fragment, boolean searchNextComment) { if (!binding.searchTextInputEditTextViewPostDetailActivity.getText().toString().isEmpty()) { fragment.searchComment(binding.searchTextInputEditTextViewPostDetailActivity.getText().toString(), searchNextComment); } } public void fetchMorePosts(boolean changePage) { if (mLoadingMorePostsStatus == LoadingMorePostsStatus.LOADING || mLoadingMorePostsStatus == LoadingMorePostsStatus.NO_MORE_POSTS) { return; } mLoadingMorePostsStatus = LoadingMorePostsStatus.LOADING; MorePostsInfoFragment morePostsFragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (morePostsFragment != null) { morePostsFragment.setStatus(LoadingMorePostsStatus.LOADING); } Handler handler = new Handler(Looper.getMainLooper()); if (postType != HistoryPostPagingSource.TYPE_READ_POSTS) { mExecutor.execute(() -> { RedditAPI api = (accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit).create(RedditAPI.class); Call call; String afterKey = posts.isEmpty() ? null : posts.get(posts.size() - 1).getFullName(); switch (postType) { case PostPagingSource.TYPE_SUBREDDIT: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { call = api.getSubredditBestPosts(subredditName, sortType, sortTime, afterKey, APIUtils.subredditAPICallLimit(subredditName)); } else { call = api.getSubredditBestPostsOauth(subredditName, sortType, sortTime, afterKey, APIUtils.subredditAPICallLimit(subredditName), APIUtils.getOAuthHeader(accessToken)); } break; case PostPagingSource.TYPE_USER: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { call = api.getUserPosts(username, afterKey, sortType, sortTime); } else { call = api.getUserPostsOauth(username, userWhere, afterKey, sortType, sortTime, APIUtils.getOAuthHeader(accessToken)); } break; case PostPagingSource.TYPE_SEARCH: if (subredditName == null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { call = api.searchPosts(query, afterKey, sortType, sortTime, trendingSource); } else { call = api.searchPostsOauth(query, afterKey, sortType, sortTime, trendingSource, APIUtils.getOAuthHeader(accessToken)); } } else { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { call = api.searchPostsInSpecificSubreddit(subredditName, query, sortType, sortTime, afterKey); } else { call = api.searchPostsInSpecificSubredditOauth(subredditName, query, sortType, sortTime, afterKey, APIUtils.getOAuthHeader(accessToken)); } } break; case PostPagingSource.TYPE_MULTI_REDDIT: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { call = api.getMultiRedditPosts(multiPath, afterKey, sortTime); } else { call = api.getMultiRedditPostsOauth(multiPath, afterKey, sortTime, APIUtils.getOAuthHeader(accessToken)); } break; case PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE: case PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: call = api.getAnonymousFrontPageOrMultiredditPosts(concatenatedSubredditNames, sortType, sortTime, afterKey, APIUtils.subredditAPICallLimit(subredditName), APIUtils.ANONYMOUS_USER_AGENT); break; default: call = api.getBestPosts(sortType, sortTime, afterKey, APIUtils.getOAuthHeader(accessToken)); } try { Response response = call.execute(); if (response.isSuccessful()) { String responseString = response.body(); LinkedHashSet newPosts = ParsePost.parsePostsSync(responseString, -1, postFilter, readPostsList); if (newPosts == null) { handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.NO_MORE_POSTS; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.NO_MORE_POSTS); } }); } else { LinkedHashSet postLinkedHashSet = new LinkedHashSet<>(posts); int currentPostsSize = postLinkedHashSet.size(); postLinkedHashSet.addAll(newPosts); if (currentPostsSize == postLinkedHashSet.size()) { handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.NO_MORE_POSTS; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.NO_MORE_POSTS); } }); } else { posts = new ArrayList<>(postLinkedHashSet); handler.post(() -> { if (changePage) { binding.viewPager2ViewPostDetailActivity.setCurrentItem(currentPostsSize - 1, false); } mSectionsPagerAdapter.notifyItemRangeInserted(currentPostsSize, postLinkedHashSet.size() - currentPostsSize); mLoadingMorePostsStatus = LoadingMorePostsStatus.NOT_LOADING; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.NOT_LOADING); } }); } } } else { handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.FAILED; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.FAILED); } }); } } catch (IOException e) { e.printStackTrace(); handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.FAILED; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.FAILED); } }); } }); } else { mExecutor.execute(() -> { long lastItem = 0; if (!posts.isEmpty()) { lastItem = mRedditDataRoomDatabase.readPostDao().getReadPost(posts.get(posts.size() - 1).getId()).getTime(); } List readPosts = mRedditDataRoomDatabase.readPostDao().getAllReadPosts(accountName, lastItem); StringBuilder ids = new StringBuilder(); for (ReadPost readPost : readPosts) { ids.append("t3_").append(readPost.getId()).append(","); } if (ids.length() > 0) { ids.deleteCharAt(ids.length() - 1); } Call historyPosts; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { historyPosts = mOauthRetrofit.create(RedditAPI.class).getInfoOauth(ids.toString(), APIUtils.getOAuthHeader(accessToken)); } else { historyPosts = mRetrofit.create(RedditAPI.class).getInfo(ids.toString()); } try { Response response = historyPosts.execute(); if (response.isSuccessful()) { String responseString = response.body(); LinkedHashSet newPosts = ParsePost.parsePostsSync(responseString, -1, postFilter, NullReadPostsList.getInstance()); if (newPosts == null || newPosts.isEmpty()) { handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.NO_MORE_POSTS; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.NO_MORE_POSTS); } }); } else { LinkedHashSet postLinkedHashSet = new LinkedHashSet<>(posts); int currentPostsSize = postLinkedHashSet.size(); postLinkedHashSet.addAll(newPosts); if (currentPostsSize == postLinkedHashSet.size()) { handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.NO_MORE_POSTS; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.NO_MORE_POSTS); } }); } else { posts = new ArrayList<>(postLinkedHashSet); handler.post(() -> { if (changePage) { binding.viewPager2ViewPostDetailActivity.setCurrentItem(currentPostsSize - 1, false); } mSectionsPagerAdapter.notifyItemRangeInserted(currentPostsSize, postLinkedHashSet.size() - currentPostsSize); mLoadingMorePostsStatus = LoadingMorePostsStatus.NOT_LOADING; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.NOT_LOADING); } }); } } } else { handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.FAILED; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.FAILED); } }); } } catch (IOException e) { e.printStackTrace(); handler.post(() -> { mLoadingMorePostsStatus = LoadingMorePostsStatus.FAILED; MorePostsInfoFragment fragment = mSectionsPagerAdapter.getMorePostsInfoFragment(); if (fragment != null) { fragment.setStatus(LoadingMorePostsStatus.FAILED); } }); } }); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } @Subscribe public void onProvidePostListToViewPostDetailActivityEvent(ProvidePostListToViewPostDetailActivityEvent event) { if (event.postFragmentId == mPostFragmentId && posts == null) { this.posts = event.posts; this.postType = event.postType; this.subredditName = event.subredditName; this.concatenatedSubredditNames = event.concatenatedSubredditNames; this.username = event.username; this.userWhere = event.userWhere; this.multiPath = event.multiPath; this.query = event.query; this.trendingSource = event.trendingSource; this.postFilter = event.postFilter; this.sortType = event.sortType.getType(); this.sortTime = event.sortType.getTime(); this.readPostsList = event.readPostsList; if (mSectionsPagerAdapter != null) { if (mPostListPosition > 0) mSectionsPagerAdapter.notifyDataSetChanged(); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_post_detail_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { triggerBackPress(); return true; } else if (item.getItemId() == R.id.action_reset_fab_position_view_post_detail_activity) { binding.fabViewPostDetailActivity.resetCoordinates(); return true; } else if (item.getItemId() == R.id.action_next_parent_comment_view_post_detail_activity) { scrollToNextParentComment(); return true; } else if (item.getItemId() == R.id.action_previous_parent_comment_view_post_detail_activity) { scrollToPreviousParentComment(); return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == EDIT_COMMENT_REQUEST_CODE) { if (data != null && resultCode == Activity.RESULT_OK) { if (data.hasExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT)) { editComment((Comment) data.getParcelableExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT), data.getIntExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT_POSITION, -1)); } else { editComment(data.getStringExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT_CONTENT), data.getIntExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT_POSITION, -1)); } } } else if (requestCode == CommentActivity.WRITE_COMMENT_REQUEST_CODE) { if (data != null && resultCode == Activity.RESULT_OK) { if (data.hasExtra(RETURN_EXTRA_COMMENT_DATA_KEY)) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { Comment comment = data.getParcelableExtra(RETURN_EXTRA_COMMENT_DATA_KEY); if (comment != null && comment.getDepth() == 0) { fragment.addComment(comment); } else { String parentFullname = data.getStringExtra(CommentActivity.EXTRA_PARENT_FULLNAME_KEY); int parentPosition = data.getIntExtra(CommentActivity.EXTRA_PARENT_POSITION_KEY, -1); if (parentFullname != null && parentPosition >= 0) { fragment.addChildComment(comment, parentFullname, parentPosition); } } } } else { Toast.makeText(this, R.string.send_comment_failed, Toast.LENGTH_SHORT).show(); } } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Bridge.saveInstanceState(this, outState); } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); Bridge.clear(this); BigImageViewer.imageLoader().cancelAll(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mVolumeKeysNavigateComments) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: scrollToPreviousParentComment(); return true; case KeyEvent.KEYCODE_VOLUME_DOWN: scrollToNextParentComment(); return true; } } return super.onKeyDown(keyCode, event); } @Override public void sortTypeSelected(SortType sortType) { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.changeSortType(sortType); binding.toolbarViewPostDetailActivity.setTitle(sortType.getType().fullName); } } @Override public void onLongPress() { ViewPostDetailFragment fragment = mSectionsPagerAdapter.getCurrentFragment(); if (fragment != null) { fragment.goToTop(); } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } public void loadAuthorIcons(List comments, ViewPostDetailActivityViewModel.LoadIconListener loadIconListener) { viewPostDetailActivityViewModel.loadAuthorImages(comments, loadIconListener); } private class SectionsPagerAdapter extends FragmentStateAdapter { public SectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); } @NonNull @Override public Fragment createFragment(int position) { ViewPostDetailFragment fragment = new ViewPostDetailFragment(); Bundle bundle = new Bundle(); if (posts != null) { if (mPostListPosition == position && post != null) { bundle.putParcelable(ViewPostDetailFragment.EXTRA_POST_DATA, post); bundle.putInt(ViewPostDetailFragment.EXTRA_POST_LIST_POSITION, position); bundle.putString(ViewPostDetailFragment.EXTRA_SINGLE_COMMENT_ID, getIntent().getStringExtra(EXTRA_SINGLE_COMMENT_ID)); bundle.putString(ViewPostDetailFragment.EXTRA_CONTEXT_NUMBER, getIntent().getStringExtra(EXTRA_CONTEXT_NUMBER)); bundle.putString(ViewPostDetailFragment.EXTRA_MESSAGE_FULLNAME, getIntent().getStringExtra(EXTRA_MESSAGE_FULLNAME)); } else { if (position >= posts.size()) { MorePostsInfoFragment morePostsInfoFragment = new MorePostsInfoFragment(); Bundle moreBundle = new Bundle(); moreBundle.putInt(MorePostsInfoFragment.EXTRA_STATUS, mLoadingMorePostsStatus); morePostsInfoFragment.setArguments(moreBundle); return morePostsInfoFragment; } bundle.putParcelable(ViewPostDetailFragment.EXTRA_POST_DATA, posts.get(position)); bundle.putInt(ViewPostDetailFragment.EXTRA_POST_LIST_POSITION, position); } } else { if (post == null) { bundle.putString(ViewPostDetailFragment.EXTRA_POST_ID, getIntent().getStringExtra(EXTRA_POST_ID)); } else { bundle.putParcelable(ViewPostDetailFragment.EXTRA_POST_DATA, post); bundle.putInt(ViewPostDetailFragment.EXTRA_POST_LIST_POSITION, mPostListPosition); } bundle.putString(ViewPostDetailFragment.EXTRA_SINGLE_COMMENT_ID, getIntent().getStringExtra(EXTRA_SINGLE_COMMENT_ID)); bundle.putString(ViewPostDetailFragment.EXTRA_CONTEXT_NUMBER, getIntent().getStringExtra(EXTRA_CONTEXT_NUMBER)); bundle.putString(ViewPostDetailFragment.EXTRA_MESSAGE_FULLNAME, getIntent().getStringExtra(EXTRA_MESSAGE_FULLNAME)); } fragment.setArguments(bundle); return fragment; } @Override public int getItemCount() { return posts == null ? 1 : posts.size() + 1; } @Nullable ViewPostDetailFragment getCurrentFragment() { if (mFragmentManager == null) { return null; } Fragment fragment = mFragmentManager.findFragmentByTag("f" + binding.viewPager2ViewPostDetailActivity.getCurrentItem()); if (fragment instanceof ViewPostDetailFragment) { return (ViewPostDetailFragment) fragment; } return null; } @Nullable MorePostsInfoFragment getMorePostsInfoFragment() { if (posts == null || mFragmentManager == null) { return null; } Fragment fragment = mFragmentManager.findFragmentByTag("f" + posts.size()); if (fragment instanceof MorePostsInfoFragment) { return (MorePostsInfoFragment) fragment; } return null; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewPrivateMessagesActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.transition.AutoTransition; import androidx.transition.TransitionManager; import com.evernote.android.state.State; import com.google.android.material.snackbar.Snackbar; import com.livefront.bridge.Bridge; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.adapters.PrivateMessagesDetailRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.asynctasks.LoadUserData; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ActivityViewPrivateMessagesBinding; import ml.docilealligator.infinityforreddit.events.PassPrivateMessageEvent; import ml.docilealligator.infinityforreddit.events.PassPrivateMessageIndexEvent; import ml.docilealligator.infinityforreddit.events.RepliedToPrivateMessageEvent; import ml.docilealligator.infinityforreddit.message.Message; import ml.docilealligator.infinityforreddit.message.ReadMessage; import ml.docilealligator.infinityforreddit.message.ReplyMessage; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class ViewPrivateMessagesActivity extends BaseActivity implements ActivityToolbarInterface { public static final String EXTRA_PRIVATE_MESSAGE_INDEX = "EPM"; public static final String EXTRA_MESSAGE_POSITION = "EMP"; private static final String USER_AVATAR_STATE = "UAS"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private LinearLayoutManagerBugFixed mLinearLayoutManager; private PrivateMessagesDetailRecyclerViewAdapter mAdapter; @State Message privateMessage; @State Message replyTo; private String mUserAvatar; private ArrayList mProvideUserAvatarCallbacks; private boolean isLoadingUserAvatar = false; private boolean isSendingMessage = false; private int mSecondaryTextColor; private int mSendMessageIconColor; private ActivityViewPrivateMessagesBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); binding = ActivityViewPrivateMessagesBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); Bridge.restoreInstanceState(this, savedInstanceState); EventBus.getDefault().register(this); applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutViewPrivateMessagesActivity); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarViewPrivateMessagesActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.linearLayoutViewPrivateMessagesActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom); return insets; } }); } setSupportActionBar(binding.toolbarViewPrivateMessagesActivity); setToolbarGoToTop(binding.toolbarViewPrivateMessagesActivity); mProvideUserAvatarCallbacks = new ArrayList<>(); if (savedInstanceState != null) { mUserAvatar = savedInstanceState.getString(USER_AVATAR_STATE); if (privateMessage == null) { EventBus.getDefault().post(new PassPrivateMessageIndexEvent(getIntent().getIntExtra(EXTRA_PRIVATE_MESSAGE_INDEX, -1))); } else { bindView(); } } else { EventBus.getDefault().post(new PassPrivateMessageIndexEvent(getIntent().getIntExtra(EXTRA_PRIVATE_MESSAGE_INDEX, -1))); } } private void bindView() { setTitle(privateMessage.getRecipient(accountName)); if (privateMessage != null) { if (privateMessage.getAuthor().equals(accountName)) { binding.toolbarViewPrivateMessagesActivity.setOnClickListener(view -> { if (privateMessage.isDestinationDeleted()) { return; } if (privateMessage.getDestination().startsWith("#")) { Intent intent = new Intent(this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, privateMessage.getSubredditName()); startActivity(intent); } else { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, privateMessage.getDestination()); startActivity(intent); } }); } else { if (privateMessage.getAuthor().equals("null")) { binding.toolbarViewPrivateMessagesActivity.setOnClickListener(view -> { Intent intent = new Intent(this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, privateMessage.getSubredditName()); startActivity(intent); }); } else { binding.toolbarViewPrivateMessagesActivity.setOnClickListener(view -> { if (privateMessage.isAuthorDeleted()) { return; } Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, privateMessage.getAuthor()); startActivity(intent); }); } } } mAdapter = new PrivateMessagesDetailRecyclerViewAdapter(this, mSharedPreferences, getResources().getConfiguration().locale, privateMessage, accountName, mCustomThemeWrapper); mLinearLayoutManager = new LinearLayoutManagerBugFixed(this); mLinearLayoutManager.setStackFromEnd(true); binding.recyclerViewViewPrivateMessagesActivity.setLayoutManager(mLinearLayoutManager); binding.recyclerViewViewPrivateMessagesActivity.setAdapter(mAdapter); goToBottom(); binding.sendImageViewViewPrivateMessagesActivity.setOnClickListener(view -> { if (!isSendingMessage) { if (!binding.editTextViewPrivateMessagesActivity.getText().toString().equals("")) { //Send Message if (privateMessage != null) { ArrayList replies = privateMessage.getReplies(); if (replyTo == null) { replyTo = privateMessage; } isSendingMessage = true; binding.sendImageViewViewPrivateMessagesActivity.setColorFilter(mSecondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); ReplyMessage.replyMessage(mExecutor, mHandler, binding.editTextViewPrivateMessagesActivity.getText().toString(), replyTo.getFullname(), getResources().getConfiguration().locale, mOauthRetrofit, accessToken, new ReplyMessage.ReplyMessageListener() { @Override public void replyMessageSuccess(Message message) { if (mAdapter != null) { mAdapter.addReply(message); } goToBottom(); binding.editTextViewPrivateMessagesActivity.setText(""); binding.sendImageViewViewPrivateMessagesActivity.setColorFilter(mSendMessageIconColor, android.graphics.PorterDuff.Mode.SRC_IN); isSendingMessage = false; EventBus.getDefault().post(new RepliedToPrivateMessageEvent(message, getIntent().getIntExtra(EXTRA_MESSAGE_POSITION, -1))); } @Override public void replyMessageFailed(String errorMessage) { if (errorMessage != null && !errorMessage.equals("")) { Snackbar.make(binding.getRoot(), errorMessage, Snackbar.LENGTH_LONG).show(); } else { Snackbar.make(binding.getRoot(), R.string.reply_message_failed, Snackbar.LENGTH_LONG).show(); } binding.sendImageViewViewPrivateMessagesActivity.setColorFilter(mSendMessageIconColor, android.graphics.PorterDuff.Mode.SRC_IN); isSendingMessage = false; } }); StringBuilder fullnames = new StringBuilder(); if (privateMessage.isNew()) { fullnames.append(privateMessage.getFullname()).append(","); } if (replies != null && !replies.isEmpty()) { for (Message m : replies) { if (m.isNew()) { fullnames.append(m).append(","); } } } if (fullnames.length() > 0) { fullnames.deleteCharAt(fullnames.length() - 1); ReadMessage.readMessage(mOauthRetrofit, accessToken, fullnames.toString(), new ReadMessage.ReadMessageListener() { @Override public void readSuccess() {} @Override public void readFailed() {} }); } } } } }); } public void fetchUserAvatar(String username, ProvideUserAvatarCallback provideUserAvatarCallback) { if (mUserAvatar == null) { mProvideUserAvatarCallbacks.add(provideUserAvatarCallback); if (!isLoadingUserAvatar) { LoadUserData.loadUserData(mExecutor, new Handler(), mRedditDataRoomDatabase, accessToken, username, mOauthRetrofit, mRetrofit, iconImageUrl -> { isLoadingUserAvatar = false; mUserAvatar = iconImageUrl == null ? "" : iconImageUrl; for (ProvideUserAvatarCallback provideUserAvatarCallbackInArrayList : mProvideUserAvatarCallbacks) { provideUserAvatarCallbackInArrayList.fetchAvatarSuccess(iconImageUrl); } mProvideUserAvatarCallbacks.clear(); }); } } else { provideUserAvatarCallback.fetchAvatarSuccess(mUserAvatar); } } public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewViewPrivateMessagesActivity, new AutoTransition()); } private void goToBottom() { if (mLinearLayoutManager != null && mAdapter != null) { mLinearLayoutManager.scrollToPositionWithOffset(mAdapter.getItemCount() - 1, 0); } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putString(USER_AVATAR_STATE, mUserAvatar); Bridge.saveInstanceState(this, outState); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); Bridge.clear(this); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutViewPrivateMessagesActivity, null, binding.toolbarViewPrivateMessagesActivity); binding.cardViewViewPrivateMessagesActivity.setCardBackgroundColor(mCustomThemeWrapper.getFilledCardViewBackgroundColor()); binding.editTextViewPrivateMessagesActivity.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); mSecondaryTextColor = mCustomThemeWrapper.getSecondaryTextColor(); binding.editTextViewPrivateMessagesActivity.setHintTextColor(mSecondaryTextColor); mSendMessageIconColor = mCustomThemeWrapper.getSendMessageIconColor(); binding.sendImageViewViewPrivateMessagesActivity.setColorFilter(mSendMessageIconColor, android.graphics.PorterDuff.Mode.SRC_IN); if (typeface != null) { binding.editTextViewPrivateMessagesActivity.setTypeface(typeface); } } @Override public void onLongPress() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } @Subscribe public void onPassPrivateMessageEvent(PassPrivateMessageEvent passPrivateMessageEvent) { privateMessage = passPrivateMessageEvent.message; if (privateMessage != null) { if (privateMessage.getAuthor().equals(accountName)) { if (privateMessage.getReplies() != null) { for (int i = privateMessage.getReplies().size() - 1; i >= 0; i--) { if (!privateMessage.getReplies().get(i).getAuthor().equals(accountName)) { replyTo = privateMessage.getReplies().get(i); break; } } } if (replyTo == null) { replyTo = privateMessage; } } else { replyTo = privateMessage; } bindView(); } } public interface ProvideUserAvatarCallback { void fetchAvatarSuccess(String userAvatarUrl); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewRedditGalleryActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Html; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.viewpager.widget.ViewPager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import app.futured.hauler.DragDirection; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; import ml.docilealligator.infinityforreddit.WallpaperSetter; import ml.docilealligator.infinityforreddit.databinding.ActivityViewRedditGalleryBinding; import ml.docilealligator.infinityforreddit.events.FinishViewMediaActivityEvent; import ml.docilealligator.infinityforreddit.font.ContentFontFamily; import ml.docilealligator.infinityforreddit.font.ContentFontStyle; import ml.docilealligator.infinityforreddit.font.FontFamily; import ml.docilealligator.infinityforreddit.font.FontStyle; import ml.docilealligator.infinityforreddit.font.TitleFontFamily; import ml.docilealligator.infinityforreddit.font.TitleFontStyle; import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryImageOrGifFragment; import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryVideoFragment; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ViewRedditGalleryActivity extends AppCompatActivity implements SetAsWallpaperCallback, CustomFontReceiver { public static final String EXTRA_POST = "EP"; public static final String EXTRA_GALLERY_ITEM_INDEX = "EGII"; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject Executor executor; public Typeface typeface; private SectionsPagerAdapter sectionsPagerAdapter; private Post post; private ArrayList gallery; private String subredditName; private boolean isNsfw; private boolean useBottomAppBar; private boolean isActionBarHidden = false; private ActivityViewRedditGalleryBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((Infinity) getApplication()).getAppComponent().inject(this); boolean systemDefault = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; int systemThemeType = Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.THEME_KEY, "2")); switch (systemThemeType) { case 0: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO); getTheme().applyStyle(R.style.Theme_Normal, true); break; case 1: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES); if (sharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); } break; case 2: if (systemDefault) { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM); } else { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY); } if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO) { getTheme().applyStyle(R.style.Theme_Normal, true); } else { if (sharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); } } } getTheme().applyStyle(FontStyle.valueOf(sharedPreferences .getString(SharedPreferencesUtils.FONT_SIZE_KEY, FontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(TitleFontStyle.valueOf(sharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_SIZE_KEY, TitleFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(ContentFontStyle.valueOf(sharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_SIZE_KEY, ContentFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(FontFamily.valueOf(sharedPreferences .getString(SharedPreferencesUtils.FONT_FAMILY_KEY, FontFamily.Default.name())).getResId(), true); getTheme().applyStyle(TitleFontFamily.valueOf(sharedPreferences .getString(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY, TitleFontFamily.Default.name())).getResId(), true); getTheme().applyStyle(ContentFontFamily.valueOf(sharedPreferences .getString(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY, ContentFontFamily.Default.name())).getResId(), true); binding = ActivityViewRedditGalleryBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); useBottomAppBar = sharedPreferences.getBoolean(SharedPreferencesUtils.USE_BOTTOM_TOOLBAR_IN_MEDIA_VIEWER, false); if (!useBottomAppBar) { ActionBar actionBar = getSupportActionBar(); Drawable upArrow = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); actionBar.setHomeAsUpIndicator(upArrow); actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.transparentActionBarAndExoPlayerControllerColor))); setTitle(" "); } else { getSupportActionBar().hide(); } post = getIntent().getParcelableExtra(EXTRA_POST); if (post == null) { finish(); return; } gallery = post.getGallery(); if (gallery == null || gallery.isEmpty()) { finish(); return; } subredditName = post.getSubredditName(); isNsfw = post.isNSFW(); if (sharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_VERTICALLY_TO_GO_BACK_FROM_MEDIA, true)) { binding.getRoot().setOnDragDismissedListener(dragDirection -> { int slide = dragDirection == DragDirection.UP ? R.anim.slide_out_up : R.anim.slide_out_down; finish(); overridePendingTransition(0, slide); }); } else { binding.getRoot().setDragEnabled(false); } setupViewPager(savedInstanceState); } public boolean isUseBottomAppBar() { return useBottomAppBar; } private void setupViewPager(Bundle savedInstanceState) { if (!useBottomAppBar) { setToolbarTitle(0); binding.viewPagerViewRedditGalleryActivity.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { setToolbarTitle(position); } }); } sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); binding.viewPagerViewRedditGalleryActivity.setAdapter(sectionsPagerAdapter); binding.viewPagerViewRedditGalleryActivity.setOffscreenPageLimit(3); if (savedInstanceState == null) { binding.viewPagerViewRedditGalleryActivity.setCurrentItem(getIntent().getIntExtra(EXTRA_GALLERY_ITEM_INDEX, 0), false); } } private void setToolbarTitle(int position) { if (gallery != null && position >= 0 && position < gallery.size()) { if (gallery.get(position).mediaType == Post.Gallery.TYPE_IMAGE) { setTitle(Utils.getTabTextWithCustomFont(typeface, Html.fromHtml("" + getString(R.string.view_reddit_gallery_activity_image_label, position + 1, gallery.size()) + ""))); } else if (gallery.get(position).mediaType == Post.Gallery.TYPE_GIF) { setTitle(Utils.getTabTextWithCustomFont(typeface, Html.fromHtml("" + getString(R.string.view_reddit_gallery_activity_gif_label, position + 1, gallery.size()) + ""))); } else { setTitle(Utils.getTabTextWithCustomFont(typeface, Html.fromHtml("" + getString(R.string.view_reddit_gallery_activity_video_label, position + 1, gallery.size()) + ""))); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_reddit_gallery_activity, menu); for (int i = 0; i < menu.size(); i++) { Utils.setTitleWithCustomFontToMenuItem(typeface, menu.getItem(i), null); } return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_download_all_gallery_media_view_reddit_gallery_activity) { // Check if download locations are set for all media types // Gallery can contain images, GIFs, and videos, so we need to check all relevant locations String imageDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String gifDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); String videoDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); String nsfwDownloadLocation = ""; boolean needsNsfwLocation = isNsfw && sharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false); if (needsNsfwLocation) { nsfwDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); if (nsfwDownloadLocation == null || nsfwDownloadLocation.isEmpty()) { Toast.makeText(this, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return true; } } else { // Check for required download locations based on the gallery content boolean hasImage = false; boolean hasGif = false; boolean hasVideo = false; for (Post.Gallery galleryItem : gallery) { if (galleryItem.mediaType == Post.Gallery.TYPE_VIDEO) { hasVideo = true; } else if (galleryItem.mediaType == Post.Gallery.TYPE_GIF) { hasGif = true; } else { hasImage = true; } } if ((hasImage && (imageDownloadLocation == null || imageDownloadLocation.isEmpty())) || (hasGif && (gifDownloadLocation == null || gifDownloadLocation.isEmpty())) || (hasVideo && (videoDownloadLocation == null || videoDownloadLocation.isEmpty()))) { Toast.makeText(this, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return true; } } //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadMediaService.constructGalleryDownloadAllMediaJobInfo(this, 5000000L * gallery.size(), post); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); Toast.makeText(this, R.string.download_started, Toast.LENGTH_SHORT).show(); return true; } return false; } @Override public void setToHomeScreen(int viewPagerPosition) { if (gallery != null && viewPagerPosition >= 0 && viewPagerPosition < gallery.size()) { WallpaperSetter.set(executor, new Handler(), gallery.get(viewPagerPosition).url, WallpaperSetter.HOME_SCREEN, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewRedditGalleryActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewRedditGalleryActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } } @Override public void setToLockScreen(int viewPagerPosition) { if (gallery != null && viewPagerPosition >= 0 && viewPagerPosition < gallery.size()) { WallpaperSetter.set(executor, new Handler(), gallery.get(viewPagerPosition).url, WallpaperSetter.LOCK_SCREEN, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewRedditGalleryActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewRedditGalleryActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } } @Override public void setToBoth(int viewPagerPosition) { if (gallery != null && viewPagerPosition >= 0 && viewPagerPosition < gallery.size()) { WallpaperSetter.set(executor, new Handler(), gallery.get(viewPagerPosition).url, WallpaperSetter.BOTH_SCREENS, this, new WallpaperSetter.SetWallpaperListener() { @Override public void success() { Toast.makeText(ViewRedditGalleryActivity.this, R.string.wallpaper_set, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewRedditGalleryActivity.this, R.string.error_set_wallpaper, Toast.LENGTH_SHORT).show(); } }); } } public int getCurrentPagePosition() { return binding.viewPagerViewRedditGalleryActivity.getCurrentItem(); } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } public boolean isActionBarHidden() { return isActionBarHidden; } public void setActionBarHidden(boolean isActionBarHidden) { this.isActionBarHidden = isActionBarHidden; } // Add getter for the Post object public Post getPost() { return post; } @Subscribe public void onFinishViewMediaActivityEvent(FinishViewMediaActivityEvent e) { finish(); } private class SectionsPagerAdapter extends FragmentStatePagerAdapter { SectionsPagerAdapter(@NonNull FragmentManager fm) { super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } @NonNull @Override public Fragment getItem(int position) { Post.Gallery media = gallery.get(position); if (media.mediaType == Post.Gallery.TYPE_VIDEO) { ViewRedditGalleryVideoFragment fragment = new ViewRedditGalleryVideoFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(ViewRedditGalleryVideoFragment.EXTRA_REDDIT_GALLERY_VIDEO, media); bundle.putString(ViewRedditGalleryVideoFragment.EXTRA_SUBREDDIT_NAME, subredditName); bundle.putInt(ViewRedditGalleryVideoFragment.EXTRA_INDEX, position); bundle.putInt(ViewRedditGalleryVideoFragment.EXTRA_MEDIA_COUNT, gallery.size()); bundle.putBoolean(ViewRedditGalleryVideoFragment.EXTRA_IS_NSFW, isNsfw); fragment.setArguments(bundle); return fragment; } else { ViewRedditGalleryImageOrGifFragment fragment = new ViewRedditGalleryImageOrGifFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(ViewRedditGalleryImageOrGifFragment.EXTRA_REDDIT_GALLERY_MEDIA, media); bundle.putString(ViewRedditGalleryImageOrGifFragment.EXTRA_SUBREDDIT_NAME, subredditName); bundle.putInt(ViewRedditGalleryImageOrGifFragment.EXTRA_INDEX, position); bundle.putInt(ViewRedditGalleryImageOrGifFragment.EXTRA_MEDIA_COUNT, gallery.size()); bundle.putBoolean(ViewRedditGalleryImageOrGifFragment.EXTRA_IS_NSFW, isNsfw); fragment.setArguments(bundle); return fragment; } } @Override public int getCount() { return gallery.size(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewSubredditDetailActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static android.graphics.BitmapFactory.decodeResource; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.badge.ExperimentalBadgeUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.textfield.TextInputEditText; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.SubredditAutocompleteRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.AccountManagement; import ml.docilealligator.infinityforreddit.asynctasks.AddSubredditOrUserToMultiReddit; import ml.docilealligator.infinityforreddit.asynctasks.CheckIsSubscribedToSubreddit; import ml.docilealligator.infinityforreddit.asynctasks.InsertSubredditData; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CopyTextBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FABMoreOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTimeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.NavigationWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityViewSubredditDetailBinding; import ml.docilealligator.infinityforreddit.events.ChangeInboxCountEvent; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.GoBackToMainPageEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.fragments.SidebarFragment; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.message.ReadMessage; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import ml.docilealligator.infinityforreddit.subreddit.FetchSubredditData; import ml.docilealligator.infinityforreddit.subreddit.ParseSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditSubscription; import ml.docilealligator.infinityforreddit.subreddit.SubredditViewModel; import ml.docilealligator.infinityforreddit.subreddit.shortcut.ShortcutManager; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ViewSubredditDetailActivity extends BaseActivity implements SortTypeSelectionCallback, PostTypeBottomSheetFragment.PostTypeSelectionCallback, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, ActivityToolbarInterface, FABMoreOptionsBottomSheetFragment.FABOptionSelectionCallback, MarkPostAsReadInterface, RecyclerViewContentScrollingInterface { public static final String EXTRA_SUBREDDIT_NAME_KEY = "ESN"; public static final String EXTRA_MESSAGE_FULLNAME = "ENF"; public static final String EXTRA_NEW_ACCOUNT_NAME = "ENAN"; public static final String EXTRA_VIEW_SIDEBAR = "EVSB"; private static final String FETCH_SUBREDDIT_INFO_STATE = "FSIS"; private static final String MESSAGE_FULLNAME_STATE = "MFS"; private static final String NEW_ACCOUNT_NAME_STATE = "NANS"; public SubredditViewModel mSubredditViewModel; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject() @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("bottom_app_bar") SharedPreferences mBottomAppBarSharedPreference; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private FragmentManager fragmentManager; private SectionsPagerAdapter sectionsPagerAdapter; private NavigationWrapper navigationWrapper; private Runnable autoCompleteRunnable; private Call subredditAutocompleteCall; private String subredditName; private String description; private boolean mFetchSubredditInfoSuccess = false; private boolean isNsfwSubreddit = false; private boolean subscriptionReady = false; private boolean showToast = false; private boolean hideFab; private boolean showBottomAppBar; private boolean lockBottomAppBar; private String mMessageFullname; private String mNewAccountName; private RequestManager glide; private int expandedTabTextColor; private int expandedTabBackgroundColor; private int expandedTabIndicatorColor; private int collapsedTabTextColor; private int collapsedTabBackgroundColor; private int collapsedTabIndicatorColor; private int unsubscribedColor; private int subscribedColor; private int fabOption; private int topSystemBarHeight; private MaterialAlertDialogBuilder nsfwWarningBuilder; private Bitmap subredditIconBitmap; private ActivityViewSubredditDetailBinding binding; private ActivityResultLauncher requestMultiredditSelectionLauncher; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityViewSubredditDetailBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); hideFab = mSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_FAB_IN_POST_FEED, false); showBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.BOTTOM_APP_BAR_KEY, false); navigationWrapper = new NavigationWrapper(findViewById(R.id.bottom_app_bar_bottom_app_bar), findViewById(R.id.linear_layout_bottom_app_bar), findViewById(R.id.option_1_bottom_app_bar), findViewById(R.id.option_2_bottom_app_bar), findViewById(R.id.option_3_bottom_app_bar), findViewById(R.id.option_4_bottom_app_bar), findViewById(R.id.fab_view_subreddit_detail_activity), findViewById(R.id.navigation_rail), customThemeWrapper, showBottomAppBar); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); mViewPager2 = binding.viewPagerViewSubredditDetailActivity; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); topSystemBarHeight = allInsets.top; int padding16 = (int) Utils.convertDpToPixel(16, ViewSubredditDetailActivity.this); if (navigationWrapper.navigationRailView == null) { if (navigationWrapper.bottomAppBar.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, padding16 + allInsets.right, padding16 + allInsets.bottom); } else { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, allInsets.bottom); } } else { if (navigationWrapper.navigationRailView.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, padding16 + allInsets.right, padding16 + allInsets.bottom); binding.viewPagerViewSubredditDetailActivity.setPadding(allInsets.left, 0, allInsets.right, 0); } else { navigationWrapper.navigationRailView.setFitsSystemWindows(false); navigationWrapper.navigationRailView.setPadding(0, 0, 0, allInsets.bottom); setMargins(navigationWrapper.navigationRailView, allInsets.left, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN ); binding.viewPagerViewSubredditDetailActivity.setPadding(0, 0, allInsets.right, 0); } } binding.toolbarConstraintLayoutViewSubredditDetailActivity.setPadding( padding16 + allInsets.left, binding.toolbarConstraintLayoutViewSubredditDetailActivity.getPaddingTop(), padding16 + allInsets.right, binding.toolbarConstraintLayoutViewSubredditDetailActivity.getPaddingBottom()); if (navigationWrapper.bottomAppBar != null) { navigationWrapper.linearLayoutBottomAppBar.setPadding( navigationWrapper.linearLayoutBottomAppBar.getPaddingLeft(), navigationWrapper.linearLayoutBottomAppBar.getPaddingTop(), navigationWrapper.linearLayoutBottomAppBar.getPaddingRight(), allInsets.bottom ); } setMargins(binding.toolbar, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.tabLayoutViewSubredditDetailActivity.setPadding(allInsets.left, 0, allInsets.right, 0); return insets; } }); /*adjustToolbar(binding.toolbar); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { if (navigationWrapper.navigationRailView == null) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); params.bottomMargin += navBarHeight; navigationWrapper.floatingActionButton.setLayoutParams(params); } }*/ showToast = true; } View decorView = window.getDecorView(); if (isChangeStatusBarIconColor()) { binding.appbarLayoutViewSubredditDetailActivity.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state) { if (state == State.COLLAPSED) { decorView.setSystemUiVisibility(getSystemVisibilityToolbarCollapsed()); binding.tabLayoutViewSubredditDetailActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutViewSubredditDetailActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutViewSubredditDetailActivity.setBackgroundColor(collapsedTabBackgroundColor); } else if (state == State.EXPANDED) { decorView.setSystemUiVisibility(getSystemVisibilityToolbarExpanded()); binding.tabLayoutViewSubredditDetailActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutViewSubredditDetailActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutViewSubredditDetailActivity.setBackgroundColor(expandedTabBackgroundColor); } } }); } else { binding.appbarLayoutViewSubredditDetailActivity.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, State state) { if (state == State.COLLAPSED) { binding.tabLayoutViewSubredditDetailActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutViewSubredditDetailActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutViewSubredditDetailActivity.setBackgroundColor(collapsedTabBackgroundColor); } else if (state == State.EXPANDED) { binding.tabLayoutViewSubredditDetailActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutViewSubredditDetailActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutViewSubredditDetailActivity.setBackgroundColor(expandedTabBackgroundColor); } } }); } } else { binding.appbarLayoutViewSubredditDetailActivity.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, State state) { if (state == State.EXPANDED) { binding.tabLayoutViewSubredditDetailActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutViewSubredditDetailActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutViewSubredditDetailActivity.setBackgroundColor(expandedTabBackgroundColor); } else if (state == State.COLLAPSED) { binding.tabLayoutViewSubredditDetailActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutViewSubredditDetailActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutViewSubredditDetailActivity.setBackgroundColor(collapsedTabBackgroundColor); } } }); } lockBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_BOTTOM_APP_BAR, false); boolean hideSubredditDescription = mSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_SUBREDDIT_DESCRIPTION, false); subredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME_KEY); fragmentManager = getSupportFragmentManager(); if (savedInstanceState == null) { mMessageFullname = getIntent().getStringExtra(EXTRA_MESSAGE_FULLNAME); mNewAccountName = getIntent().getStringExtra(EXTRA_NEW_ACCOUNT_NAME); } else { mFetchSubredditInfoSuccess = savedInstanceState.getBoolean(FETCH_SUBREDDIT_INFO_STATE); mMessageFullname = savedInstanceState.getString(MESSAGE_FULLNAME_STATE); mNewAccountName = savedInstanceState.getString(NEW_ACCOUNT_NAME_STATE); } sectionsPagerAdapter = new SectionsPagerAdapter(this); checkNewAccountAndBindView(); fetchSubredditData(); String title = "r/" + subredditName; binding.subredditNameTextViewViewSubredditDetailActivity.setText(title); binding.toolbar.setTitle(title); setSupportActionBar(binding.toolbar); setToolbarGoToTop(binding.toolbar); glide = Glide.with(this); Locale locale = getResources().getConfiguration().locale; MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(ViewSubredditDetailActivity.this, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(mCustomThemeWrapper.getLinkColor()); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(getSupportFragmentManager(), null); return true; }; Markwon markwon = MarkdownUtils.createDescriptionMarkwon(this, miscPlugin, onLinkLongClickListener); binding.descriptionTextViewViewSubredditDetailActivity.setOnLongClickListener(view -> { if (description != null && !description.equals("") && binding.descriptionTextViewViewSubredditDetailActivity.getSelectionStart() == -1 && binding.descriptionTextViewViewSubredditDetailActivity.getSelectionEnd() == -1) { CopyTextBottomSheetFragment.show(getSupportFragmentManager(), description, null); return true; } return false; }); mSubredditViewModel = new ViewModelProvider(this, new SubredditViewModel.Factory(mRedditDataRoomDatabase, subredditName)) .get(SubredditViewModel.class); mSubredditViewModel.getSubredditLiveData().observe(this, subredditData -> { if (subredditData != null) { isNsfwSubreddit = subredditData.isNSFW(); if (subredditData.getBannerUrl().equals("")) { binding.iconGifImageViewViewSubredditDetailActivity.setOnClickListener(view -> { //Do nothing as it has no image }); } else { glide.load(subredditData.getBannerUrl()).into(binding.bannerImageViewViewSubredditDetailActivity); binding.bannerImageViewViewSubredditDetailActivity.setOnClickListener(view -> { Intent intent = new Intent(ViewSubredditDetailActivity.this, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, subredditData.getBannerUrl()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, subredditName + "-banner.jpg"); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, subredditName); startActivity(intent); }); } if (subredditData.getIconUrl().equals("")) { glide.load(getDrawable(R.drawable.subreddit_default_icon)) .transform(new RoundedCornersTransformation(216, 0)) .into(binding.iconGifImageViewViewSubredditDetailActivity); binding.iconGifImageViewViewSubredditDetailActivity.setOnClickListener(null); } else { // Note: This already uses asBitmap() which loads only the first frame, // effectively disabling animation for all cases. The setting check is // kept here for consistency but asBitmap() always prevents animation. glide.asBitmap() .load(subredditData.getIconUrl()) .transform(new RoundedCornersTransformation(216, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(216, 0))) .into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { subredditIconBitmap = resource; binding.iconGifImageViewViewSubredditDetailActivity.setImageBitmap(resource); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { subredditIconBitmap = null; } }); binding.iconGifImageViewViewSubredditDetailActivity.setOnClickListener(view -> { Intent intent = new Intent(ViewSubredditDetailActivity.this, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, subredditData.getIconUrl()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, subredditName + "-icon.jpg"); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, subredditName); startActivity(intent); }); } String subredditFullName = "r/" + subredditData.getName(); if (!title.equals(subredditFullName)) { getSupportActionBar().setTitle(subredditFullName); } binding.subredditNameTextViewViewSubredditDetailActivity.setText(subredditFullName); String nSubscribers = getString(R.string.subscribers_number_detail, subredditData.getNSubscribers()); binding.subscriberCountTextViewViewSubredditDetailActivity.setText(nSubscribers); binding.creationTimeTextViewViewSubredditDetailActivity.setText(new SimpleDateFormat("MMM d, yyyy", locale).format(subredditData.getCreatedUTC())); description = subredditData.getDescription(); if (hideSubredditDescription || description.equals("")) { binding.descriptionTextViewViewSubredditDetailActivity.setVisibility(View.GONE); } else { binding.descriptionTextViewViewSubredditDetailActivity.setVisibility(View.VISIBLE); markwon.setMarkdown(binding.descriptionTextViewViewSubredditDetailActivity, description); } if (subredditData.isNSFW()) { if (nsfwWarningBuilder == null && mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) || !mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false)) { nsfwWarningBuilder = new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.warning) .setMessage(R.string.this_is_a_nsfw_subreddit) .setPositiveButton(R.string.leave, (dialogInterface, i) -> { finish(); }) .setNegativeButton(R.string.dismiss, null); nsfwWarningBuilder.show(); } } } }); requestMultiredditSelectionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { Intent data = result.getData(); if (data != null) { MultiReddit multiReddit = data.getParcelableExtra(SelectThingReturnKey.RETRUN_EXTRA_MULTIREDDIT); if (multiReddit != null) { AddSubredditOrUserToMultiReddit.addSubredditOrUserToMultiReddit(mOauthRetrofit, accessToken, multiReddit.getPath(), subredditName, new AddSubredditOrUserToMultiReddit.AddSubredditOrUserToMultiRedditListener() { @Override public void success() { Toast.makeText(ViewSubredditDetailActivity.this, getString(R.string.add_subreddit_or_user_to_multireddit_success, subredditName, multiReddit.getDisplayName()), Toast.LENGTH_LONG).show(); } @Override public void failed(int code) { Toast.makeText(ViewSubredditDetailActivity.this, getString(R.string.add_subreddit_or_user_to_multireddit_failed, subredditName, multiReddit.getDisplayName()), Toast.LENGTH_LONG).show(); } }); } } }); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (sectionsPagerAdapter != null) { return sectionsPagerAdapter.handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); binding.appbarLayoutViewSubredditDetailActivity.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { binding.appbarLayoutViewSubredditDetailActivity.getViewTreeObserver().removeOnGlobalLayoutListener(this); binding.collapsingToolbarLayoutViewSubredditDetailActivity.setScrimVisibleHeightTrigger(binding.toolbar.getHeight() + binding.tabLayoutViewSubredditDetailActivity.getHeight() + topSystemBarHeight * 2); } }); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutViewSubredditDetailActivity, binding.collapsingToolbarLayoutViewSubredditDetailActivity, binding.toolbar, false); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutViewSubredditDetailActivity, binding.tabLayoutViewSubredditDetailActivity); expandedTabTextColor = mCustomThemeWrapper.getTabLayoutWithExpandedCollapsingToolbarTextColor(); expandedTabIndicatorColor = mCustomThemeWrapper.getTabLayoutWithExpandedCollapsingToolbarTabIndicator(); expandedTabBackgroundColor = mCustomThemeWrapper.getTabLayoutWithExpandedCollapsingToolbarTabBackground(); collapsedTabTextColor = mCustomThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTextColor(); collapsedTabIndicatorColor = mCustomThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTabIndicator(); collapsedTabBackgroundColor = mCustomThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTabBackground(); binding.toolbarConstraintLayoutViewSubredditDetailActivity.setBackgroundColor(expandedTabBackgroundColor); binding.subredditNameTextViewViewSubredditDetailActivity.setTextColor(mCustomThemeWrapper.getSubreddit()); binding.subscribeSubredditChipViewSubredditDetailActivity.setTextColor(mCustomThemeWrapper.getChipTextColor()); int primaryTextColor = mCustomThemeWrapper.getPrimaryTextColor(); binding.subscriberCountTextViewViewSubredditDetailActivity.setTextColor(primaryTextColor); binding.sinceTextViewViewSubredditDetailActivity.setTextColor(primaryTextColor); binding.creationTimeTextViewViewSubredditDetailActivity.setTextColor(primaryTextColor); binding.descriptionTextViewViewSubredditDetailActivity.setTextColor(primaryTextColor); navigationWrapper.applyCustomTheme(mCustomThemeWrapper.getBottomAppBarIconColor(), mCustomThemeWrapper.getBottomAppBarBackgroundColor()); applyTabLayoutTheme(binding.tabLayoutViewSubredditDetailActivity); applyFABTheme(navigationWrapper.floatingActionButton); if (typeface != null) { binding.subredditNameTextViewViewSubredditDetailActivity.setTypeface(typeface); binding.subscribeSubredditChipViewSubredditDetailActivity.setTypeface(typeface); binding.subscriberCountTextViewViewSubredditDetailActivity.setTypeface(typeface); binding.sinceTextViewViewSubredditDetailActivity.setTypeface(typeface); binding.creationTimeTextViewViewSubredditDetailActivity.setTypeface(typeface); binding.descriptionTextViewViewSubredditDetailActivity.setTypeface(typeface); } unsubscribedColor = mCustomThemeWrapper.getUnsubscribed(); subscribedColor = mCustomThemeWrapper.getSubscribed(); } @OptIn(markerClass = ExperimentalBadgeUtils.class) private void checkNewAccountAndBindView() { if (mNewAccountName != null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT) || !accountName.equals(mNewAccountName)) { AccountManagement.switchAccount(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), mNewAccountName, newAccount -> { EventBus.getDefault().post(new SwitchAccountEvent(getClass().getName())); Toast.makeText(this, R.string.account_switched, Toast.LENGTH_SHORT).show(); mNewAccountName = null; if (newAccount != null) { accessToken = newAccount.getAccessToken(); accountName = newAccount.getAccountName(); } bindView(); }); } else { bindView(); } } else { bindView(); } } private void fetchSubredditData() { if (!mFetchSubredditInfoSuccess) { Handler handler = new Handler(); FetchSubredditData.fetchSubredditData(mExecutor, handler, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? null : mOauthRetrofit, mRetrofit, subredditName, accessToken, new FetchSubredditData.FetchSubredditDataListener() { @Override public void onFetchSubredditDataSuccess(SubredditData subredditData, int nCurrentOnlineSubscribers) { InsertSubredditData.insertSubredditData(mExecutor, handler, mRedditDataRoomDatabase, subredditData, () -> mFetchSubredditInfoSuccess = true); } @Override public void onFetchSubredditDataFail(boolean isQuarantined) { makeSnackbar(R.string.cannot_fetch_subreddit_info, true); mFetchSubredditInfoSuccess = false; } }); } } private void bottomAppBarOptionAction(int option) { switch (option) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME: { EventBus.getDefault().post(new GoBackToMainPageEvent()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX: { Intent intent = new Intent(this, InboxActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE: { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SHOW_MULTIREDDITS, true); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: { PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(false); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: { displaySortTypeBottomSheetFragment(); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, subredditName); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_UPVOTED); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_DOWNVOTED); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_HIDDEN); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED: { Intent intent = new Intent(ViewSubredditDetailActivity.this, AccountSavedThingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; } } } private int getBottomAppBarOptionDrawableResource(int option) { switch (option) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME: return R.drawable.ic_home_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: return R.drawable.ic_subscriptions_bottom_app_bar_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX: return R.drawable.ic_inbox_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE: return R.drawable.ic_account_circle_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: return R.drawable.ic_multi_reddit_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: return R.drawable.ic_add_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH: return R.drawable.ic_refresh_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: return R.drawable.ic_sort_toolbar_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: return R.drawable.ic_post_layout_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH: return R.drawable.ic_search_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: return R.drawable.ic_subreddit_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER: return R.drawable.ic_user_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: return R.drawable.ic_hide_read_posts_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: return R.drawable.ic_filter_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED: return R.drawable.ic_arrow_upward_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED: return R.drawable.ic_arrow_downward_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN: return R.drawable.ic_lock_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED: return R.drawable.ic_bookmarks_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: return R.drawable.ic_keyboard_double_arrow_up_day_night_24dp; } } @ExperimentalBadgeUtils private void bindView() { if (mMessageFullname != null) { ReadMessage.readMessage(mOauthRetrofit, accessToken, mMessageFullname, new ReadMessage.ReadMessageListener() { @Override public void readSuccess() { mMessageFullname = null; } @Override public void readFailed() { } }); } if (showBottomAppBar) { int optionCount = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_COUNT, 4); int option1 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_1, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME); int option2 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_2, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS); if (optionCount == 2) { navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2)); navigationWrapper.bindOptions(option1, option2); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option2BottomAppBar, option1); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option4BottomAppBar, option2); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } return false; }); } } else { int option3 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_3, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS : SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX); int option4 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_4, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH : SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE); navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2), getBottomAppBarOptionDrawableResource(option3), getBottomAppBarOptionDrawableResource(option4)); navigationWrapper.bindOptions(option1, option2, option3, option4); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option1BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); navigationWrapper.option3BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option3); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option4); }); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option1BottomAppBar, option1); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option2BottomAppBar, option2); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option3BottomAppBar, option3); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option4BottomAppBar, option4); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } else if (itemId == R.id.navigation_rail_option_3) { bottomAppBarOptionAction(option3); return true; } else if (itemId == R.id.navigation_rail_option_4) { bottomAppBarOptionAction(option4); return true; } return false; }); } } } else { CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); lp.setAnchorId(View.NO_ID); lp.gravity = Gravity.END | Gravity.BOTTOM; navigationWrapper.floatingActionButton.setLayoutParams(lp); } fabOption = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS); switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_refresh_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_sort_toolbar_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_post_layout_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_search_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_subreddit_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_user_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_hide_read_posts_day_night_24dp); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_keyboard_double_arrow_up_day_night_24dp); break; default: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_add_day_night_24dp); } break; } setOtherActivitiesFabContentDescription(navigationWrapper.floatingActionButton, fabOption); navigationWrapper.floatingActionButton.setOnClickListener(view -> { switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(false); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: { displaySortTypeBottomSheetFragment(); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, subredditName); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; default: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } }); navigationWrapper.floatingActionButton.setOnLongClickListener(view -> { FABMoreOptionsBottomSheetFragment fabMoreOptionsBottomSheetFragment = new FABMoreOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(FABMoreOptionsBottomSheetFragment.EXTRA_ANONYMOUS_MODE, accountName.equals(Account.ANONYMOUS_ACCOUNT)); fabMoreOptionsBottomSheetFragment.setArguments(bundle); fabMoreOptionsBottomSheetFragment.show(getSupportFragmentManager(), fabMoreOptionsBottomSheetFragment.getTag()); return true; }); navigationWrapper.floatingActionButton.setVisibility(hideFab ? View.GONE : View.VISIBLE); binding.subscribeSubredditChipViewSubredditDetailActivity.setOnClickListener(view -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (subscriptionReady) { subscriptionReady = false; if (getResources().getString(R.string.subscribe).contentEquals(binding.subscribeSubredditChipViewSubredditDetailActivity.getText())) { SubredditSubscription.anonymousSubscribeToSubreddit(mExecutor, new Handler(), mRetrofit, mRedditDataRoomDatabase, subredditName, new SubredditSubscription.SubredditSubscriptionListener() { @Override public void onSubredditSubscriptionSuccess() { binding.subscribeSubredditChipViewSubredditDetailActivity.setText(R.string.unsubscribe); binding.subscribeSubredditChipViewSubredditDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(subscribedColor)); makeSnackbar(R.string.subscribed, false); subscriptionReady = true; } @Override public void onSubredditSubscriptionFail() { makeSnackbar(R.string.subscribe_failed, false); subscriptionReady = true; } }); } else { SubredditSubscription.anonymousUnsubscribeToSubreddit(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, new SubredditSubscription.SubredditSubscriptionListener() { @Override public void onSubredditSubscriptionSuccess() { binding.subscribeSubredditChipViewSubredditDetailActivity.setText(R.string.subscribe); binding.subscribeSubredditChipViewSubredditDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(unsubscribedColor)); makeSnackbar(R.string.unsubscribed, false); subscriptionReady = true; } @Override public void onSubredditSubscriptionFail() { makeSnackbar(R.string.unsubscribe_failed, false); subscriptionReady = true; } }); } } } else { if (subscriptionReady) { subscriptionReady = false; if (getResources().getString(R.string.subscribe).contentEquals(binding.subscribeSubredditChipViewSubredditDetailActivity.getText())) { SubredditSubscription.subscribeToSubreddit(mExecutor, new Handler(), mOauthRetrofit, mRetrofit, accessToken, subredditName, accountName, mRedditDataRoomDatabase, new SubredditSubscription.SubredditSubscriptionListener() { @Override public void onSubredditSubscriptionSuccess() { binding.subscribeSubredditChipViewSubredditDetailActivity.setText(R.string.unsubscribe); binding.subscribeSubredditChipViewSubredditDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(subscribedColor)); makeSnackbar(R.string.subscribed, false); subscriptionReady = true; } @Override public void onSubredditSubscriptionFail() { makeSnackbar(R.string.subscribe_failed, false); subscriptionReady = true; } }); } else { SubredditSubscription.unsubscribeToSubreddit(mExecutor, new Handler(), mOauthRetrofit, accessToken, subredditName, accountName, mRedditDataRoomDatabase, new SubredditSubscription.SubredditSubscriptionListener() { @Override public void onSubredditSubscriptionSuccess() { binding.subscribeSubredditChipViewSubredditDetailActivity.setText(R.string.subscribe); binding.subscribeSubredditChipViewSubredditDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(unsubscribedColor)); makeSnackbar(R.string.unsubscribed, false); subscriptionReady = true; } @Override public void onSubredditSubscriptionFail() { makeSnackbar(R.string.unsubscribe_failed, false); subscriptionReady = true; } }); } } } }); CheckIsSubscribedToSubreddit.checkIsSubscribedToSubreddit(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditName, accountName, new CheckIsSubscribedToSubreddit.CheckIsSubscribedToSubredditListener() { @Override public void isSubscribed() { binding.subscribeSubredditChipViewSubredditDetailActivity.setText(R.string.unsubscribe); binding.subscribeSubredditChipViewSubredditDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(subscribedColor)); subscriptionReady = true; } @Override public void isNotSubscribed() { binding.subscribeSubredditChipViewSubredditDetailActivity.setText(R.string.subscribe); binding.subscribeSubredditChipViewSubredditDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(unsubscribedColor)); subscriptionReady = true; } }); binding.viewPagerViewSubredditDetailActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } if (showBottomAppBar) { navigationWrapper.showNavigation(); } if (!hideFab) { navigationWrapper.showFab(); } sectionsPagerAdapter.displaySortTypeInToolbar(); } }); binding.viewPagerViewSubredditDetailActivity.setAdapter(sectionsPagerAdapter); binding.viewPagerViewSubredditDetailActivity.setUserInputEnabled(!mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false)); new TabLayoutMediator(binding.tabLayoutViewSubredditDetailActivity, binding.viewPagerViewSubredditDetailActivity, (tab, position) -> { switch (position) { case 0: tab.setText(R.string.posts); break; case 1: tab.setText(R.string.about); } }).attach(); fixViewPager2Sensitivity(binding.viewPagerViewSubredditDetailActivity); // Add double-tap listener for tabs binding.tabLayoutViewSubredditDetailActivity.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { private long lastTabClickTime = 0; private int lastClickedTabPosition = -1; private static final long DOUBLE_TAP_TIME_DELTA = 300; // milliseconds @Override public void onTabSelected(TabLayout.Tab tab) { handleTabClick(tab); } @Override public void onTabUnselected(TabLayout.Tab tab) {} @Override public void onTabReselected(TabLayout.Tab tab) { handleTabClick(tab); } private void handleTabClick(TabLayout.Tab tab) { int position = tab.getPosition(); long currentTime = System.currentTimeMillis(); if (position == lastClickedTabPosition && currentTime - lastTabClickTime < DOUBLE_TAP_TIME_DELTA) { // Double tap detected on same tab scrollTabToTop(position); lastTabClickTime = 0; // Reset to prevent triple-tap lastClickedTabPosition = -1; } else { lastTabClickTime = currentTime; lastClickedTabPosition = position; } } }); boolean viewSidebar = getIntent().getBooleanExtra(EXTRA_VIEW_SIDEBAR, false); if (viewSidebar) { binding.viewPagerViewSubredditDetailActivity.setCurrentItem(1, false); } navigationWrapper.bottomAppBar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { navigationWrapper.bottomAppBar.getViewTreeObserver().removeOnGlobalLayoutListener(this); setInboxCount(mCurrentAccountSharedPreferences.getInt(SharedPreferencesUtils.INBOX_COUNT, 0)); } }); } private void displaySortTypeBottomSheetFragment() { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { SortTypeBottomSheetFragment sortTypeBottomSheetFragment = SortTypeBottomSheetFragment.getNewInstance(true, ((PostFragment) fragment).getSortType()); sortTypeBottomSheetFragment.show(fragmentManager, sortTypeBottomSheetFragment.getTag()); } } @ExperimentalBadgeUtils private void setInboxCount(int inboxCount) { mHandler.post(() -> navigationWrapper.setInboxCount(this, inboxCount)); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_subreddit_detail_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_sort_view_subreddit_detail_activity) { displaySortTypeBottomSheetFragment(); return true; } else if (itemId == R.id.action_search_view_subreddit_detail_activity) { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, subredditName); startActivity(intent); return true; } else if (itemId == R.id.action_refresh_view_subreddit_detail_activity) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(true); } return true; } else if (itemId == R.id.action_change_post_layout_view_subreddit_detail_activity) { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } else if (itemId == R.id.action_select_user_flair_view_subreddit_detail_activity) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(this, R.string.login_first, Toast.LENGTH_SHORT).show(); return true; } Intent selectUserFlairIntent = new Intent(this, SelectUserFlairActivity.class); selectUserFlairIntent.putExtra(SelectUserFlairActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(selectUserFlairIntent); return true; } else if (itemId == R.id.action_add_to_multireddit_view_subreddit_detail_activity) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(this, R.string.login_first, Toast.LENGTH_SHORT).show(); return true; } Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_MULTIREDDIT); requestMultiredditSelectionLauncher.launch(intent); } else if (itemId == R.id.action_add_to_post_filter_view_subreddit_detail_activity) { Intent intent = new Intent(this, PostFilterPreferenceActivity.class); intent.putExtra(PostFilterPreferenceActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(intent); return true; } else if (itemId == R.id.action_share_view_subreddit_detail_activity) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, "https://www.reddit.com/r/" + subredditName); if (shareIntent.resolveActivity(getPackageManager()) != null) { startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); } else { Toast.makeText(this, R.string.no_app, Toast.LENGTH_SHORT).show(); } return true; } else if (itemId == R.id.action_go_to_wiki_view_subreddit_detail_activity) { Intent wikiIntent = new Intent(this, WikiActivity.class); wikiIntent.putExtra(WikiActivity.EXTRA_SUBREDDIT_NAME, subredditName); wikiIntent.putExtra(WikiActivity.EXTRA_WIKI_PATH, "index"); startActivity(wikiIntent); return true; } else if (itemId == R.id.action_contact_mods_view_subreddit_detail_activity) { Intent intent = new Intent(this, SendPrivateMessageActivity.class); intent.putExtra(SendPrivateMessageActivity.EXTRA_RECIPIENT_USERNAME, "r/" + subredditName); startActivity(intent); return true; } else if (itemId == R.id.action_add_to_home_screen_view_subreddit_detail_activity) { Bitmap icon = subredditIconBitmap == null ? decodeResource(getResources(), R.drawable.subreddit_default_icon) : subredditIconBitmap; return ShortcutManager.requestPinShortcut(this, subredditName, icon); } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(FETCH_SUBREDDIT_INFO_STATE, mFetchSubredditInfoSuccess); outState.putString(MESSAGE_FULLNAME_STATE, mMessageFullname); outState.putString(NEW_ACCOUNT_NAME_STATE, mNewAccountName); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } public boolean isNsfwSubreddit() { return isNsfwSubreddit; } private void scrollTabToTop(int position) { // Get the fragment at the specified position and scroll to top if (sectionsPagerAdapter != null) { if(position == 0) { PostFragment fragment = (PostFragment) sectionsPagerAdapter.getFragmentAtPosition(position); if (fragment != null) { fragment.goBackToTop(); } }else if (position == 1) { SidebarFragment fragment = (SidebarFragment) sectionsPagerAdapter.getFragmentAtPosition(position); if (fragment != null) { fragment.goBackToTop(); } } } } private void makeSnackbar(int resId, boolean retry) { if (showToast) { Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); } else { if (retry) { Snackbar.make(binding.getRoot(), resId, Snackbar.LENGTH_SHORT).setAction(R.string.retry, view -> fetchSubredditData()).show(); } else { Snackbar.make(binding.getRoot(), resId, Snackbar.LENGTH_SHORT).show(); } } } @Override public void sortTypeSelected(SortType sortType) { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.changeSortType(sortType); } } @Override public void sortTypeSelected(String sortType) { SortTimeBottomSheetFragment sortTimeBottomSheetFragment = new SortTimeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(SortTimeBottomSheetFragment.EXTRA_SORT_TYPE, sortType); sortTimeBottomSheetFragment.setArguments(bundle); sortTimeBottomSheetFragment.show(getSupportFragmentManager(), sortTimeBottomSheetFragment.getTag()); } @Override public void postTypeSelected(int postType) { Intent intent; switch (postType) { case PostTypeBottomSheetFragment.TYPE_TEXT: intent = new Intent(this, PostTextActivity.class); intent.putExtra(PostTextActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_LINK: intent = new Intent(this, PostLinkActivity.class); intent.putExtra(PostLinkActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_IMAGE: intent = new Intent(this, PostImageActivity.class); intent.putExtra(PostImageActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_VIDEO: intent = new Intent(this, PostVideoActivity.class); intent.putExtra(PostVideoActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_GALLERY: intent = new Intent(this, PostGalleryActivity.class); intent.putExtra(PostGalleryActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_POLL: intent = new Intent(this, PostPollActivity.class); intent.putExtra(PostPollActivity.EXTRA_SUBREDDIT_NAME, subredditName); startActivity(intent); } } @Override public void postLayoutSelected(int postLayout) { mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_SUBREDDIT_POST_BASE + subredditName, postLayout).apply(); sectionsPagerAdapter.changePostLayout(postLayout); } @Override public void contentScrollUp() { if (showBottomAppBar && !lockBottomAppBar) { navigationWrapper.showNavigation(); } if (!(showBottomAppBar && lockBottomAppBar) && !hideFab) { navigationWrapper.showFab(); } } @Override public void contentScrollDown() { if (!(showBottomAppBar && lockBottomAppBar) && !hideFab) { navigationWrapper.hideFab(); } if (showBottomAppBar && !lockBottomAppBar) { navigationWrapper.hideNavigation(); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } @Subscribe public void onChangeNSFWEvent(ChangeNSFWEvent changeNSFWEvent) { sectionsPagerAdapter.changeNSFW(changeNSFWEvent.nsfw); } @Subscribe public void goBackToMainPageEvent(GoBackToMainPageEvent event) { finish(); } @ExperimentalBadgeUtils @Subscribe public void onChangeInboxCountEvent(ChangeInboxCountEvent event) { setInboxCount(event.inboxCount); } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void displaySortType() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.displaySortTypeInToolbar(); } } @Override public void fabOptionSelected(int option) { switch (option) { case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SUBMIT_POST: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_REFRESH: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(false); } break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_SORT_TYPE: displaySortTypeBottomSheetFragment(); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_POST_LAYOUT: PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SEARCH: Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, subredditName); startActivity(intent); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_SUBREDDIT: { goToSubreddit(); break; } case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_USER: { goToUser(); break; } case FABMoreOptionsBottomSheetFragment.FAB_HIDE_READ_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_FILTER_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_GO_TO_TOP: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; } } } private void goToSubreddit() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); RecyclerView recyclerView = rootView.findViewById(R.id.recycler_view_go_to_thing_edit_text); thingEditText.requestFocus(); SubredditAutocompleteRecyclerViewAdapter adapter = new SubredditAutocompleteRecyclerViewAdapter( this, mCustomThemeWrapper, subredditData -> { Utils.hideKeyboard(this); Intent intent = new Intent(ViewSubredditDetailActivity.this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditData.getName()); startActivity(intent); }); recyclerView.setAdapter(adapter); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); return true; } return false; }); Handler handler = new Handler(); boolean nsfw = mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); thingEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { if (subredditAutocompleteCall != null && subredditAutocompleteCall.isExecuted()) { subredditAutocompleteCall.cancel(); } if (autoCompleteRunnable != null) { handler.removeCallbacks(autoCompleteRunnable); } } @Override public void afterTextChanged(Editable editable) { String currentQuery = editable.toString().trim(); if (!currentQuery.isEmpty()) { autoCompleteRunnable = () -> { subredditAutocompleteCall = mOauthRetrofit.create(RedditAPI.class).subredditAutocomplete(APIUtils.getOAuthHeader(accessToken), currentQuery, nsfw); subredditAutocompleteCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { subredditAutocompleteCall = null; if (response.isSuccessful()) { ParseSubredditData.parseSubredditListingData(mExecutor, handler, response.body(), nsfw, new ParseSubredditData.ParseSubredditListingDataListener() { @Override public void onParseSubredditListingDataSuccess(ArrayList subredditData, String after) { adapter.setSubreddits(subredditData); } @Override public void onParseSubredditListingDataFail() { } }); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { subredditAutocompleteCall = null; } }); }; handler.postDelayed(autoCompleteRunnable, 500); } } }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_subreddit) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } private void goToUser() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); thingEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); return true; } return false; }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_user) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } @Override public void markPostAsRead(Post post) { int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, accountName, post.getId(), readPostsLimit); } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } private class SectionsPagerAdapter extends FragmentStateAdapter { SectionsPagerAdapter(FragmentActivity fa) { super(fa); } @NonNull @Override public Fragment createFragment(int position) { if (position == 0) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putString(PostFragment.EXTRA_NAME, subredditName); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_SUBREDDIT); fragment.setArguments(bundle); return fragment; } SidebarFragment fragment = new SidebarFragment(); Bundle bundle = new Bundle(); bundle.putString(SidebarFragment.EXTRA_SUBREDDIT_NAME, subredditName); fragment.setArguments(bundle); return fragment; } @Nullable private Fragment getCurrentFragment() { if (fragmentManager == null) { return null; } return fragmentManager.findFragmentByTag("f" + binding.viewPagerViewSubredditDetailActivity.getCurrentItem()); } @Nullable private Fragment getFragmentAtPosition(int position) { if (fragmentManager == null) { return null; } Fragment fragment = fragmentManager.findFragmentByTag("f" + position); if (fragment instanceof PostFragment || fragment instanceof SidebarFragment) { return fragment; } return null; } public boolean handleKeyDown(int keyCode) { if (binding.viewPagerViewSubredditDetailActivity.getCurrentItem() == 0) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { return ((PostFragment) fragment).handleKeyDown(keyCode); } } return false; } public void refresh(boolean refreshSubredditData) { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { ((PostFragment) fragment).refresh(); if (refreshSubredditData) { mFetchSubredditInfoSuccess = false; fetchSubredditData(); } } fragment = fragmentManager.findFragmentByTag("f1"); if (fragment instanceof SidebarFragment) { ((SidebarFragment) fragment).fetchSubredditData(); } } public void changeSortType(SortType sortType) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeSortType(sortType); Utils.displaySortTypeInToolbar(sortType, binding.toolbar); } } public void changeNSFW(boolean nsfw) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeNSFW(nsfw); } } void changePostLayout(int postLayout) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changePostLayout(postLayout); } } void goBackToTop() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).goBackToTop(); } else if (fragment instanceof SidebarFragment) { ((SidebarFragment) fragment).goBackToTop(); } } void displaySortTypeInToolbar() { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f" + binding.viewPagerViewSubredditDetailActivity.getCurrentItem()); if (fragment instanceof PostFragment) { SortType sortType = ((PostFragment) fragment).getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.toolbar); } } } void hideReadPosts() { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { ((PostFragment) fragment).hideReadPosts(); } } } void filterPosts() { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { ((PostFragment) fragment).filterPosts(); } } } @Override public int getItemCount() { return 2; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewUserDetailActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.badge.ExperimentalBadgeUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.textfield.TextInputEditText; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.SubredditAutocompleteRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.AccountManagement; import ml.docilealligator.infinityforreddit.asynctasks.AddSubredditOrUserToMultiReddit; import ml.docilealligator.infinityforreddit.asynctasks.CheckIsFollowingUser; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CopyTextBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FABMoreOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.KarmaInfoBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostLayoutBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SortTimeBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UserThingSortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.NavigationWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityViewUserDetailBinding; import ml.docilealligator.infinityforreddit.events.ChangeInboxCountEvent; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.GoBackToMainPageEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.fragments.CommentsListingFragment; import ml.docilealligator.infinityforreddit.fragments.PostFragment; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.message.ReadMessage; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import ml.docilealligator.infinityforreddit.subreddit.ParseSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.thing.DeleteThing; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.user.BlockUser; import ml.docilealligator.infinityforreddit.user.FetchUserData; import ml.docilealligator.infinityforreddit.user.UserData; import ml.docilealligator.infinityforreddit.user.UserFollowing; import ml.docilealligator.infinityforreddit.user.UserViewModel; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ViewUserDetailActivity extends BaseActivity implements SortTypeSelectionCallback, PostTypeBottomSheetFragment.PostTypeSelectionCallback, PostLayoutBottomSheetFragment.PostLayoutSelectionCallback, ActivityToolbarInterface, FABMoreOptionsBottomSheetFragment.FABOptionSelectionCallback, MarkPostAsReadInterface, RecyclerViewContentScrollingInterface { public static final String EXTRA_USER_NAME_KEY = "EUNK"; public static final String EXTRA_MESSAGE_FULLNAME = "ENF"; public static final String EXTRA_NEW_ACCOUNT_NAME = "ENAN"; public static final int EDIT_COMMENT_REQUEST_CODE = 300; private static final String FETCH_USER_INFO_STATE = "FSIS"; private static final String MESSAGE_FULLNAME_STATE = "MFS"; private static final String NEW_ACCOUNT_NAME_STATE = "NANS"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject @Named("bottom_app_bar") SharedPreferences mBottomAppBarSharedPreference; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; public UserViewModel userViewModel; private FragmentManager fragmentManager; private SectionsPagerAdapter sectionsPagerAdapter; private RequestManager glide; private NavigationWrapper navigationWrapper; private Runnable autoCompleteRunnable; private Call subredditAutocompleteCall; private String username; private String description; private boolean subscriptionReady = false; private boolean mFetchUserInfoSuccess = false; private int expandedTabTextColor; private int expandedTabBackgroundColor; private int expandedTabIndicatorColor; private int collapsedTabTextColor; private int collapsedTabBackgroundColor; private int collapsedTabIndicatorColor; private int unsubscribedColor; private int subscribedColor; private int fabOption; private int topSystemBarHeight; private boolean showToast = false; private boolean hideFab; private boolean showBottomAppBar; private boolean lockBottomAppBar; private String mMessageFullname; private String mNewAccountName; //private MaterialAlertDialogBuilder nsfwWarningBuilder; private ActivityViewUserDetailBinding binding; private ActivityResultLauncher requestMultiredditSelectionLauncher; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setTransparentStatusBarAfterToolbarCollapsed(); super.onCreate(savedInstanceState); binding = ActivityViewUserDetailBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); hideFab = mSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_FAB_IN_POST_FEED, false); showBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.BOTTOM_APP_BAR_KEY, false); navigationWrapper = new NavigationWrapper(findViewById(R.id.bottom_app_bar_bottom_app_bar), findViewById(R.id.linear_layout_bottom_app_bar), findViewById(R.id.option_1_bottom_app_bar), findViewById(R.id.option_2_bottom_app_bar), findViewById(R.id.option_3_bottom_app_bar), findViewById(R.id.option_4_bottom_app_bar), findViewById(R.id.fab_view_user_detail_activity), findViewById(R.id.navigation_rail), customThemeWrapper, showBottomAppBar); EventBus.getDefault().register(this); applyCustomTheme(); attachSliderPanelIfApplicable(); mViewPager2 = binding.viewPagerViewUserDetailActivity; username = getIntent().getStringExtra(EXTRA_USER_NAME_KEY); fragmentManager = getSupportFragmentManager(); lockBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_BOTTOM_APP_BAR, false); if (username.equalsIgnoreCase("me")) { username = accountName; } if (savedInstanceState == null) { mMessageFullname = getIntent().getStringExtra(EXTRA_MESSAGE_FULLNAME); mNewAccountName = getIntent().getStringExtra(EXTRA_NEW_ACCOUNT_NAME); } else { mFetchUserInfoSuccess = savedInstanceState.getBoolean(FETCH_USER_INFO_STATE); mMessageFullname = savedInstanceState.getString(MESSAGE_FULLNAME_STATE); mNewAccountName = savedInstanceState.getString(NEW_ACCOUNT_NAME_STATE); } sectionsPagerAdapter = new SectionsPagerAdapter(this); checkNewAccountAndInitializeViewPager(); fetchUserInfo(); Resources resources = getResources(); String title = "u/" + username; binding.userNameTextViewViewUserDetailActivity.setText(title); binding.toolbarViewUserDetailActivity.setTitle(title); setSupportActionBar(binding.toolbarViewUserDetailActivity); setToolbarGoToTop(binding.toolbarViewUserDetailActivity); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewGroupCompat.installCompatInsetsDispatch(binding.getRoot()); ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); topSystemBarHeight = allInsets.top; int padding16 = (int) Utils.convertDpToPixel(16, ViewUserDetailActivity.this); if (navigationWrapper.navigationRailView == null) { if (navigationWrapper.bottomAppBar.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, padding16 + allInsets.right, padding16 + allInsets.bottom); } else { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, allInsets.bottom); } } else { if (navigationWrapper.navigationRailView.getVisibility() != View.VISIBLE) { setMargins(navigationWrapper.floatingActionButton, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, padding16 + allInsets.right, padding16 + allInsets.bottom); binding.viewPagerViewUserDetailActivity.setPadding(allInsets.left, 0, allInsets.right, 0); } else { navigationWrapper.navigationRailView.setFitsSystemWindows(false); navigationWrapper.navigationRailView.setPadding(0, 0, 0, allInsets.bottom); setMargins(navigationWrapper.navigationRailView, allInsets.left, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN, BaseActivity.IGNORE_MARGIN ); binding.viewPagerViewUserDetailActivity.setPadding(0, 0, allInsets.right, 0); } } binding.toolbarConstraintLayoutViewUserDetailActivity.setPadding( padding16 + allInsets.left, binding.toolbarConstraintLayoutViewUserDetailActivity.getPaddingTop(), padding16 + allInsets.right, binding.toolbarConstraintLayoutViewUserDetailActivity.getPaddingBottom()); if (navigationWrapper.bottomAppBar != null) { navigationWrapper.linearLayoutBottomAppBar.setPadding( navigationWrapper.linearLayoutBottomAppBar.getPaddingLeft(), navigationWrapper.linearLayoutBottomAppBar.getPaddingTop(), navigationWrapper.linearLayoutBottomAppBar.getPaddingRight(), allInsets.bottom ); } setMargins(binding.toolbarViewUserDetailActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.tabLayoutViewUserDetailActivity.setPadding(allInsets.left, 0, allInsets.right, 0); return insets; } }); /*adjustToolbar(binding.toolbarViewUserDetailActivity); int navBarHeight = getNavBarHeight(); if (navBarHeight > 0) { if (navigationWrapper.navigationRailView == null) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); params.bottomMargin += navBarHeight; navigationWrapper.floatingActionButton.setLayoutParams(params); } }*/ showToast = true; } View decorView = window.getDecorView(); if (isChangeStatusBarIconColor()) { binding.appbarLayoutViewUserDetail.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, State state) { if (state == State.COLLAPSED) { decorView.setSystemUiVisibility(getSystemVisibilityToolbarCollapsed()); binding.tabLayoutViewUserDetailActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutViewUserDetailActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutViewUserDetailActivity.setBackgroundColor(collapsedTabBackgroundColor); } else if (state == State.EXPANDED) { decorView.setSystemUiVisibility(getSystemVisibilityToolbarExpanded()); binding.tabLayoutViewUserDetailActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutViewUserDetailActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutViewUserDetailActivity.setBackgroundColor(expandedTabBackgroundColor); } } }); } else { binding.appbarLayoutViewUserDetail.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, State state) { if (state == State.COLLAPSED) { binding.tabLayoutViewUserDetailActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutViewUserDetailActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutViewUserDetailActivity.setBackgroundColor(collapsedTabBackgroundColor); } else if (state == State.EXPANDED) { binding.tabLayoutViewUserDetailActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutViewUserDetailActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutViewUserDetailActivity.setBackgroundColor(expandedTabBackgroundColor); } } }); } } else { binding.appbarLayoutViewUserDetail.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, State state) { if (state == State.EXPANDED) { binding.tabLayoutViewUserDetailActivity.setTabTextColors(expandedTabTextColor, expandedTabTextColor); binding.tabLayoutViewUserDetailActivity.setSelectedTabIndicatorColor(expandedTabIndicatorColor); binding.tabLayoutViewUserDetailActivity.setBackgroundColor(expandedTabBackgroundColor); } else if (state == State.COLLAPSED) { binding.tabLayoutViewUserDetailActivity.setTabTextColors(collapsedTabTextColor, collapsedTabTextColor); binding.tabLayoutViewUserDetailActivity.setSelectedTabIndicatorColor(collapsedTabIndicatorColor); binding.tabLayoutViewUserDetailActivity.setBackgroundColor(collapsedTabBackgroundColor); } } }); } glide = Glide.with(this); Locale locale = getResources().getConfiguration().locale; MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(ViewUserDetailActivity.this, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(mCustomThemeWrapper.getLinkColor()); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(getSupportFragmentManager(), null); return true; }; Markwon markwon = MarkdownUtils.createDescriptionMarkwon(this, miscPlugin, onLinkLongClickListener); binding.descriptionTextViewViewUserDetailActivity.setOnLongClickListener(view -> { if (description != null && !description.equals("") && binding.descriptionTextViewViewUserDetailActivity.getSelectionStart() == -1 && binding.descriptionTextViewViewUserDetailActivity.getSelectionEnd() == -1) { CopyTextBottomSheetFragment.show(getSupportFragmentManager(), description, null); return true; } return false; }); userViewModel = new ViewModelProvider(this, new UserViewModel.Factory(mRedditDataRoomDatabase, username)) .get(UserViewModel.class); userViewModel.getUserLiveData().observe(this, userData -> { if (userData != null) { if (userData.getBanner().equals("")) { binding.bannerImageViewViewUserDetailActivity.setOnClickListener(null); } else { glide.load(userData.getBanner()).into(binding.bannerImageViewViewUserDetailActivity); binding.bannerImageViewViewUserDetailActivity.setOnClickListener(view -> { Intent intent = new Intent(this, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, userData.getBanner()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, username + "-banner.jpg"); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, username); startActivity(intent); }); } if (userData.getIconUrl().equals("")) { glide.load(getDrawable(R.drawable.subreddit_default_icon)) .transform(new RoundedCornersTransformation(216, 0)) .into(binding.iconGifImageViewViewUserDetailActivity); binding.iconGifImageViewViewUserDetailActivity.setOnClickListener(null); } else { boolean disableAnimation = mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_PROFILE_AVATAR_ANIMATION, false); if (disableAnimation) { // Use asBitmap() to load only the first frame and prevent animation glide.asBitmap() .load(userData.getIconUrl()) .transform(new RoundedCornersTransformation(216, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(216, 0))) .into(binding.iconGifImageViewViewUserDetailActivity); } else { glide.load(userData.getIconUrl()) .transform(new RoundedCornersTransformation(216, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(216, 0))) .into(binding.iconGifImageViewViewUserDetailActivity); } binding.iconGifImageViewViewUserDetailActivity.setOnClickListener(view -> { Intent intent = new Intent(this, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, userData.getIconUrl()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, username + "-icon.jpg"); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, username); startActivity(intent); }); } if (userData.isCanBeFollowed()) { binding.subscribeUserChipViewUserDetailActivity.setVisibility(View.VISIBLE); binding.subscribeUserChipViewUserDetailActivity.setOnClickListener(view -> { if (subscriptionReady) { subscriptionReady = false; if (resources.getString(R.string.follow).contentEquals(binding.subscribeUserChipViewUserDetailActivity.getText())) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { UserFollowing.anonymousFollowUser(mExecutor, new Handler(), mRetrofit, username, mRedditDataRoomDatabase, new UserFollowing.UserFollowingListener() { @Override public void onUserFollowingSuccess() { binding.subscribeUserChipViewUserDetailActivity.setText(R.string.unfollow); binding.subscribeUserChipViewUserDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(subscribedColor)); showMessage(R.string.followed, false); subscriptionReady = true; } @Override public void onUserFollowingFail() { showMessage(R.string.follow_failed, false); subscriptionReady = true; } }); } else { UserFollowing.followUser(mExecutor, mHandler, mOauthRetrofit, mRetrofit, accessToken, username, accountName, mRedditDataRoomDatabase, new UserFollowing.UserFollowingListener() { @Override public void onUserFollowingSuccess() { binding.subscribeUserChipViewUserDetailActivity.setText(R.string.unfollow); binding.subscribeUserChipViewUserDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(subscribedColor)); showMessage(R.string.followed, false); subscriptionReady = true; } @Override public void onUserFollowingFail() { showMessage(R.string.follow_failed, false); subscriptionReady = true; } }); } } else { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { UserFollowing.anonymousUnfollowUser(mExecutor, new Handler(), username, mRedditDataRoomDatabase, new UserFollowing.UserFollowingListener() { @Override public void onUserFollowingSuccess() { binding.subscribeUserChipViewUserDetailActivity.setText(R.string.follow); binding.subscribeUserChipViewUserDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(unsubscribedColor)); showMessage(R.string.unfollowed, false); subscriptionReady = true; } @Override public void onUserFollowingFail() { //Will not be called } }); } else { UserFollowing.unfollowUser(mExecutor, mHandler, mOauthRetrofit, mRetrofit, accessToken, username, accountName, mRedditDataRoomDatabase, new UserFollowing.UserFollowingListener() { @Override public void onUserFollowingSuccess() { binding.subscribeUserChipViewUserDetailActivity.setText(R.string.follow); binding.subscribeUserChipViewUserDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(unsubscribedColor)); showMessage(R.string.unfollowed, false); subscriptionReady = true; } @Override public void onUserFollowingFail() { showMessage(R.string.unfollow_failed, false); subscriptionReady = true; } }); } } } }); CheckIsFollowingUser.checkIsFollowingUser(mExecutor, new Handler(), mRedditDataRoomDatabase, username, accountName, new CheckIsFollowingUser.CheckIsFollowingUserListener() { @Override public void isSubscribed() { binding.subscribeUserChipViewUserDetailActivity.setText(R.string.unfollow); binding.subscribeUserChipViewUserDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(subscribedColor)); subscriptionReady = true; } @Override public void isNotSubscribed() { binding.subscribeUserChipViewUserDetailActivity.setText(R.string.follow); binding.subscribeUserChipViewUserDetailActivity.setChipBackgroundColor(ColorStateList.valueOf(unsubscribedColor)); subscriptionReady = true; } }); } else { binding.subscribeUserChipViewUserDetailActivity.setVisibility(View.GONE); } String userFullName = "u/" + userData.getName(); binding.userNameTextViewViewUserDetailActivity.setText(userFullName); if (!title.equals(userFullName)) { getSupportActionBar().setTitle(userFullName); } String karma = getString(R.string.karma_info_user_detail, userData.getTotalKarma(), userData.getLinkKarma(), userData.getCommentKarma()); binding.karmaTextViewViewUserDetailActivity.setText(karma); binding.cakedayTextViewViewUserDetailActivity.setText(getString(R.string.cakeday_info, new SimpleDateFormat("MMM d, yyyy", locale).format(userData.getCakeday()))); if (userData.getDescription() == null || userData.getDescription().equals("")) { binding.descriptionTextViewViewUserDetailActivity.setVisibility(View.GONE); } else { binding.descriptionTextViewViewUserDetailActivity.setVisibility(View.VISIBLE); description = userData.getDescription(); markwon.setMarkdown(binding.descriptionTextViewViewUserDetailActivity, description); } /*if (userData.isNSFW()) { if (nsfwWarningBuilder == null && !mNsfwAndSpoilerSharedPreferences.getBoolean((mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mAccountName) + SharedPreferencesUtils.NSFW_BASE, false)) { nsfwWarningBuilder = new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.warning) .setMessage(R.string.this_user_has_nsfw_content) .setPositiveButton(R.string.leave, (dialogInterface, i) -> { finish(); }) .setNegativeButton(R.string.dismiss, null); nsfwWarningBuilder.show(); } }*/ } }); binding.karmaTextViewViewUserDetailActivity.setOnClickListener(view -> { UserData userData = userViewModel.getUserLiveData().getValue(); if (userData != null) { KarmaInfoBottomSheetFragment karmaInfoBottomSheetFragment = KarmaInfoBottomSheetFragment.newInstance( userData.getLinkKarma(), userData.getCommentKarma(), userData.getAwarderKarma(), userData.getAwardeeKarma() ); karmaInfoBottomSheetFragment.show(getSupportFragmentManager(), karmaInfoBottomSheetFragment.getTag()); } }); requestMultiredditSelectionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { Intent data = result.getData(); if (data != null) { MultiReddit multiReddit = data.getParcelableExtra(SelectThingReturnKey.RETRUN_EXTRA_MULTIREDDIT); if (multiReddit != null) { AddSubredditOrUserToMultiReddit.addSubredditOrUserToMultiReddit(mOauthRetrofit, accessToken, multiReddit.getPath(), "u_" + username, new AddSubredditOrUserToMultiReddit.AddSubredditOrUserToMultiRedditListener() { @Override public void success() { Toast.makeText(ViewUserDetailActivity.this, getString(R.string.add_subreddit_or_user_to_multireddit_success, username, multiReddit.getDisplayName()), Toast.LENGTH_LONG).show(); } @Override public void failed(int code) { Toast.makeText(ViewUserDetailActivity.this, getString(R.string.add_subreddit_or_user_to_multireddit_failed, username, multiReddit.getDisplayName()), Toast.LENGTH_LONG).show(); } }); } } }); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (sectionsPagerAdapter != null) { return sectionsPagerAdapter.handleKeyDown(keyCode) || super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); binding.appbarLayoutViewUserDetail.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { binding.appbarLayoutViewUserDetail.getViewTreeObserver().removeOnGlobalLayoutListener(this); binding.collapsingToolbarLayoutViewUserDetailActivity.setScrimVisibleHeightTrigger(binding.toolbarViewUserDetailActivity.getHeight() + binding.tabLayoutViewUserDetailActivity.getHeight() + topSystemBarHeight * 2); } }); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutViewUserDetail, binding.collapsingToolbarLayoutViewUserDetailActivity, binding.toolbarViewUserDetailActivity, false); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutViewUserDetailActivity, binding.tabLayoutViewUserDetailActivity); expandedTabTextColor = mCustomThemeWrapper.getTabLayoutWithExpandedCollapsingToolbarTextColor(); expandedTabIndicatorColor = mCustomThemeWrapper.getTabLayoutWithExpandedCollapsingToolbarTabIndicator(); expandedTabBackgroundColor = mCustomThemeWrapper.getTabLayoutWithExpandedCollapsingToolbarTabBackground(); collapsedTabTextColor = mCustomThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTextColor(); collapsedTabIndicatorColor = mCustomThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTabIndicator(); collapsedTabBackgroundColor = mCustomThemeWrapper.getTabLayoutWithCollapsedCollapsingToolbarTabBackground(); binding.toolbarConstraintLayoutViewUserDetailActivity.setBackgroundColor(expandedTabBackgroundColor); unsubscribedColor = mCustomThemeWrapper.getUnsubscribed(); subscribedColor = mCustomThemeWrapper.getSubscribed(); binding.userNameTextViewViewUserDetailActivity.setTextColor(mCustomThemeWrapper.getUsername()); binding.karmaTextViewViewUserDetailActivity.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); binding.cakedayTextViewViewUserDetailActivity.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); navigationWrapper.applyCustomTheme(mCustomThemeWrapper.getBottomAppBarIconColor(), mCustomThemeWrapper.getBottomAppBarBackgroundColor()); applyFABTheme(navigationWrapper.floatingActionButton); binding.descriptionTextViewViewUserDetailActivity.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); binding.subscribeUserChipViewUserDetailActivity.setTextColor(mCustomThemeWrapper.getChipTextColor()); applyTabLayoutTheme(binding.tabLayoutViewUserDetailActivity); if (typeface != null) { binding.userNameTextViewViewUserDetailActivity.setTypeface(typeface); binding.karmaTextViewViewUserDetailActivity.setTypeface(typeface); binding.cakedayTextViewViewUserDetailActivity.setTypeface(typeface); binding.subscribeUserChipViewUserDetailActivity.setTypeface(typeface); binding.descriptionTextViewViewUserDetailActivity.setTypeface(typeface); } } @OptIn(markerClass = ExperimentalBadgeUtils.class) private void checkNewAccountAndInitializeViewPager() { if (mNewAccountName != null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT) || !accountName.equals(mNewAccountName)) { AccountManagement.switchAccount(mRedditDataRoomDatabase, mCurrentAccountSharedPreferences, mExecutor, new Handler(), mNewAccountName, newAccount -> { EventBus.getDefault().post(new SwitchAccountEvent(getClass().getName())); Toast.makeText(this, R.string.account_switched, Toast.LENGTH_SHORT).show(); mNewAccountName = null; if (newAccount != null) { accessToken = newAccount.getAccessToken(); accountName = newAccount.getAccountName(); } initializeViewPager(); }); } else { initializeViewPager(); } } else { initializeViewPager(); } } @ExperimentalBadgeUtils private void initializeViewPager() { binding.viewPagerViewUserDetailActivity.setAdapter(sectionsPagerAdapter); binding.viewPagerViewUserDetailActivity.setUserInputEnabled(!mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false)); new TabLayoutMediator(binding.tabLayoutViewUserDetailActivity, binding.viewPagerViewUserDetailActivity, (tab, position) -> { switch (position) { case 0: tab.setText(R.string.posts); break; case 1: tab.setText(R.string.comments); break; } }).attach(); binding.viewPagerViewUserDetailActivity.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (position == 0) { unlockSwipeRightToGoBack(); } else { lockSwipeRightToGoBack(); } if (showBottomAppBar) { navigationWrapper.showNavigation(); } if (!hideFab) { navigationWrapper.showFab(); } sectionsPagerAdapter.displaySortTypeInToolbar(); } }); fixViewPager2Sensitivity(binding.viewPagerViewUserDetailActivity); if (mMessageFullname != null) { ReadMessage.readMessage(mOauthRetrofit, accessToken, mMessageFullname, new ReadMessage.ReadMessageListener() { @Override public void readSuccess() { mMessageFullname = null; } @Override public void readFailed() { } }); } navigationWrapper.floatingActionButton.setVisibility(hideFab ? View.GONE : View.VISIBLE); if (showBottomAppBar) { int optionCount = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_COUNT, 4); int option1 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_1, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME); int option2 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_2, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS); if (optionCount == 2) { navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2)); navigationWrapper.bindOptions(option1, option2); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option2BottomAppBar, option1); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option4BottomAppBar, option2); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } return false; }); } } else { int option3 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_3, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS : SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX); int option4 = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_4, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH : SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE); navigationWrapper.bindOptionDrawableResource(getBottomAppBarOptionDrawableResource(option1), getBottomAppBarOptionDrawableResource(option2), getBottomAppBarOptionDrawableResource(option3), getBottomAppBarOptionDrawableResource(option4)); navigationWrapper.bindOptions(option1, option2, option3, option4); if (navigationWrapper.navigationRailView == null) { navigationWrapper.option1BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option1); }); navigationWrapper.option2BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option2); }); navigationWrapper.option3BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option3); }); navigationWrapper.option4BottomAppBar.setOnClickListener(view -> { bottomAppBarOptionAction(option4); }); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option1BottomAppBar, option1); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option2BottomAppBar, option2); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option3BottomAppBar, option3); navigationWrapper.setOtherActivitiesContentDescription(this, navigationWrapper.option4BottomAppBar, option4); } else { navigationWrapper.navigationRailView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.navigation_rail_option_1) { bottomAppBarOptionAction(option1); return true; } else if (itemId == R.id.navigation_rail_option_2) { bottomAppBarOptionAction(option2); return true; } else if (itemId == R.id.navigation_rail_option_3) { bottomAppBarOptionAction(option3); return true; } else if (itemId == R.id.navigation_rail_option_4) { bottomAppBarOptionAction(option4); return true; } return false; }); } } } else { CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) navigationWrapper.floatingActionButton.getLayoutParams(); lp.setAnchorId(View.NO_ID); lp.gravity = Gravity.END | Gravity.BOTTOM; navigationWrapper.floatingActionButton.setLayoutParams(lp); } fabOption = mBottomAppBarSharedPreference.getInt((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB, SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS); switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_refresh_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_sort_toolbar_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_post_layout_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_search_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_subreddit_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_user_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_hide_read_posts_day_night_24dp); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_keyboard_double_arrow_up_day_night_24dp); break; default: if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_filter_day_night_24dp); fabOption = SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS; } else { navigationWrapper.floatingActionButton.setImageResource(R.drawable.ic_add_day_night_24dp); } break; } setOtherActivitiesFabContentDescription(navigationWrapper.floatingActionButton, fabOption); navigationWrapper.floatingActionButton.setOnClickListener(view -> { switch (fabOption) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE: { break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT: { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, username); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, SelectThingReturnKey.THING_TYPE.USER); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; default: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } }); navigationWrapper.floatingActionButton.setOnLongClickListener(view -> { FABMoreOptionsBottomSheetFragment fabMoreOptionsBottomSheetFragment = new FABMoreOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(FABMoreOptionsBottomSheetFragment.EXTRA_ANONYMOUS_MODE, accountName.equals(Account.ANONYMOUS_ACCOUNT)); fabMoreOptionsBottomSheetFragment.setArguments(bundle); fabMoreOptionsBottomSheetFragment.show(getSupportFragmentManager(), fabMoreOptionsBottomSheetFragment.getTag()); return true; }); navigationWrapper.bottomAppBar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { navigationWrapper.bottomAppBar.getViewTreeObserver().removeOnGlobalLayoutListener(this); setInboxCount(mCurrentAccountSharedPreferences.getInt(SharedPreferencesUtils.INBOX_COUNT, 0)); } }); } private void bottomAppBarOptionAction(int option) { switch (option) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME: { EventBus.getDefault().post(new GoBackToMainPageEvent()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX: { Intent intent = new Intent(this, InboxActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE: { Intent intent = new Intent(this, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, accountName); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: { Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_SHOW_MULTIREDDITS, true); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: { PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: { displaySortTypeBottomSheetFragment(); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH: { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, username); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, SelectThingReturnKey.THING_TYPE.USER); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: goToSubreddit(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER: goToUser(); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_UPVOTED); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_DOWNVOTED); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN: { Intent intent = new Intent(this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_HIDDEN); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED: { Intent intent = new Intent(ViewUserDetailActivity.this, AccountSavedThingActivity.class); startActivity(intent); break; } case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; } } } private int getBottomAppBarOptionDrawableResource(int option) { switch (option) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME: return R.drawable.ic_home_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: return R.drawable.ic_subscriptions_bottom_app_bar_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX: return R.drawable.ic_inbox_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE: return R.drawable.ic_account_circle_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: return R.drawable.ic_multi_reddit_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: return R.drawable.ic_add_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH: return R.drawable.ic_refresh_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: return R.drawable.ic_sort_toolbar_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: return R.drawable.ic_post_layout_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH: return R.drawable.ic_search_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: return R.drawable.ic_subreddit_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER: return R.drawable.ic_user_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: return R.drawable.ic_hide_read_posts_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: return R.drawable.ic_filter_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED: return R.drawable.ic_arrow_upward_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED: return R.drawable.ic_arrow_downward_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN: return R.drawable.ic_lock_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED: return R.drawable.ic_bookmarks_day_night_24dp; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: return R.drawable.ic_keyboard_double_arrow_up_day_night_24dp; } } private void displaySortTypeBottomSheetFragment() { Fragment fragment = sectionsPagerAdapter.getCurrentFragment(); if (fragment instanceof PostFragment) { UserThingSortTypeBottomSheetFragment userThingSortTypeBottomSheetFragment = UserThingSortTypeBottomSheetFragment.getNewInstance(((PostFragment) fragment).getSortType()); userThingSortTypeBottomSheetFragment.show(getSupportFragmentManager(), userThingSortTypeBottomSheetFragment.getTag()); } else if (fragment instanceof CommentsListingFragment) { UserThingSortTypeBottomSheetFragment userThingSortTypeBottomSheetFragment = UserThingSortTypeBottomSheetFragment.getNewInstance(((CommentsListingFragment) fragment).getSortType()); userThingSortTypeBottomSheetFragment.show(getSupportFragmentManager(), userThingSortTypeBottomSheetFragment.getTag()); } } private void fetchUserInfo() { if (!mFetchUserInfoSuccess) { FetchUserData.fetchUserData(mExecutor, mHandler, null, mOauthRetrofit, mRetrofit, accessToken, username, new FetchUserData.FetchUserDataListener() { @Override public void onFetchUserDataSuccess(UserData userData, int inboxCount) { mExecutor.execute(() -> { mRedditDataRoomDatabase.userDao().insert(userData); mHandler.post(() -> { mFetchUserInfoSuccess = true; }); }); } @Override public void onFetchUserDataFailed() { showMessage(R.string.cannot_fetch_user_info, true); mFetchUserInfoSuccess = false; } }); } } public void deleteComment(String fullName) { new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.delete_this_comment) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.delete, (dialogInterface, i) -> DeleteThing.delete(mOauthRetrofit, fullName, accessToken, new DeleteThing.DeleteThingListener() { @Override public void deleteSuccess() { Toast.makeText(ViewUserDetailActivity.this, R.string.delete_post_success, Toast.LENGTH_SHORT).show(); } @Override public void deleteFailed() { Toast.makeText(ViewUserDetailActivity.this, R.string.delete_post_failed, Toast.LENGTH_SHORT).show(); } })) .setNegativeButton(R.string.cancel, null) .show(); } public void toggleReplyNotifications(Comment comment, int position) { sectionsPagerAdapter.toggleCommentReplyNotification(comment, position); } @ExperimentalBadgeUtils private void setInboxCount(int inboxCount) { mHandler.post(() -> navigationWrapper.setInboxCount(this, inboxCount)); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_user_detail_activity, menu); if (username.equals(accountName)) { menu.findItem(R.id.action_send_private_message_view_user_detail_activity).setVisible(false); menu.findItem(R.id.action_report_view_user_detail_activity).setVisible(false); menu.findItem(R.id.action_block_user_view_user_detail_activity).setVisible(false); } else { menu.findItem(R.id.action_edit_profile_view_user_detail_activity).setVisible(false); } applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_sort_view_user_detail_activity) { displaySortTypeBottomSheetFragment(); return true; } else if (itemId == R.id.action_search_view_user_detail_activity) { Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, username); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, SelectThingReturnKey.THING_TYPE.USER); startActivity(intent); return true; } else if (itemId == R.id.action_refresh_view_user_detail_activity) { sectionsPagerAdapter.refresh(); mFetchUserInfoSuccess = false; fetchUserInfo(); return true; } else if (itemId == R.id.action_change_post_layout_view_user_detail_activity) { PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); return true; } else if (itemId == R.id.action_share_view_user_detail_activity) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, "https://www.reddit.com/user/" + username); if (shareIntent.resolveActivity(getPackageManager()) != null) { startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); } else { Toast.makeText(this, R.string.no_app, Toast.LENGTH_SHORT).show(); } return true; } else if (itemId == R.id.action_send_private_message_view_user_detail_activity) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(this, R.string.login_first, Toast.LENGTH_SHORT).show(); return true; } Intent pmIntent = new Intent(this, SendPrivateMessageActivity.class); pmIntent.putExtra(SendPrivateMessageActivity.EXTRA_RECIPIENT_USERNAME, username); startActivity(pmIntent); return true; } else if (itemId == R.id.action_add_to_multireddit_view_user_detail_activity) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(this, R.string.login_first, Toast.LENGTH_SHORT).show(); return true; } Intent intent = new Intent(this, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_MULTIREDDIT); requestMultiredditSelectionLauncher.launch(intent); } else if (itemId == R.id.action_add_to_post_filter_view_user_detail_activity) { Intent intent = new Intent(this, PostFilterPreferenceActivity.class); intent.putExtra(PostFilterPreferenceActivity.EXTRA_USER_NAME, username); startActivity(intent); return true; } else if (itemId == R.id.action_report_view_user_detail_activity) { Intent reportIntent = new Intent(this, LinkResolverActivity.class); reportIntent.setData(Uri.parse("https://www.reddithelp.com/en/categories/rules-reporting/account-and-community-restrictions/what-should-i-do-if-i-see-something-i")); startActivity(reportIntent); return true; } else if (itemId == R.id.action_block_user_view_user_detail_activity) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(this, R.string.login_first, Toast.LENGTH_SHORT).show(); return true; } new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.block_user) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> BlockUser.blockUser(mOauthRetrofit, accessToken, username, new BlockUser.BlockUserListener() { @Override public void success() { Toast.makeText(ViewUserDetailActivity.this, R.string.block_user_success, Toast.LENGTH_SHORT).show(); } @Override public void failed() { Toast.makeText(ViewUserDetailActivity.this, R.string.block_user_failed, Toast.LENGTH_SHORT).show(); } })) .setNegativeButton(R.string.no, null) .show(); return true; } else if (itemId == R.id.action_edit_profile_view_user_detail_activity) { startActivity(new Intent(this, EditProfileActivity.class)); return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == EDIT_COMMENT_REQUEST_CODE) { if (data != null) { if (sectionsPagerAdapter != null) { if (data.hasExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT)) { sectionsPagerAdapter.editComment( (Comment) data.getParcelableExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT), data.getIntExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT_POSITION, -1)); } else { sectionsPagerAdapter.editComment( data.getStringExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT_CONTENT), data.getIntExtra(EditCommentActivity.RETURN_EXTRA_EDITED_COMMENT_POSITION, -1)); } } } } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(FETCH_USER_INFO_STATE, mFetchUserInfoSuccess); outState.putString(MESSAGE_FULLNAME_STATE, mMessageFullname); outState.putString(NEW_ACCOUNT_NAME_STATE, mNewAccountName); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } private void showMessage(int resId, boolean retry) { if (showToast) { Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); } else { if (retry) { Snackbar.make(binding.getRoot(), resId, Snackbar.LENGTH_SHORT).setAction(R.string.retry, view -> fetchUserInfo()).show(); } else { Snackbar.make(binding.getRoot(), resId, Snackbar.LENGTH_SHORT).show(); } } } @Override public void sortTypeSelected(SortType sortType) { sectionsPagerAdapter.changeSortType(sortType); } @Override public void sortTypeSelected(String sortType) { SortTimeBottomSheetFragment sortTimeBottomSheetFragment = new SortTimeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(SortTimeBottomSheetFragment.EXTRA_SORT_TYPE, sortType); sortTimeBottomSheetFragment.setArguments(bundle); sortTimeBottomSheetFragment.show(getSupportFragmentManager(), sortTimeBottomSheetFragment.getTag()); } @Override public void postLayoutSelected(int postLayout) { sectionsPagerAdapter.changePostLayout(postLayout); } @Override public void fabOptionSelected(int option) { switch (option) { case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SUBMIT_POST: PostTypeBottomSheetFragment postTypeBottomSheetFragment = new PostTypeBottomSheetFragment(); postTypeBottomSheetFragment.show(getSupportFragmentManager(), postTypeBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_REFRESH: if (sectionsPagerAdapter != null) { sectionsPagerAdapter.refresh(); } break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_SORT_TYPE: displaySortTypeBottomSheetFragment(); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_CHANGE_POST_LAYOUT: PostLayoutBottomSheetFragment postLayoutBottomSheetFragment = new PostLayoutBottomSheetFragment(); postLayoutBottomSheetFragment.show(getSupportFragmentManager(), postLayoutBottomSheetFragment.getTag()); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_SEARCH: Intent intent = new Intent(this, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_SUBREDDIT_OR_USER_NAME, username); intent.putExtra(SearchActivity.EXTRA_SEARCH_IN_THING_TYPE, SelectThingReturnKey.THING_TYPE.USER); startActivity(intent); break; case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_SUBREDDIT: { goToSubreddit(); break; } case FABMoreOptionsBottomSheetFragment.FAB_OPTION_GO_TO_USER: { goToUser(); break; } case FABMoreOptionsBottomSheetFragment.FAB_HIDE_READ_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.hideReadPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_FILTER_POSTS: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.filterPosts(); } break; } case FABMoreOptionsBottomSheetFragment.FAB_GO_TO_TOP: { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } break; } } } private void goToSubreddit() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); RecyclerView recyclerView = rootView.findViewById(R.id.recycler_view_go_to_thing_edit_text); thingEditText.requestFocus(); SubredditAutocompleteRecyclerViewAdapter adapter = new SubredditAutocompleteRecyclerViewAdapter( this, mCustomThemeWrapper, subredditData -> { Utils.hideKeyboard(this); Intent intent = new Intent(ViewUserDetailActivity.this, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditData.getName()); startActivity(intent); }); recyclerView.setAdapter(adapter); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); return true; } return false; }); Handler handler = new Handler(); boolean nsfw = mNsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); thingEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { if (subredditAutocompleteCall != null && subredditAutocompleteCall.isExecuted()) { subredditAutocompleteCall.cancel(); } if (autoCompleteRunnable != null) { handler.removeCallbacks(autoCompleteRunnable); } } @Override public void afterTextChanged(Editable editable) { String currentQuery = editable.toString().trim(); if (!currentQuery.isEmpty()) { autoCompleteRunnable = () -> { subredditAutocompleteCall = mOauthRetrofit.create(RedditAPI.class).subredditAutocomplete(APIUtils.getOAuthHeader(accessToken), currentQuery, nsfw); subredditAutocompleteCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { subredditAutocompleteCall = null; if (response.isSuccessful()) { ParseSubredditData.parseSubredditListingData(mExecutor, handler, response.body(), nsfw, new ParseSubredditData.ParseSubredditListingDataListener() { @Override public void onParseSubredditListingDataSuccess(ArrayList subredditData, String after) { adapter.setSubreddits(subredditData); } @Override public void onParseSubredditListingDataFail() { } }); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { subredditAutocompleteCall = null; } }); }; handler.postDelayed(autoCompleteRunnable, 500); } } }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_subreddit) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent subredditIntent = new Intent(this, ViewSubredditDetailActivity.class); subredditIntent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, thingEditText.getText().toString()); startActivity(subredditIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } private void goToUser() { View rootView = getLayoutInflater().inflate(R.layout.dialog_go_to_thing_edit_text, binding.getRoot(), false); TextInputEditText thingEditText = rootView.findViewById(R.id.text_input_edit_text_go_to_thing_edit_text); thingEditText.requestFocus(); Utils.showKeyboard(this, new Handler(), thingEditText); thingEditText.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE) { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); return true; } return false; }); new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme) .setTitle(R.string.go_to_user) .setView(rootView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(this); Intent userIntent = new Intent(this, ViewUserDetailActivity.class); userIntent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, thingEditText.getText().toString()); startActivity(userIntent); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(this); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(this); }) .show(); } @Override public void contentScrollUp() { if (showBottomAppBar && !lockBottomAppBar) { navigationWrapper.showNavigation(); } if (!(showBottomAppBar && lockBottomAppBar) && !hideFab) { navigationWrapper.showFab(); } } @Override public void contentScrollDown() { if (!(showBottomAppBar && lockBottomAppBar) && !hideFab) { navigationWrapper.hideFab(); } if (showBottomAppBar && !lockBottomAppBar) { navigationWrapper.hideNavigation(); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } @Subscribe public void onChangeNSFWEvent(ChangeNSFWEvent changeNSFWEvent) { sectionsPagerAdapter.changeNSFW(changeNSFWEvent.nsfw); } @Subscribe public void goBackToMainPageEvent(GoBackToMainPageEvent event) { finish(); } @ExperimentalBadgeUtils @Subscribe public void onChangeInboxCountEvent(ChangeInboxCountEvent event) { setInboxCount(event.inboxCount); } @Override public void onLongPress() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.goBackToTop(); } } @Override public void displaySortType() { if (sectionsPagerAdapter != null) { sectionsPagerAdapter.displaySortTypeInToolbar(); } } @Override public void markPostAsRead(Post post) { int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, accountName, post.getId(), readPostsLimit); } @Override public void postTypeSelected(int postType) { Intent intent; switch (postType) { case PostTypeBottomSheetFragment.TYPE_TEXT: intent = new Intent(this, PostTextActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_LINK: intent = new Intent(this, PostLinkActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_IMAGE: intent = new Intent(this, PostImageActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_VIDEO: intent = new Intent(this, PostVideoActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_GALLERY: intent = new Intent(this, PostGalleryActivity.class); startActivity(intent); break; case PostTypeBottomSheetFragment.TYPE_POLL: intent = new Intent(this, PostPollActivity.class); startActivity(intent); } } private class SectionsPagerAdapter extends FragmentStateAdapter { SectionsPagerAdapter(FragmentActivity fa) { super(fa); } @NonNull @Override public Fragment createFragment(int position) { if (position == 0) { PostFragment fragment = new PostFragment(); Bundle bundle = new Bundle(); bundle.putInt(PostFragment.EXTRA_POST_TYPE, PostPagingSource.TYPE_USER); bundle.putString(PostFragment.EXTRA_USER_NAME, username); bundle.putString(PostFragment.EXTRA_USER_WHERE, PostPagingSource.USER_WHERE_SUBMITTED); fragment.setArguments(bundle); return fragment; } CommentsListingFragment fragment = new CommentsListingFragment(); Bundle bundle = new Bundle(); bundle.putString(CommentsListingFragment.EXTRA_USERNAME, username); bundle.putBoolean(CommentsListingFragment.EXTRA_ARE_SAVED_COMMENTS, false); fragment.setArguments(bundle); return fragment; } @Override public int getItemCount() { return 2; } @Nullable private Fragment getCurrentFragment() { if (fragmentManager == null) { return null; } return fragmentManager.findFragmentByTag("f" + binding.viewPagerViewUserDetailActivity.getCurrentItem()); } public boolean handleKeyDown(int keyCode) { if (binding.viewPagerViewUserDetailActivity.getCurrentItem() == 0) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { return ((PostFragment) fragment).handleKeyDown(keyCode); } } return false; } public void refresh() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).refresh(); } else if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).refresh(); } } public void changeSortType(SortType sortType) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeSortType(sortType); Utils.displaySortTypeInToolbar(sortType, binding.toolbarViewUserDetailActivity); } else if (fragment instanceof CommentsListingFragment) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_USER_COMMENT, sortType.getType().name()).apply(); if(sortType.getTime() != null) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TIME_USER_COMMENT, sortType.getTime().name()).apply(); } ((CommentsListingFragment) fragment).changeSortType(sortType); Utils.displaySortTypeInToolbar(sortType, binding.toolbarViewUserDetailActivity); } } public void changeNSFW(boolean nsfw) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changeNSFW(nsfw); } } void changePostLayout(int postLayout) { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).changePostLayout(postLayout); } } void goBackToTop() { Fragment fragment = getCurrentFragment(); if (fragment instanceof PostFragment) { ((PostFragment) fragment).goBackToTop(); } else if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).goBackToTop(); } } void displaySortTypeInToolbar() { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f" + binding.viewPagerViewUserDetailActivity.getCurrentItem()); if (fragment instanceof PostFragment) { SortType sortType = ((PostFragment) fragment).getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.toolbarViewUserDetailActivity); } else if (fragment instanceof CommentsListingFragment) { SortType sortType = ((CommentsListingFragment) fragment).getSortType(); Utils.displaySortTypeInToolbar(sortType, binding.toolbarViewUserDetailActivity); } } } void editComment(Comment comment, int position) { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f1"); if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).editComment(comment, position); } } } void editComment(String commentMarkdown, int position) { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f1"); if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).editComment(commentMarkdown, position); } } } void hideReadPosts() { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { ((PostFragment) fragment).hideReadPosts(); } } } void filterPosts() { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f0"); if (fragment instanceof PostFragment) { ((PostFragment) fragment).filterPosts(); } } } void toggleCommentReplyNotification(Comment comment, int position) { if (fragmentManager != null) { Fragment fragment = fragmentManager.findFragmentByTag("f1"); if (fragment instanceof CommentsListingFragment) { ((CommentsListingFragment) fragment).toggleReplyNotifications(comment, position); return; } } Toast.makeText(ViewUserDetailActivity.this, R.string.cannot_find_comment, Toast.LENGTH_SHORT).show(); } } @Override public void lockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipeRightToGoBack() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import android.Manifest; import android.app.Dialog; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Matrix; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.PersistableBundle; import android.provider.Settings; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.OrientationEventListener; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.SimpleCache; import androidx.media3.datasource.okhttp.OkHttpDataSource; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.hls.HlsMediaSource; import androidx.media3.exoplayer.source.ProgressiveMediaSource; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.ui.PlayerControlView; import androidx.media3.ui.PlayerView; import androidx.media3.ui.TrackSelectionDialogBuilder; import com.google.android.material.button.MaterialButton; import com.google.common.collect.ImmutableList; import com.otaliastudios.zoom.ZoomEngine; import com.otaliastudios.zoom.ZoomSurfaceView; import org.apache.commons.io.FilenameUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import app.futured.hauler.DragDirection; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.FetchVideoLinkListener; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.VideoLinkFetcher; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PlaybackSpeedBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityViewVideoBinding; import ml.docilealligator.infinityforreddit.databinding.ActivityViewVideoZoomableBinding; import ml.docilealligator.infinityforreddit.events.FinishViewMediaActivityEvent; import ml.docilealligator.infinityforreddit.font.ContentFontFamily; import ml.docilealligator.infinityforreddit.font.ContentFontStyle; import ml.docilealligator.infinityforreddit.font.FontFamily; import ml.docilealligator.infinityforreddit.font.FontStyle; import ml.docilealligator.infinityforreddit.font.TitleFontFamily; import ml.docilealligator.infinityforreddit.font.TitleFontStyle; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.services.DownloadRedditVideoService; import ml.docilealligator.infinityforreddit.thing.StreamableVideo; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import okhttp3.OkHttpClient; import retrofit2.Retrofit; public class ViewVideoActivity extends AppCompatActivity implements CustomFontReceiver { public static final int PLAYBACK_SPEED_25 = 25; public static final int PLAYBACK_SPEED_50 = 50; public static final int PLAYBACK_SPEED_75 = 75; public static final int PLAYBACK_SPEED_NORMAL = 100; public static final int PLAYBACK_SPEED_125 = 125; public static final int PLAYBACK_SPEED_150 = 150; public static final int PLAYBACK_SPEED_175 = 175; public static final int PLAYBACK_SPEED_200 = 200; public static final String EXTRA_VIDEO_DOWNLOAD_URL = "EVDU"; public static final String EXTRA_SUBREDDIT = "ES"; public static final String EXTRA_ID = "EI"; public static final String EXTRA_POST = "EP"; public static final String EXTRA_PROGRESS_SECONDS = "EPS"; public static final String EXTRA_REDGIFS_ID = "EGI"; public static final String EXTRA_V_REDD_IT_URL = "EVRIU"; public static final String EXTRA_STREAMABLE_SHORT_CODE = "ESSC"; public static final String EXTRA_IS_NSFW = "EIN"; public static final String EXTRA_VIDEO_TYPE = "EVT"; public static final int VIDEO_TYPE_IMGUR = 7; public static final int VIDEO_TYPE_STREAMABLE = 5; public static final int VIDEO_TYPE_V_REDD_IT = 4; public static final int VIDEO_TYPE_DIRECT = 3; public static final int VIDEO_TYPE_REDGIFS = 2; private static final int VIDEO_TYPE_NORMAL = 0; private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0; private static final String IS_MUTE_STATE = "IMS"; private static final String VIDEO_DOWNLOAD_URL_STATE = "VDUS"; private static final String VIDEO_URI_STATE = "VUS"; private static final String VIDEO_TYPE_STATE = "VTS"; private static final String SUBREDDIT_NAME_STATE = "SNS"; private static final String ID_STATE= "IS"; private static final String PLAYBACK_SPEED_STATE = "PSS"; private static final String SET_NON_DATA_SAVING_MODE_DEFAULT_RESOLUTION_ALREADY_STATE = "PSS"; public Typeface typeface; private Uri mVideoUri; private ExoPlayer player; @UnstableApi private DefaultTrackSelector trackSelector; private DataSource.Factory dataSourceFactory; private String videoDownloadUrl; private String videoFileName; private String videoFallbackDirectUrl; private String subredditName; private String id; private boolean wasPlaying; private boolean isDownloading = false; private boolean isMute = false; private boolean isNSFW; private long resumePosition = -1; private int videoType; private boolean isDataSavingMode; private int dataSavingModeDefaultResolution; private int nonDataSavingModeDefaultResolution; private boolean setDefaultResolutionAlready = false; private Integer originalOrientation; private int playbackSpeed = 100; private boolean useBottomAppBar; private ViewVideoActivityBindingAdapter binding; @Inject @Named("media3") OkHttpClient mOkHttpClient; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("redgifs") Retrofit mRedgifsRetrofit; @Inject @Named("vReddIt") Retrofit mVReddItRetrofit; @Inject Provider mStreamableApiProvider; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; @UnstableApi @Inject SimpleCache mSimpleCache; private Post post; @OptIn(markerClass = UnstableApi.class) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((Infinity) getApplication()).getAppComponent().inject(this); boolean systemDefault = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; int systemThemeType = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.THEME_KEY, "2")); switch (systemThemeType) { case 0: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO); getTheme().applyStyle(R.style.Theme_Normal, true); break; case 1: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES); if(mSharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); } break; case 2: if (systemDefault) { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM); } else { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY); } if((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO) { getTheme().applyStyle(R.style.Theme_Normal, true); } else { if(mSharedPreferences.getBoolean(SharedPreferencesUtils.AMOLED_DARK_KEY, false)) { getTheme().applyStyle(R.style.Theme_Normal_AmoledDark, true); } else { getTheme().applyStyle(R.style.Theme_Normal_NormalDark, true); } } } getTheme().applyStyle(FontStyle.valueOf(mSharedPreferences.getString(SharedPreferencesUtils.FONT_SIZE_KEY, FontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(TitleFontStyle.valueOf(mSharedPreferences.getString(SharedPreferencesUtils.TITLE_FONT_SIZE_KEY, TitleFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(ContentFontStyle.valueOf(mSharedPreferences.getString(SharedPreferencesUtils.CONTENT_FONT_SIZE_KEY, ContentFontStyle.Normal.name())).getResId(), true); getTheme().applyStyle(FontFamily.valueOf(mSharedPreferences.getString(SharedPreferencesUtils.FONT_FAMILY_KEY, FontFamily.Default.name())).getResId(), true); getTheme().applyStyle(TitleFontFamily.valueOf(mSharedPreferences.getString(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY, TitleFontFamily.Default.name())).getResId(), true); getTheme().applyStyle(ContentFontFamily.valueOf(mSharedPreferences.getString(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY, ContentFontFamily.Default.name())).getResId(), true); boolean zoomable = mSharedPreferences.getBoolean(SharedPreferencesUtils.PINCH_TO_ZOOM_VIDEO, false); if (zoomable) { binding = new ViewVideoActivityBindingAdapter(ActivityViewVideoZoomableBinding.inflate(getLayoutInflater())); setContentView(binding.getRoot()); } else { binding = new ViewVideoActivityBindingAdapter(ActivityViewVideoBinding.inflate(getLayoutInflater())); setContentView(binding.getRoot()); } EventBus.getDefault().register(this); applyCustomTheme(); setVolumeControlStream(AudioManager.STREAM_MUSIC); setTitle(" "); if (typeface != null) { binding.getTitleTextView().setTypeface(typeface); } Resources resources = getResources(); getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); useBottomAppBar = mSharedPreferences.getBoolean(SharedPreferencesUtils.USE_BOTTOM_TOOLBAR_IN_MEDIA_VIEWER, false); if (useBottomAppBar) { getSupportActionBar().hide(); binding.getBottomAppBar().setVisibility(View.VISIBLE); binding.getBackButton().setOnClickListener(view -> { finish(); }); binding.getDownloadButton().setOnClickListener(view -> { if (isDownloading) { return; } if (videoDownloadUrl == null) { Toast.makeText(this, R.string.fetching_video_info_please_wait, Toast.LENGTH_SHORT).show(); return; } isDownloading = true; requestPermissionAndDownload(); }); binding.getPlaybackSpeedButton().setOnClickListener(view -> { changePlaybackSpeed(); }); } else { ActionBar actionBar = getSupportActionBar(); Drawable upArrow = resources.getDrawable(R.drawable.ic_arrow_back_white_24dp); actionBar.setHomeAsUpIndicator(upArrow); actionBar.setBackgroundDrawable(new ColorDrawable(resources.getColor(R.color.transparentActionBarAndExoPlayerControllerColor))); } String dataSavingModeString = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); int networkType = Utils.getConnectedNetwork(this); if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ALWAYS)) { isDataSavingMode = true; } else if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { isDataSavingMode = networkType == Utils.NETWORK_TYPE_CELLULAR; } dataSavingModeDefaultResolution = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION, "360")); nonDataSavingModeDefaultResolution = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION_NO_DATA_SAVING, "0")); if (!mSharedPreferences.getBoolean(SharedPreferencesUtils.VIDEO_PLAYER_IGNORE_NAV_BAR, false)) { LinearLayout controllerLinearLayout = findViewById(R.id.linear_layout_exo_playback_control_view); ViewCompat.setOnApplyWindowInsetsListener(controllerLinearLayout, new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets navigationBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars()); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) controllerLinearLayout.getLayoutParams(); params.bottomMargin = navigationBars.bottom; params.rightMargin = navigationBars.right; return WindowInsetsCompat.CONSUMED; } }); } if (mSharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_VERTICALLY_TO_GO_BACK_FROM_MEDIA, true)) { binding.getRoot().setOnDragDismissedListener(dragDirection -> { player.stop(); int slide = dragDirection == DragDirection.UP ? R.anim.slide_out_up : R.anim.slide_out_down; finish(); overridePendingTransition(0, slide); }); } else { binding.getRoot().setDragEnabled(false); } Intent intent = getIntent(); isNSFW = intent.getBooleanExtra(EXTRA_IS_NSFW, false); if (savedInstanceState == null) { resumePosition = intent.getLongExtra(EXTRA_PROGRESS_SECONDS, -1); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.VIDEO_PLAYER_AUTOMATIC_LANDSCAPE_ORIENTATION, false)) { originalOrientation = resources.getConfiguration().orientation; try { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); if (android.provider.Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1) { OrientationEventListener orientationEventListener = new OrientationEventListener(this) { @Override public void onOrientationChanged(int orientation) { int epsilon = 10; int leftLandscape = 90; int rightLandscape = 270; if(epsilonCheck(orientation, leftLandscape, epsilon) || epsilonCheck(orientation, rightLandscape, epsilon)) { try { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); disable(); } catch (Exception ignore) {} } } private boolean epsilonCheck(int a, int b, int epsilon) { return a > b - epsilon && a < b + epsilon; } }; orientationEventListener.enable(); } } catch (Exception ignore) {} } } post = intent.getParcelableExtra(EXTRA_POST); if (post != null) { binding.getTitleTextView().setText(post.getTitle()); videoFallbackDirectUrl = post.getVideoFallBackDirectUrl(); } trackSelector = new DefaultTrackSelector(this); player = new ExoPlayer.Builder(this) .setTrackSelector(trackSelector) .setRenderersFactory(new DefaultRenderersFactory(this).setEnableDecoderFallback(true)) .build(); if (zoomable) { PlayerControlView playerControlView = findViewById(R.id.player_control_view_view_video_activity); playerControlView.addVisibilityListener(visibility -> { switch (visibility) { case View.GONE: getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); break; case View.VISIBLE: getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } }); playerControlView.setPlayer(player); ZoomSurfaceView zoomSurfaceView = findViewById(R.id.zoom_surface_view_view_video_activity); player.addListener(new Player.Listener() { @Override public void onVideoSizeChanged(VideoSize videoSize) { zoomSurfaceView.setContentSize(videoSize.width, videoSize.height); } }); zoomSurfaceView.addCallback(new ZoomSurfaceView.Callback() { @Override public void onZoomSurfaceCreated(@NonNull ZoomSurfaceView zoomSurfaceView) { player.setVideoSurface(zoomSurfaceView.getSurface()); } @Override public void onZoomSurfaceDestroyed(@NonNull ZoomSurfaceView zoomSurfaceView) { } }); zoomSurfaceView.getEngine().addListener(new ZoomEngine.Listener() { @Override public void onUpdate(@NonNull ZoomEngine zoomEngine, @NonNull Matrix matrix) { if (zoomEngine.getZoom() < 1.00001) { binding.getRoot().setDragEnabled(true); binding.getNestedScrollView().setScrollEnabled(true); } else { binding.getRoot().setDragEnabled(false); binding.getNestedScrollView().setScrollEnabled(false); } } @Override public void onIdle(@NonNull ZoomEngine zoomEngine) {} }); zoomSurfaceView.setOnClickListener(view -> { if (playerControlView.isVisible()) { playerControlView.hide(); } else { playerControlView.show(); } }); } else { PlayerView videoPlayerView = findViewById(R.id.player_view_view_video_activity); videoPlayerView.setPlayer(player); videoPlayerView.setControllerVisibilityListener((PlayerView.ControllerVisibilityListener) visibility -> { switch (visibility) { case View.GONE: getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); break; case View.VISIBLE: getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } }); } if (savedInstanceState == null) { mVideoUri = intent.getData(); videoType = getIntent().getIntExtra(EXTRA_VIDEO_TYPE, VIDEO_TYPE_NORMAL); subredditName = intent.getStringExtra(EXTRA_SUBREDDIT); id = intent.getStringExtra(EXTRA_ID); setPlaybackSpeed(Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.DEFAULT_PLAYBACK_SPEED, "100"))); } else { String videoUrl = savedInstanceState.getString(VIDEO_URI_STATE); if (videoUrl != null) { mVideoUri = Uri.parse(videoUrl); } videoType = savedInstanceState.getInt(VIDEO_TYPE_STATE); subredditName = savedInstanceState.getString(SUBREDDIT_NAME_STATE); id = savedInstanceState.getString(ID_STATE); setDefaultResolutionAlready = savedInstanceState.getBoolean(SET_NON_DATA_SAVING_MODE_DEFAULT_RESOLUTION_ALREADY_STATE); setPlaybackSpeed(savedInstanceState.getInt(PLAYBACK_SPEED_STATE, 100)); } // If subredditName is null and we have a post object, get it from the post if (subredditName == null && post != null) { subredditName = post.getSubredditName(); Log.d("ViewVideoActivity", "Got subredditName from post: " + subredditName); } // If id is null and we have a post object, get it from the post if (id == null && post != null) { id = post.getId(); Log.d("ViewVideoActivity", "Got id from post: " + id); } // If this is a Tumblr post, ensure videoType is VIDEO_TYPE_DIRECT // This handles cases where the calling intent might not set EXTRA_VIDEO_TYPE appropriately for Tumblr MP4s. if (post != null && post.isTumblr()) { // Assuming post.isTumblr() method exists if (videoType != VIDEO_TYPE_DIRECT) { Log.d("ViewVideoActivity", "Tumblr post detected. Overriding videoType to DIRECT. Original type: " + videoType); videoType = VIDEO_TYPE_DIRECT; } } MaterialButton playPauseButton = findViewById(R.id.exo_play_pause_button_exo_playback_control_view); Drawable playDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_play_arrow_24dp, null); Drawable pauseDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_pause_24dp, null); binding.getPlayPauseButton().setOnClickListener(view -> { Util.handlePlayPauseButtonAction(player); }); player.addListener(new Player.Listener() { @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { if (events.containsAny( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { binding.getPlayPauseButton().setIcon(Util.shouldShowPlayButton(player) ? playDrawable : pauseDrawable); } } @Override public void onTracksChanged(@NonNull Tracks tracks) { ImmutableList trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { if (videoType == VIDEO_TYPE_NORMAL) { binding.getVideoQualityButton().setVisibility(View.VISIBLE); binding.getVideoQualityButton().setOnClickListener(view -> { TrackSelectionDialogBuilder builder = new TrackSelectionDialogBuilder(ViewVideoActivity.this, getString(R.string.select_video_quality), player, C.TRACK_TYPE_VIDEO); builder.setShowDisableOption(true); builder.setAllowAdaptiveSelections(false); Dialog dialog = builder.setTheme(R.style.MaterialAlertDialogTheme).build(); dialog.show(); if (dialog instanceof AlertDialog) { ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); } }); if (!setDefaultResolutionAlready) { int desiredResolution = 0; if (isDataSavingMode) { if (dataSavingModeDefaultResolution > 0) { desiredResolution = dataSavingModeDefaultResolution; } } else if (nonDataSavingModeDefaultResolution > 0) { desiredResolution = nonDataSavingModeDefaultResolution; } if (desiredResolution > 0) { TrackSelectionOverride trackSelectionOverride = null; int bestTrackIndex = -1; int bestResolution = -1; int worstResolution = Integer.MAX_VALUE; int worstTrackIndex = -1; Tracks.Group bestTrackGroup = null; Tracks.Group worstTrackGroup = null; for (Tracks.Group trackGroup : tracks.getGroups()) { if (trackGroup.getType() == C.TRACK_TYPE_VIDEO) { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { int trackResolution = Math.min(trackGroup.getTrackFormat(trackIndex).height, trackGroup.getTrackFormat(trackIndex).width); if (trackResolution <= desiredResolution && trackResolution > bestResolution) { bestTrackIndex = trackIndex; bestResolution = trackResolution; bestTrackGroup = trackGroup; } if (trackResolution < worstResolution) { worstTrackIndex = trackIndex; worstResolution = trackResolution; worstTrackGroup = trackGroup; } } } } if (bestTrackIndex != -1 && bestTrackGroup != null) { trackSelectionOverride = new TrackSelectionOverride(bestTrackGroup.getMediaTrackGroup(), ImmutableList.of(bestTrackIndex)); } else if (worstTrackIndex != -1 && worstTrackGroup != null) { trackSelectionOverride = new TrackSelectionOverride(worstTrackGroup.getMediaTrackGroup(), ImmutableList.of(worstTrackIndex)); } if (trackSelectionOverride != null) { player.setTrackSelectionParameters(player.getTrackSelectionParameters().buildUpon().addOverride(trackSelectionOverride).build()); } } setDefaultResolutionAlready = true; } } for (Tracks.Group trackGroup : tracks.getGroups()) { if (trackGroup.getType() == C.TRACK_TYPE_AUDIO) { if (videoType == VIDEO_TYPE_NORMAL && trackGroup.length > 1) { // Reddit video HLS usually has two audio tracks. The first is mono. // The second (index 1) is stereo. // Select the stereo audio track if possible. trackSelector.setParameters(trackSelector.buildUponParameters().setOverrideForType(new TrackSelectionOverride(trackGroup.getMediaTrackGroup(), 1))); } if (binding.getMuteButton().getVisibility() != View.VISIBLE) { binding.getMuteButton().setVisibility(View.VISIBLE); binding.getMuteButton().setOnClickListener(view -> { if (isMute) { isMute = false; player.setVolume(1f); binding.getMuteButton().setIconResource(R.drawable.ic_unmute_24dp); } else { isMute = true; player.setVolume(0f); binding.getMuteButton().setIconResource(R.drawable.ic_mute_24dp); } }); } break; } } } else { binding.getMuteButton().setVisibility(View.GONE); } } @Override public void onPlayerError(@NonNull PlaybackException error) { loadFallbackVideo(savedInstanceState); } }); // Produces DataSource instances through which media data is loaded. dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache).setUpstreamDataSourceFactory(new OkHttpDataSource.Factory(mOkHttpClient).setUserAgent(APIUtils.USER_AGENT)); String redgifsId = null; if (videoType == VIDEO_TYPE_STREAMABLE) { if (savedInstanceState != null) { videoDownloadUrl = savedInstanceState.getString(VIDEO_DOWNLOAD_URL_STATE); } else { videoDownloadUrl = intent.getStringExtra(EXTRA_VIDEO_DOWNLOAD_URL); } String shortCode = intent.getStringExtra(EXTRA_STREAMABLE_SHORT_CODE); } else if (videoType == VIDEO_TYPE_REDGIFS) { if (savedInstanceState != null) { videoDownloadUrl = savedInstanceState.getString(VIDEO_DOWNLOAD_URL_STATE); } else { videoDownloadUrl = intent.getStringExtra(EXTRA_VIDEO_DOWNLOAD_URL); } redgifsId = intent.getStringExtra(EXTRA_REDGIFS_ID); /*if (redgifsId != null && redgifsId.contains("-")) { redgifsId = redgifsId.substring(0, redgifsId.indexOf('-')); }*/ videoFileName = "Redgifs-" + redgifsId + ".mp4"; } else if (videoType == VIDEO_TYPE_DIRECT || videoType == VIDEO_TYPE_IMGUR) { videoDownloadUrl = mVideoUri.toString(); } else { videoDownloadUrl = intent.getStringExtra(EXTRA_VIDEO_DOWNLOAD_URL); } if (mVideoUri == null) { binding.getLoadingIndicator().setVisibility(View.VISIBLE); VideoLinkFetcher.fetchVideoLink(mExecutor, new Handler(getMainLooper()), mRetrofit, mVReddItRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCurrentAccountSharedPreferences, videoType, redgifsId, getIntent().getStringExtra(EXTRA_V_REDD_IT_URL), intent.getStringExtra(EXTRA_STREAMABLE_SHORT_CODE), new FetchVideoLinkListener() { @Override public void onFetchRedditVideoLinkSuccess(Post post, String fileName) { videoType = VIDEO_TYPE_NORMAL; videoFileName = fileName; binding.getLoadingIndicator().setVisibility(View.GONE); mVideoUri = Uri.parse(post.getVideoUrl()); subredditName = post.getSubredditName(); id = post.getId(); ViewVideoActivity.this.videoDownloadUrl = post.getVideoDownloadUrl(); videoFileName = subredditName + "-" + id + ".mp4"; // Prepare the player with the source. preparePlayer(savedInstanceState); player.prepare(); player.setMediaSource(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); } @Override public void onFetchImgurVideoLinkSuccess(String videoUrl, String videoDownloadUrl, String fileName) { videoType = VIDEO_TYPE_IMGUR; videoFileName = fileName; binding.getLoadingIndicator().setVisibility(View.GONE); mVideoUri = Uri.parse(videoUrl); ViewVideoActivity.this.videoDownloadUrl = videoDownloadUrl; videoFileName = "Imgur-" + FilenameUtils.getName(videoDownloadUrl); // Prepare the player with the source. player.prepare(); player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } @Override public void onFetchRedgifsVideoLinkSuccess(String webm, String mp4) { videoType = VIDEO_TYPE_REDGIFS; binding.getLoadingIndicator().setVisibility(View.GONE); mVideoUri = Uri.parse(webm); videoDownloadUrl = mp4; preparePlayer(savedInstanceState); player.prepare(); player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); } @Override public void onFetchStreamableVideoLinkSuccess(StreamableVideo streamableVideo) { videoType = VIDEO_TYPE_STREAMABLE; binding.getLoadingIndicator().setVisibility(View.GONE); if (streamableVideo.mp4 == null && streamableVideo.mp4Mobile == null) { Toast.makeText(ViewVideoActivity.this, R.string.fetch_streamable_video_failed, Toast.LENGTH_SHORT).show(); return; } binding.getTitleTextView().setText(streamableVideo.title); videoDownloadUrl = streamableVideo.mp4 == null ? streamableVideo.mp4Mobile.url : streamableVideo.mp4.url; mVideoUri = Uri.parse(videoDownloadUrl); preparePlayer(savedInstanceState); player.prepare(); player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); } @Override public void onChangeFileName(String fileName) { videoFileName = fileName; } @Override public void onFetchVideoFallbackDirectUrlSuccess(String videoFallbackDirectUrl) { ViewVideoActivity.this.videoFallbackDirectUrl = videoFallbackDirectUrl; } @Override public void failed(@Nullable Integer messageRes) { binding.getLoadingIndicator().setVisibility(View.GONE); if (videoType == VIDEO_TYPE_V_REDD_IT) { if (messageRes != null) { Toast.makeText(ViewVideoActivity.this, messageRes, Toast.LENGTH_LONG).show(); } } else { loadFallbackVideo(savedInstanceState); } } }); } else { binding.getLoadingIndicator().setVisibility(View.GONE); if (videoType == VIDEO_TYPE_NORMAL) { // Prepare the player with the source. player.prepare(); player.setMediaSource(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } else { // Prepare the player with the source. player.prepare(); player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } } getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { player.stop(); setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); } }); } private void applyCustomTheme() { binding.getPlayPauseButton().setBackgroundColor(mCustomThemeWrapper.getColorAccent()); binding.getPlayPauseButton().setIconTint(ColorStateList.valueOf(mCustomThemeWrapper.getFABIconColor())); } private void preparePlayer(Bundle savedInstanceState) { if (mSharedPreferences.getBoolean(SharedPreferencesUtils.LOOP_VIDEO, true)) { player.setRepeatMode(Player.REPEAT_MODE_ALL); } else { player.setRepeatMode(Player.REPEAT_MODE_OFF); } if (resumePosition > 0) { player.seekTo(resumePosition); } player.setPlayWhenReady(true); wasPlaying = true; boolean muteVideo = mSharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_VIDEO, false) || (mSharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_NSFW_VIDEO, false) && isNSFW); if (savedInstanceState != null) { isMute = savedInstanceState.getBoolean(IS_MUTE_STATE); if (isMute) { player.setVolume(0f); binding.getMuteButton().setIconResource(R.drawable.ic_mute_24dp); } else { player.setVolume(1f); binding.getMuteButton().setIconResource(R.drawable.ic_unmute_24dp); } } else if (muteVideo) { isMute = true; player.setVolume(0f); binding.getMuteButton().setIconResource(R.drawable.ic_mute_24dp); } else { binding.getMuteButton().setIconResource(R.drawable.ic_unmute_24dp); } } private void changePlaybackSpeed() { PlaybackSpeedBottomSheetFragment playbackSpeedBottomSheetFragment = new PlaybackSpeedBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putInt(PlaybackSpeedBottomSheetFragment.EXTRA_PLAYBACK_SPEED, playbackSpeed); playbackSpeedBottomSheetFragment.setArguments(bundle); playbackSpeedBottomSheetFragment.show(getSupportFragmentManager(), playbackSpeedBottomSheetFragment.getTag()); } @OptIn(markerClass = UnstableApi.class) private int inferPrimaryTrackType(Format format) { int trackType = MimeTypes.getTrackType(format.sampleMimeType); if (trackType != C.TRACK_TYPE_UNKNOWN) { return trackType; } if (MimeTypes.getVideoMediaMimeType(format.codecs) != null) { return C.TRACK_TYPE_VIDEO; } if (MimeTypes.getAudioMediaMimeType(format.codecs) != null) { return C.TRACK_TYPE_AUDIO; } if (format.width != Format.NO_VALUE || format.height != Format.NO_VALUE) { return C.TRACK_TYPE_VIDEO; } if (format.channelCount != Format.NO_VALUE || format.sampleRate != Format.NO_VALUE) { return C.TRACK_TYPE_AUDIO; } return C.TRACK_TYPE_UNKNOWN; } @OptIn(markerClass = UnstableApi.class) private void loadFallbackVideo(Bundle savedInstanceState) { if (videoFallbackDirectUrl != null) { MediaItem mediaItem = player.getCurrentMediaItem(); if (mediaItem == null || (mediaItem.localConfiguration != null && !videoFallbackDirectUrl.equals(mediaItem.localConfiguration.uri.toString()))) { videoType = VIDEO_TYPE_DIRECT; videoDownloadUrl = videoFallbackDirectUrl; mVideoUri = Uri.parse(videoFallbackDirectUrl); videoFileName = videoFileName == null ? FilenameUtils.getName(videoDownloadUrl) : videoFileName; player.prepare(); player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.view_video_activity, menu); for (int i = 0; i < menu.size(); i++) { Utils.setTitleWithCustomFontToMenuItem(typeface, menu.getItem(i), null); } return true; } @Override protected void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); player.seekToDefaultPosition(); player.stop(); player.release(); } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { finish(); return true; } else if (itemId == R.id.action_download_view_video_activity) { if (isDownloading) { return false; } if (videoDownloadUrl == null) { Toast.makeText(this, R.string.fetching_video_info_please_wait, Toast.LENGTH_SHORT).show(); return true; } isDownloading = true; requestPermissionAndDownload(); return true; } else if (itemId == R.id.action_playback_speed_view_video_activity) { changePlaybackSpeed(); return true; } return false; } public void setPlaybackSpeed(int speed100X) { this.playbackSpeed = speed100X <= 0 ? 100 : speed100X; player.setPlaybackParameters(new PlaybackParameters((speed100X / 100.0f))); } private void requestPermissionAndDownload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted download(); } } else { download(); } } @Override protected void onStart() { super.onStart(); if (wasPlaying) { player.setPlayWhenReady(true); } } @Override protected void onStop() { super.onStop(); wasPlaying = player.getPlayWhenReady(); player.setPlayWhenReady(false); if (originalOrientation != null) { try { setRequestedOrientation(originalOrientation); } catch (Exception ignore) {} } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED && isDownloading) { download(); } isDownloading = false; } } private void download() { isDownloading = false; // Check download location before starting download String downloadLocation = ""; if (isNSFW && mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); } else { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); } if (downloadLocation == null || downloadLocation.isEmpty()) { Toast.makeText(this, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return; } PersistableBundle extras = new PersistableBundle(); if (post != null) { String title = post.getTitle(); String sanitizedTitle = title.replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("[\\s_]+", "_").replaceAll("^_+|_+$", ""); if (sanitizedTitle.length() > 100) sanitizedTitle = sanitizedTitle.substring(0, 100).replaceAll("_+$", ""); if (sanitizedTitle.isEmpty()) sanitizedTitle = "video_" + System.currentTimeMillis(); if (post.getId() != null && !post.getId().isEmpty()) { sanitizedTitle = sanitizedTitle + "_" + post.getId(); } if (videoType != VIDEO_TYPE_NORMAL || post.isTumblr()) { if (post.getPostType() == Post.GIF_TYPE) { extras.putString(DownloadMediaService.EXTRA_URL, post.getVideoUrl()); extras.putInt(DownloadMediaService.EXTRA_MEDIA_TYPE, DownloadMediaService.EXTRA_MEDIA_TYPE_GIF); extras.putString(DownloadMediaService.EXTRA_FILE_NAME, sanitizedTitle + ".gif"); } else { extras.putString(DownloadMediaService.EXTRA_URL, videoDownloadUrl); extras.putInt(DownloadMediaService.EXTRA_MEDIA_TYPE, DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO); extras.putString(DownloadMediaService.EXTRA_FILE_NAME, sanitizedTitle + ".mp4"); } extras.putString(DownloadMediaService.EXTRA_SUBREDDIT_NAME, subredditName); extras.putInt(DownloadMediaService.EXTRA_IS_NSFW, isNSFW ? 1 : 0); //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadMediaService.constructJobInfo(this, 5000000, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); } else { extras.putString(DownloadRedditVideoService.EXTRA_VIDEO_URL, videoDownloadUrl); extras.putString(DownloadRedditVideoService.EXTRA_POST_ID, post.getId()); extras.putString(DownloadRedditVideoService.EXTRA_SUBREDDIT, subredditName); extras.putInt(DownloadRedditVideoService.EXTRA_IS_NSFW, isNSFW ? 1 : 0); extras.putString(DownloadRedditVideoService.EXTRA_FILE_NAME, sanitizedTitle + ".mp4"); //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadRedditVideoService.constructJobInfo(this, 5000000, extras); ((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); } Toast.makeText(this, R.string.download_started, Toast.LENGTH_SHORT).show(); } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(IS_MUTE_STATE, isMute); outState.putInt(VIDEO_TYPE_STATE, videoType); if (mVideoUri != null) { outState.putString(VIDEO_URI_STATE, mVideoUri.toString()); outState.putString(VIDEO_DOWNLOAD_URL_STATE, videoDownloadUrl); outState.putString(SUBREDDIT_NAME_STATE, subredditName); outState.putString(ID_STATE, id); } outState.putInt(PLAYBACK_SPEED_STATE, playbackSpeed); outState.putBoolean(SET_NON_DATA_SAVING_MODE_DEFAULT_RESOLUTION_ALREADY_STATE, setDefaultResolutionAlready); } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } @Subscribe public void onFinishViewMediaActivityEvent(FinishViewMediaActivityEvent e) { finish(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivityBindingAdapter.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.widget.ImageButton; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.google.android.material.bottomappbar.BottomAppBar; import com.google.android.material.button.MaterialButton; import com.google.android.material.loadingindicator.LoadingIndicator; import app.futured.hauler.HaulerView; import app.futured.hauler.LockableNestedScrollView; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.databinding.ActivityViewVideoBinding; import ml.docilealligator.infinityforreddit.databinding.ActivityViewVideoZoomableBinding; public class ViewVideoActivityBindingAdapter { @Nullable private ActivityViewVideoBinding binding; @Nullable private ActivityViewVideoZoomableBinding zoomableBinding; private final MaterialButton playPauseButton; private final ImageButton forwardButton; private final ImageButton rewindButton; private final MaterialButton muteButton; private final MaterialButton videoQualityButton; private final BottomAppBar bottomAppBar; private final TextView titleTextView; private final MaterialButton backButton; private final MaterialButton downloadButton; private final MaterialButton playbackSpeedButton; public ViewVideoActivityBindingAdapter(ActivityViewVideoBinding binding) { this.binding = binding; playPauseButton = binding.getRoot().findViewById(R.id.exo_play_pause_button_exo_playback_control_view); forwardButton = binding.getRoot().findViewById(R.id.exo_ffwd); rewindButton = binding.getRoot().findViewById(R.id.exo_rew); muteButton = binding.getRoot().findViewById(R.id.mute_exo_playback_control_view); videoQualityButton = binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view); bottomAppBar = binding.getRoot().findViewById(R.id.bottom_navigation_exo_playback_control_view); titleTextView = binding.getRoot().findViewById(R.id.title_text_view_exo_playback_control_view); backButton = binding.getRoot().findViewById(R.id.back_button_exo_playback_control_view); downloadButton = binding.getRoot().findViewById(R.id.download_image_view_exo_playback_control_view); playbackSpeedButton = binding.getRoot().findViewById(R.id.playback_speed_image_view_exo_playback_control_view); } public ViewVideoActivityBindingAdapter(ActivityViewVideoZoomableBinding binding) { zoomableBinding = binding; playPauseButton = binding.getRoot().findViewById(R.id.exo_play_pause_button_exo_playback_control_view); forwardButton = binding.getRoot().findViewById(R.id.exo_ffwd); rewindButton = binding.getRoot().findViewById(R.id.exo_rew); muteButton = binding.getRoot().findViewById(R.id.mute_exo_playback_control_view); videoQualityButton = binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view); bottomAppBar = binding.getRoot().findViewById(R.id.bottom_navigation_exo_playback_control_view); titleTextView = binding.getRoot().findViewById(R.id.title_text_view_exo_playback_control_view); backButton = binding.getRoot().findViewById(R.id.back_button_exo_playback_control_view); downloadButton = binding.getRoot().findViewById(R.id.download_image_view_exo_playback_control_view); playbackSpeedButton = binding.getRoot().findViewById(R.id.playback_speed_image_view_exo_playback_control_view); } public HaulerView getRoot() { return binding == null ? zoomableBinding.getRoot() : binding.getRoot(); } public CoordinatorLayout getCoordinatorLayout() { return binding == null ? zoomableBinding.coordinatorLayoutViewVideoActivity : binding.coordinatorLayoutViewVideoActivity; } public LoadingIndicator getLoadingIndicator() { return binding == null ? zoomableBinding.progressBarViewVideoActivity : binding.progressBarViewVideoActivity; } public MaterialButton getPlayPauseButton() { return playPauseButton; } public ImageButton getForwardButton() { return forwardButton; } public ImageButton getRewindButton() { return rewindButton; } public MaterialButton getMuteButton() { return muteButton; } public MaterialButton getVideoQualityButton() { return videoQualityButton; } public BottomAppBar getBottomAppBar() { return bottomAppBar; } public TextView getTitleTextView() { return titleTextView; } public MaterialButton getBackButton() { return backButton; } public MaterialButton getDownloadButton() { return downloadButton; } public MaterialButton getPlaybackSpeedButton() { return playbackSpeedButton; } public LockableNestedScrollView getNestedScrollView() { return binding == null ? zoomableBinding.lockableNestedScrollViewViewVideoActivity : binding.lockableNestedScrollViewViewVideoActivity; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/WebViewActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.InflateException; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ActivityWebViewBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class WebViewActivity extends BaseActivity { @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private String url; private ActivityWebViewBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); setImmersiveModeNotApplicableBelowAndroid16(); super.onCreate(savedInstanceState); try { binding = ActivityWebViewBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } catch (InflateException ie) { Log.e("WebViewActivity", "Failed to inflate WebViewActivity: " + ie.getMessage()); Toast.makeText(WebViewActivity.this, R.string.no_system_webview_error, Toast.LENGTH_SHORT).show(); finish(); return; } applyCustomTheme(); if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, true, isForcedImmersiveInterface()); setMargins(binding.toolbarWebViewActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); binding.webViewWebViewActivity.setPadding( allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } setSupportActionBar(binding.toolbarWebViewActivity); if (mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCESS_TOKEN, null) == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binding.webViewWebViewActivity.setAnonymous(true); } binding.webViewWebViewActivity.getSettings().setJavaScriptEnabled(true); binding.webViewWebViewActivity.getSettings().setDomStorageEnabled(true); url = getIntent().getDataString(); if (savedInstanceState == null) { binding.toolbarWebViewActivity.setTitle(url); binding.webViewWebViewActivity.loadUrl(url); } WebViewClient client = new WebViewClient() { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { WebViewActivity.this.url = url; binding.toolbarWebViewActivity.setTitle(url); } @Override public void onPageFinished(WebView view, String url) { binding.toolbarWebViewActivity.setTitle(view.getTitle()); } }; binding.webViewWebViewActivity.setWebViewClient(client); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (binding.webViewWebViewActivity.canGoBack()) { binding.webViewWebViewActivity.goBack(); } else { setEnabled(false); triggerBackPress(); } } }); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutWebViewActivity, null, binding.toolbarWebViewActivity); Drawable closeIcon = Utils.getTintedDrawable(this, R.drawable.ic_close_24dp, mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor()); binding.toolbarWebViewActivity.setNavigationIcon(closeIcon); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.web_view_activity, menu); applyMenuItemTheme(menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.action_refresh_web_view_activity) { binding.webViewWebViewActivity.reload(); return true; } else if (item.getItemId() == R.id.action_share_link_web_view_activity) { try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, url); startActivity(Intent.createChooser(intent, getString(R.string.share))); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_activity_found_for_share, Toast.LENGTH_SHORT).show(); } return true; } else if (item.getItemId() == R.id.action_copy_link_web_view_activity) { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", url); clipboard.setPrimaryClip(clip); if (android.os.Build.VERSION.SDK_INT < 33) { Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(this, R.string.copy_link_failed, Toast.LENGTH_SHORT).show(); } return true; } else if (item.getItemId() == R.id.action_open_external_browser_web_view_activity) { try { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_activity_found_for_external_browser, Toast.LENGTH_SHORT).show(); } } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); binding.webViewWebViewActivity.saveState(outState); } @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); binding.webViewWebViewActivity.restoreState(savedInstanceState); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/activities/WikiActivity.java ================================================ package ml.docilealligator.infinityforreddit.activities; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.Spanned; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.json.JSONException; import org.json.JSONObject; import javax.inject.Inject; import javax.inject.Named; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.recycler.MarkwonAdapter; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.customviews.SwipeLockInterface; import ml.docilealligator.infinityforreddit.customviews.SwipeLockLinearLayoutManager; import ml.docilealligator.infinityforreddit.databinding.ActivityWikiBinding; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent; import ml.docilealligator.infinityforreddit.markdown.EmoteCloseBracketInlineProcessor; import ml.docilealligator.infinityforreddit.markdown.EmotePlugin; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class WikiActivity extends BaseActivity { public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_WIKI_PATH = "EWP"; private static final String WIKI_MARKDOWN_STATE = "WMS"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private String wikiMarkdown; private String mSubredditName; private EmoteCloseBracketInlineProcessor emoteCloseBracketInlineProcessor; private EmotePlugin emotePlugin; private ImageAndGifPlugin imageAndGifPlugin; private ImageAndGifEntry imageAndGifEntry; private Markwon markwon; private MarkwonAdapter markwonAdapter; private boolean isRefreshing = false; private RequestManager mGlide; private ActivityWikiBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { ((Infinity) getApplication()).getAppComponent().inject(this); super.onCreate(savedInstanceState); binding = ActivityWikiBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); EventBus.getDefault().register(this); applyCustomTheme(); setSupportActionBar(binding.toolbarCommentWikiActivity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); attachSliderPanelIfApplicable(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Window window = getWindow(); if (isChangeStatusBarIconColor()) { addOnOffsetChangedListener(binding.appbarLayoutCommentWikiActivity); } if (isImmersiveInterfaceRespectForcedEdgeToEdge()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false); } else { window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, isForcedImmersiveInterface()); setMargins(binding.toolbarCommentWikiActivity, allInsets.left, allInsets.top, allInsets.right, BaseActivity.IGNORE_MARGIN); int padding16 = (int) Utils.convertDpToPixel(16, WikiActivity.this); binding.contentMarkdownViewCommentWikiActivity.setPadding( padding16 + allInsets.left, 0, padding16 + allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); /*adjustToolbar(binding.toolbarCommentWikiActivity); binding.contentMarkdownViewCommentWikiActivity.setPadding(binding.contentMarkdownViewCommentWikiActivity.getPaddingLeft(), 0, binding.contentMarkdownViewCommentWikiActivity.getPaddingRight(), getNavBarHeight());*/ } } mGlide = Glide.with(this); mSubredditName = getIntent().getStringExtra(EXTRA_SUBREDDIT_NAME); binding.swipeRefreshLayoutWikiActivity.setEnabled(mSharedPreferences.getBoolean(SharedPreferencesUtils.PULL_TO_REFRESH, true)); binding.swipeRefreshLayoutWikiActivity.setOnRefreshListener(this::loadWiki); int markdownColor = mCustomThemeWrapper.getPrimaryTextColor(); int spoilerBackgroundColor = markdownColor | 0xFF000000; int linkColor = mCustomThemeWrapper.getLinkColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (contentTypeface != null) { textView.setTypeface(contentTypeface); } textView.setTextColor(markdownColor); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(WikiActivity.this, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(linkColor); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(getSupportFragmentManager(), null); return true; }; emoteCloseBracketInlineProcessor = new EmoteCloseBracketInlineProcessor(); emotePlugin = EmotePlugin.create(this, SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, mediaMetadata -> { Intent intent = new Intent(this, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, mSubredditName); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); imageAndGifPlugin = new ImageAndGifPlugin(); imageAndGifEntry = new ImageAndGifEntry(this, mGlide, SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, (mediaMetadata, commentId, postId) -> { Intent intent = new Intent(this, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, mSubredditName); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); markwon = MarkdownUtils.createFullRedditMarkwon(this, miscPlugin, emoteCloseBracketInlineProcessor, emotePlugin, imageAndGifPlugin, markdownColor, spoilerBackgroundColor, onLinkLongClickListener); markwonAdapter = MarkdownUtils.createCustomTablesAndImagesAdapter(this, imageAndGifEntry); LinearLayoutManagerBugFixed linearLayoutManager = new SwipeLockLinearLayoutManager(this, new SwipeLockInterface() { @Override public void lockSwipe() { if (mSliderPanel != null) { mSliderPanel.lock(); } } @Override public void unlockSwipe() { if (mSliderPanel != null) { mSliderPanel.unlock(); } } }); binding.contentMarkdownViewCommentWikiActivity.setLayoutManager(linearLayoutManager); binding.contentMarkdownViewCommentWikiActivity.setAdapter(markwonAdapter); if (savedInstanceState != null) { wikiMarkdown = savedInstanceState.getString(WIKI_MARKDOWN_STATE); } if (wikiMarkdown == null) { loadWiki(); } else { markwonAdapter.setMarkdown(markwon, wikiMarkdown); // noinspection NotifyDataSetChanged markwonAdapter.notifyDataSetChanged(); } } private void loadWiki() { if (isRefreshing) { return; } isRefreshing = true; binding.swipeRefreshLayoutWikiActivity.setRefreshing(true); Glide.with(this).clear(binding.fetchWikiImageViewWikiActivity); binding.fetchWikiLinearLayoutWikiActivity.setVisibility(View.GONE); mRetrofit.create(RedditAPI.class).getWikiPage(mSubredditName, getIntent().getStringExtra(EXTRA_WIKI_PATH)).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { try { String markdown = new JSONObject(response.body()) .getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.CONTENT_MD_KEY); markwonAdapter.setMarkdown(markwon, Utils.modifyMarkdown(markdown)); // noinspection NotifyDataSetChanged markwonAdapter.notifyDataSetChanged(); } catch (JSONException e) { e.printStackTrace(); showErrorView(R.string.error_loading_wiki); } } else { if (response.code() == 404 || response.code() == 403) { showErrorView(R.string.no_wiki); } else { showErrorView(R.string.error_loading_wiki); } } isRefreshing = false; binding.swipeRefreshLayoutWikiActivity.setRefreshing(false); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { showErrorView(R.string.error_loading_wiki); isRefreshing = false; binding.swipeRefreshLayoutWikiActivity.setRefreshing(false); } }); } private void showErrorView(int stringResId) { binding.swipeRefreshLayoutWikiActivity.setRefreshing(false); binding.fetchWikiLinearLayoutWikiActivity.setVisibility(View.VISIBLE); binding.fetchWikiTextViewWikiActivity.setText(stringResId); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return false; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putString(WIKI_MARKDOWN_STATE, wikiMarkdown); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Override public SharedPreferences getDefaultSharedPreferences() { return mSharedPreferences; } @Override public SharedPreferences getCurrentAccountSharedPreferences() { return mCurrentAccountSharedPreferences; } @Override public CustomThemeWrapper getCustomThemeWrapper() { return mCustomThemeWrapper; } @Override protected void applyCustomTheme() { binding.getRoot().setBackgroundColor(mCustomThemeWrapper.getBackgroundColor()); applyAppBarLayoutAndCollapsingToolbarLayoutAndToolbarTheme(binding.appbarLayoutCommentWikiActivity, binding.collapsingToolbarLayoutWikiActivity, binding.toolbarCommentWikiActivity); applyAppBarScrollFlagsIfApplicable(binding.collapsingToolbarLayoutWikiActivity); binding.swipeRefreshLayoutWikiActivity.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutWikiActivity.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); binding.fetchWikiTextViewWikiActivity.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (typeface != null) { binding.fetchWikiTextViewWikiActivity.setTypeface(typeface); } } @Subscribe public void onAccountSwitchEvent(SwitchAccountEvent event) { if (!getClass().getName().equals(event.excludeActivityClassName)) { finish(); } } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { String dataSavingMode = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { if (emotePlugin != null) { emotePlugin.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } if (imageAndGifEntry != null) { imageAndGifEntry.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/AccountChooserRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import java.util.List; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerAccountBinding; public class AccountChooserRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity baseActivity; private ArrayList accounts; private final RequestManager glide; private final int primaryTextColor; private final ItemClickListener itemClickListener; public AccountChooserRecyclerViewAdapter(BaseActivity baseActivity, CustomThemeWrapper customThemeWrapper, RequestManager glide, ItemClickListener itemClickListener) { this.baseActivity = baseActivity; this.glide = glide; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); this.itemClickListener = itemClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new AccountViewHolder(ItemNavDrawerAccountBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof AccountViewHolder) { glide.load(accounts.get(position).getProfileImageUrl()) .error(glide.load(R.drawable.subreddit_default_icon)) .transform(new RoundedCornersTransformation(128, 0)) .into(((AccountViewHolder) holder).binding.profileImageItemAccount); ((AccountViewHolder) holder).binding.usernameTextViewItemAccount.setText(accounts.get(position).getAccountName()); holder.itemView.setOnClickListener(view -> itemClickListener.onClick(accounts.get(position))); } } @Override public int getItemCount() { return accounts == null ? 0 : accounts.size(); } public void changeAccountsDataset(List accounts) { this.accounts = (ArrayList) accounts; notifyDataSetChanged(); } class AccountViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerAccountBinding binding; AccountViewHolder(@NonNull ItemNavDrawerAccountBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.usernameTextViewItemAccount.setTypeface(baseActivity.typeface); } binding.usernameTextViewItemAccount.setTextColor(primaryTextColor); } } public interface ItemClickListener { void onClick(Account account); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/AcknowledgementRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.databinding.ItemAcknowledgementBinding; import ml.docilealligator.infinityforreddit.settings.Acknowledgement; public class AcknowledgementRecyclerViewAdapter extends RecyclerView.Adapter { private final ArrayList acknowledgements; private final SettingsActivity activity; public AcknowledgementRecyclerViewAdapter(SettingsActivity activity, ArrayList acknowledgements) { this.activity = activity; this.acknowledgements = acknowledgements; } @NonNull @Override public AcknowledgementViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new AcknowledgementViewHolder(ItemAcknowledgementBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull AcknowledgementViewHolder holder, int position) { Acknowledgement acknowledgement = acknowledgements.get(holder.getBindingAdapterPosition()); if (acknowledgement != null) { holder.binding.nameTextViewItemAcknowledgement.setText(acknowledgement.getName()); holder.binding.introductionTextViewItemAcknowledgement.setText(acknowledgement.getIntroduction()); holder.itemView.setOnClickListener(view -> { if (activity != null) { Intent intent = new Intent(activity, LinkResolverActivity.class); intent.setData(acknowledgement.getLink()); activity.startActivity(intent); } }); } } @Override public int getItemCount() { return acknowledgements == null ? 0 : acknowledgements.size(); } class AcknowledgementViewHolder extends RecyclerView.ViewHolder { ItemAcknowledgementBinding binding; AcknowledgementViewHolder(@NonNull ItemAcknowledgementBinding binding) { super(binding.getRoot()); this.binding = binding; binding.nameTextViewItemAcknowledgement.setTextColor(activity.customThemeWrapper.getPrimaryTextColor()); binding.introductionTextViewItemAcknowledgement.setTextColor(activity.customThemeWrapper.getSecondaryTextColor()); if (activity.typeface != null) { binding.nameTextViewItemAcknowledgement.setTypeface(activity.typeface); binding.introductionTextViewItemAcknowledgement.setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentFilterUsageEmbeddedRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.databinding.ItemCommentFilterUsageEmbeddedBinding; public class CommentFilterUsageEmbeddedRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity baseActivity; private List commentFilterUsageList; public CommentFilterUsageEmbeddedRecyclerViewAdapter(BaseActivity baseActivity) { this.baseActivity = baseActivity; } @NonNull @Override public EntryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new EntryViewHolder(ItemCommentFilterUsageEmbeddedBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull EntryViewHolder holder, int position) { if (commentFilterUsageList == null || commentFilterUsageList.isEmpty()) { holder.textView.setText(R.string.comment_filter_applied_to_all_subreddits); } else if (holder.getBindingAdapterPosition() > 4) { holder.textView.setText(baseActivity.getString(R.string.comment_filter_usage_embedded_more_count, commentFilterUsageList.size() - 5)); } else { CommentFilterUsage commentFilterUsage = commentFilterUsageList.get(holder.getBindingAdapterPosition()); if (commentFilterUsage.usage == CommentFilterUsage.SUBREDDIT_TYPE) { holder.textView.setText("r/" + commentFilterUsage.nameOfUsage); } } } @Override public int getItemCount() { return commentFilterUsageList == null || commentFilterUsageList.isEmpty() ? 1 : (commentFilterUsageList.size() > 5 ? 6 : commentFilterUsageList.size()); } public void setCommentFilterUsageList(List commentFilterUsageList) { this.commentFilterUsageList = commentFilterUsageList; notifyDataSetChanged(); } class EntryViewHolder extends RecyclerView.ViewHolder { TextView textView; public EntryViewHolder(@NonNull ItemCommentFilterUsageEmbeddedBinding binding) { super(binding.getRoot()); textView = binding.getRoot(); textView.setTextColor(baseActivity.customThemeWrapper.getSecondaryTextColor()); if (baseActivity.typeface != null) { textView.setTypeface(baseActivity.typeface); } textView.setOnClickListener(view -> { Toast.makeText(baseActivity, textView.getText(), Toast.LENGTH_SHORT).show(); }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentFilterUsageRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; public class CommentFilterUsageRecyclerViewAdapter extends RecyclerView.Adapter { private List commentFilterUsages; private final BaseActivity activity; private final CustomThemeWrapper customThemeWrapper; private final CommentFilterUsageRecyclerViewAdapter.OnItemClickListener onItemClickListener; public interface OnItemClickListener { void onClick(CommentFilterUsage commentFilterUsage); } public CommentFilterUsageRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, CommentFilterUsageRecyclerViewAdapter.OnItemClickListener onItemClickListener) { this.activity = activity; this.customThemeWrapper = customThemeWrapper; this.onItemClickListener = onItemClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new CommentFilterUsageRecyclerViewAdapter.CommentFilterUsageViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_post_filter_usage, parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { CommentFilterUsage commentFilterUsage = commentFilterUsages.get(position); if (commentFilterUsage.usage == CommentFilterUsage.SUBREDDIT_TYPE) { ((CommentFilterUsageViewHolder) holder).usageTextView.setText(activity.getString(R.string.post_filter_usage_subreddit, commentFilterUsage.nameOfUsage)); } } @Override public int getItemCount() { return commentFilterUsages == null ? 0 : commentFilterUsages.size(); } public void setCommentFilterUsages(List commentFilterUsages) { this.commentFilterUsages = commentFilterUsages; notifyDataSetChanged(); } private class CommentFilterUsageViewHolder extends RecyclerView.ViewHolder { TextView usageTextView; public CommentFilterUsageViewHolder(@NonNull View itemView) { super(itemView); usageTextView = (TextView) itemView; usageTextView.setTextColor(customThemeWrapper.getPrimaryTextColor()); if (activity.typeface != null) { usageTextView.setTypeface(activity.typeface); } usageTextView.setOnClickListener(view -> { onItemClickListener.onClick(commentFilterUsages.get(getBindingAdapterPosition())); }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentFilterWithUsageRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterWithUsage; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ItemCommentFilterWithUsageBinding; public class CommentFilterWithUsageRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private final OnItemClickListener onItemClickListener; private List commentFilterWithUsageList; private final RecyclerView.RecycledViewPool recycledViewPool; public interface OnItemClickListener { void onItemClick(CommentFilter commentFilter); } public CommentFilterWithUsageRecyclerViewAdapter(BaseActivity activity, OnItemClickListener onItemClickListener) { this.activity = activity; this.recycledViewPool = new RecyclerView.RecycledViewPool(); this.onItemClickListener = onItemClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new CommentFilterViewHolder(ItemCommentFilterWithUsageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof CommentFilterViewHolder) { ((CommentFilterViewHolder) holder).binding.commentFilterNameTextViewItemCommentFilter.setText(commentFilterWithUsageList.get(position).commentFilter.name); ((CommentFilterViewHolder) holder).adapter.setCommentFilterUsageList(commentFilterWithUsageList.get(position).commentFilterUsageList); } } @Override public int getItemCount() { return commentFilterWithUsageList == null ? 0 : commentFilterWithUsageList.size(); } public void setCommentFilterWithUsageList(List commentFilterWithUsageList) { this.commentFilterWithUsageList = commentFilterWithUsageList; notifyDataSetChanged(); } private class CommentFilterViewHolder extends RecyclerView.ViewHolder { ItemCommentFilterWithUsageBinding binding; CommentFilterUsageEmbeddedRecyclerViewAdapter adapter; public CommentFilterViewHolder(@NonNull ItemCommentFilterWithUsageBinding binding) { super(binding.getRoot()); this.binding = binding; binding.commentFilterNameTextViewItemCommentFilter.setTextColor(activity.customThemeWrapper.getPrimaryTextColor()); if (activity.typeface != null) { binding.commentFilterNameTextViewItemCommentFilter.setTypeface(activity.typeface); } binding.getRoot().setOnClickListener(view -> { onItemClickListener.onItemClick(commentFilterWithUsageList.get(getBindingAdapterPosition()).commentFilter); }); binding.commentFilterUsageRecyclerViewItemCommentFilter.setRecycledViewPool(recycledViewPool); binding.commentFilterUsageRecyclerViewItemCommentFilter.setLayoutManager(new LinearLayoutManagerBugFixed(activity)); adapter = new CommentFilterUsageEmbeddedRecyclerViewAdapter(activity); binding.commentFilterUsageRecyclerViewItemCommentFilter.setAdapter(adapter); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsListingRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.paging.PagedListAdapter; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.google.android.material.button.MaterialButton; import java.util.Locale; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.fragments.CommentsListingFragment; import ml.docilealligator.infinityforreddit.thing.SaveThing; import ml.docilealligator.infinityforreddit.thing.VoteThing; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CommentMoreBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.CommentIndentationView; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView; import ml.docilealligator.infinityforreddit.customviews.SwipeLockInterface; import ml.docilealligator.infinityforreddit.customviews.SwipeLockLinearLayoutManager; import ml.docilealligator.infinityforreddit.databinding.ItemCommentBinding; import ml.docilealligator.infinityforreddit.databinding.ItemFooterErrorBinding; import ml.docilealligator.infinityforreddit.databinding.ItemFooterLoadingBinding; import ml.docilealligator.infinityforreddit.markdown.CustomMarkwonAdapter; import ml.docilealligator.infinityforreddit.markdown.EmoteCloseBracketInlineProcessor; import ml.docilealligator.infinityforreddit.markdown.EmotePlugin; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class CommentsListingRecyclerViewAdapter extends PagedListAdapter { private static final int VIEW_TYPE_DATA = 0; private static final int VIEW_TYPE_ERROR = 1; private static final int VIEW_TYPE_LOADING = 2; private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(@NonNull Comment comment, @NonNull Comment t1) { return comment.getId().equals(t1.getId()); } @Override public boolean areContentsTheSame(@NonNull Comment comment, @NonNull Comment t1) { return comment.getCommentMarkdown().equals(t1.getCommentMarkdown()); } }; private final BaseActivity mActivity; private final CommentsListingFragment mFragment; private final Retrofit mOauthRetrofit; private final Locale mLocale; private final EmoteCloseBracketInlineProcessor mEmoteCloseBracketInlineProcessor; private final EmotePlugin mEmotePlugin; private final ImageAndGifPlugin mImageAndGifPlugin; private final Markwon mMarkwon; private final ImageAndGifEntry mImageAndGifEntry; private final RecyclerView.RecycledViewPool recycledViewPool; private final String mAccessToken; private final String mAccountName; private final int mColorPrimaryLightTheme; private final int mSecondaryTextColor; private final int mCommentBackgroundColor; private int mCommentColor; private final int mDividerColor; private final int mUsernameColor; private final int mAuthorFlairColor; private final int mSubredditColor; private final int mUpvotedColor; private final int mDownvotedColor; private final int mButtonTextColor; private final int mColorAccent; private final int mCommentIconAndInfoColor; private final boolean mVoteButtonsOnTheRight; private final boolean mShowElapsedTime; private final String mTimeFormatPattern; private final boolean mShowCommentDivider; private final boolean mShowAbsoluteNumberOfVotes; private boolean canStartActivity = true; private NetworkState networkState; private final RetryLoadingMoreCallback mRetryLoadingMoreCallback; public CommentsListingRecyclerViewAdapter(BaseActivity activity, CommentsListingFragment fragment, Retrofit oauthRetrofit, CustomThemeWrapper customThemeWrapper, Locale locale, SharedPreferences sharedPreferences, String accessToken, @NonNull String accountName, String username, RetryLoadingMoreCallback retryLoadingMoreCallback) { super(DIFF_CALLBACK); mActivity = activity; mFragment = fragment; mOauthRetrofit = oauthRetrofit; mCommentColor = customThemeWrapper.getCommentColor(); int commentSpoilerBackgroundColor = mCommentColor | 0xFF000000; mLocale = locale; mAccessToken = accessToken; mAccountName = accountName; mShowElapsedTime = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ELAPSED_TIME_KEY, false); mShowCommentDivider = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_COMMENT_DIVIDER, false); mShowAbsoluteNumberOfVotes = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ABSOLUTE_NUMBER_OF_VOTES, true); mVoteButtonsOnTheRight = sharedPreferences.getBoolean(SharedPreferencesUtils.VOTE_BUTTONS_ON_THE_RIGHT_KEY, false); mTimeFormatPattern = sharedPreferences.getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE); mRetryLoadingMoreCallback = retryLoadingMoreCallback; mColorPrimaryLightTheme = customThemeWrapper.getColorPrimaryLightTheme(); mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); mCommentBackgroundColor = customThemeWrapper.getCommentBackgroundColor(); mCommentColor = customThemeWrapper.getCommentColor(); mDividerColor = customThemeWrapper.getDividerColor(); mSubredditColor = customThemeWrapper.getSubreddit(); mUsernameColor = customThemeWrapper.getUsername(); mAuthorFlairColor = customThemeWrapper.getAuthorFlairTextColor(); mUpvotedColor = customThemeWrapper.getUpvoted(); mDownvotedColor = customThemeWrapper.getDownvoted(); mButtonTextColor = customThemeWrapper.getButtonTextColor(); mColorAccent = customThemeWrapper.getColorAccent(); mCommentIconAndInfoColor = customThemeWrapper.getCommentIconAndInfoColor(); int linkColor = customThemeWrapper.getLinkColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (mActivity.contentTypeface != null) { textView.setTypeface(mActivity.contentTypeface); } textView.setTextColor(mCommentColor); textView.setHighlightColor(Color.TRANSPARENT); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); mActivity.startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(linkColor); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { if (!activity.isDestroyed() && !activity.isFinishing()) { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(activity.getSupportFragmentManager(), urlMenuBottomSheetFragment.getTag()); } return true; }; mEmoteCloseBracketInlineProcessor = new EmoteCloseBracketInlineProcessor(); mEmotePlugin = EmotePlugin.create(activity, Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.EMBEDDED_MEDIA_TYPE, "15")), mediaMetadata -> { Intent intent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, username); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); if (canStartActivity) { canStartActivity = false; activity.startActivity(intent); } }); mImageAndGifPlugin = new ImageAndGifPlugin(); mMarkwon = MarkdownUtils.createFullRedditMarkwon(mActivity, miscPlugin, mEmoteCloseBracketInlineProcessor, mEmotePlugin, mImageAndGifPlugin, mCommentColor, commentSpoilerBackgroundColor, onLinkLongClickListener); mImageAndGifEntry = new ImageAndGifEntry(activity, Glide.with(activity), Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.EMBEDDED_MEDIA_TYPE, "15")), (mediaMetadata, commentId, postId) -> { Intent intent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, username); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); if (commentId != null && !commentId.isEmpty()) { intent.putExtra(ViewImageOrGifActivity.EXTRA_COMMENT_ID_KEY, commentId); } if (postId != null && !postId.isEmpty()) { intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, postId); } if (canStartActivity) { canStartActivity = false; activity.startActivity(intent); } }); recycledViewPool = new RecyclerView.RecycledViewPool(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_DATA) { return new CommentViewHolder(ItemCommentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_ERROR) { return new ErrorViewHolder(ItemFooterErrorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new LoadingViewHolder(ItemFooterLoadingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof CommentBaseViewHolder) { Comment comment = getItem(holder.getBindingAdapterPosition()); if (comment != null) { String name = "r/" + comment.getSubredditName(); ((CommentBaseViewHolder) holder).authorTextView.setText(name); ((CommentBaseViewHolder) holder).authorTextView.setTextColor(mSubredditColor); if (comment.getAuthorFlairHTML() != null && !comment.getAuthorFlairHTML().equals("")) { ((CommentBaseViewHolder) holder).authorFlairTextView.setVisibility(View.VISIBLE); Utils.setHTMLWithImageToTextView(((CommentBaseViewHolder) holder).authorFlairTextView, comment.getAuthorFlairHTML(), true); } else if (comment.getAuthorFlair() != null && !comment.getAuthorFlair().equals("")) { ((CommentBaseViewHolder) holder).authorFlairTextView.setVisibility(View.VISIBLE); ((CommentBaseViewHolder) holder).authorFlairTextView.setText(comment.getAuthorFlair()); } if (mShowElapsedTime) { ((CommentBaseViewHolder) holder).commentTimeTextView.setText( Utils.getElapsedTime(mActivity, comment.getCommentTimeMillis())); } else { ((CommentBaseViewHolder) holder).commentTimeTextView.setText(Utils.getFormattedTime(mLocale, comment.getCommentTimeMillis(), mTimeFormatPattern)); } mEmoteCloseBracketInlineProcessor.setMediaMetadataMap(comment.getMediaMetadataMap()); mImageAndGifPlugin.setMediaMetadataMap(comment.getMediaMetadataMap()); mImageAndGifEntry.setCurrentCommentId(comment.getId()); mImageAndGifEntry.setCurrentPostId(comment.getLinkId()); ((CommentBaseViewHolder) holder).markwonAdapter.setMarkdown(mMarkwon, comment.getCommentMarkdown()); // noinspection NotifyDataSetChanged ((CommentBaseViewHolder) holder).markwonAdapter.notifyDataSetChanged(); String commentScoreText = ""; if (comment.isScoreHidden()) { commentScoreText = mActivity.getString(R.string.hidden); } else { commentScoreText = Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType()); } ((CommentBaseViewHolder) holder).scoreTextView.setText(commentScoreText); switch (comment.getVoteType()) { case Comment.VOTE_TYPE_UPVOTE: ((CommentBaseViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); ((CommentBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); ((CommentBaseViewHolder) holder).scoreTextView.setTextColor(mUpvotedColor); break; case Comment.VOTE_TYPE_DOWNVOTE: ((CommentBaseViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); ((CommentBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); ((CommentBaseViewHolder) holder).scoreTextView.setTextColor(mDownvotedColor); break; } if (comment.isSaved()) { ((CommentBaseViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } else { ((CommentBaseViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } } } } @Override public int getItemViewType(int position) { // Reached at the end if (hasExtraRow() && position == getItemCount() - 1) { if (networkState.getStatus() == NetworkState.Status.LOADING) { return VIEW_TYPE_LOADING; } else { return VIEW_TYPE_ERROR; } } else { return VIEW_TYPE_DATA; } } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof CommentBaseViewHolder) { ((CommentBaseViewHolder) holder).authorFlairTextView.setText(""); ((CommentBaseViewHolder) holder).authorFlairTextView.setVisibility(View.GONE); ((CommentBaseViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); ((CommentBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); ((CommentBaseViewHolder) holder).scoreTextView.setTextColor(mCommentIconAndInfoColor); ((CommentBaseViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); ((CommentBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); } } @Override public int getItemCount() { if (hasExtraRow()) { return super.getItemCount() + 1; } return super.getItemCount(); } private boolean hasExtraRow() { return networkState != null && networkState.getStatus() != NetworkState.Status.SUCCESS; } public void setNetworkState(NetworkState newNetworkState) { NetworkState previousState = this.networkState; boolean previousExtraRow = hasExtraRow(); this.networkState = newNetworkState; boolean newExtraRow = hasExtraRow(); if (previousExtraRow != newExtraRow) { if (previousExtraRow) { notifyItemRemoved(super.getItemCount()); } else { notifyItemInserted(super.getItemCount()); } } else if (newExtraRow && !previousState.equals(newNetworkState)) { notifyItemChanged(getItemCount() - 1); } } public void onItemSwipe(RecyclerView.ViewHolder viewHolder, int direction, int swipeLeftAction, int swipeRightAction) { if (viewHolder instanceof CommentBaseViewHolder) { if (direction == ItemTouchHelper.LEFT || direction == ItemTouchHelper.START) { if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((CommentBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((CommentBaseViewHolder) viewHolder).downvoteButton.performClick(); } } else { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((CommentBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((CommentBaseViewHolder) viewHolder).downvoteButton.performClick(); } } } } public void editComment(Comment comment, int position) { Comment oldComment = getItem(position); if (oldComment != null) { oldComment.setCommentMarkdown(comment.getCommentMarkdown()); oldComment.setMediaMetadataMap(comment.getMediaMetadataMap()); notifyItemChanged(position); } } public void editComment(String commentContentMarkdown, int position) { Comment comment = getItem(position); if (comment != null) { comment.setCommentMarkdown(commentContentMarkdown); notifyItemChanged(position); } } public void toggleReplyNotifications(int position) { Comment comment = getItem(position); if (comment != null) { comment.toggleSendReplies(); notifyItemChanged(position); } } public void updateModdedStatus(int position) { Comment originalComment = getItem(position); if (originalComment != null) { notifyItemChanged(position); } } public void setCanStartActivity(boolean canStartActivity) { this.canStartActivity = canStartActivity; } public void setDataSavingMode(boolean dataSavingMode) { mEmotePlugin.setDataSavingMode(dataSavingMode); mImageAndGifEntry.setDataSavingMode(dataSavingMode); } public interface RetryLoadingMoreCallback { void retryLoadingMore(); } public class CommentBaseViewHolder extends RecyclerView.ViewHolder { LinearLayout linearLayout; TextView authorTextView; TextView authorFlairTextView; TextView commentTimeTextView; RecyclerView commentMarkdownView; ConstraintLayout bottomConstraintLayout; MaterialButton upvoteButton; TextView scoreTextView; MaterialButton downvoteButton; View placeholder; MaterialButton moreButton; MaterialButton saveButton; MaterialButton replyButton; View commentDivider; CustomMarkwonAdapter markwonAdapter; CommentBaseViewHolder(@NonNull View itemView) { super(itemView); } void setBaseView(LinearLayout linearLayout, TextView authorTextView, TextView authorFlairTextView, TextView commentTimeTextView, RecyclerView commentMarkdownView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, View placeholder, MaterialButton moreButton, MaterialButton saveButton, TextView expandButton, MaterialButton replyButton, CommentIndentationView commentIndentationView, View commentDivider) { this.linearLayout = linearLayout; this.authorTextView = authorTextView; this.authorFlairTextView = authorFlairTextView; this.commentTimeTextView = commentTimeTextView; this.commentMarkdownView = commentMarkdownView; this.bottomConstraintLayout = bottomConstraintLayout; this.upvoteButton = upvoteButton; this.scoreTextView = scoreTextView; this.downvoteButton = downvoteButton; this.placeholder = placeholder; this.moreButton = moreButton; this.saveButton = saveButton; this.replyButton = replyButton; this.commentDivider = commentDivider; replyButton.setVisibility(View.GONE); ((ConstraintLayout.LayoutParams) authorTextView.getLayoutParams()).setMarginStart(0); ((ConstraintLayout.LayoutParams) authorFlairTextView.getLayoutParams()).setMarginStart(0); if (mVoteButtonsOnTheRight) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(bottomConstraintLayout); constraintSet.clear(upvoteButton.getId(), ConstraintSet.START); constraintSet.clear(upvoteButton.getId(), ConstraintSet.END); constraintSet.clear(scoreTextView.getId(), ConstraintSet.START); constraintSet.clear(scoreTextView.getId(), ConstraintSet.END); constraintSet.clear(downvoteButton.getId(), ConstraintSet.START); constraintSet.clear(downvoteButton.getId(), ConstraintSet.END); constraintSet.clear(expandButton.getId(), ConstraintSet.START); constraintSet.clear(expandButton.getId(), ConstraintSet.END); constraintSet.clear(saveButton.getId(), ConstraintSet.START); constraintSet.clear(saveButton.getId(), ConstraintSet.END); constraintSet.clear(replyButton.getId(), ConstraintSet.START); constraintSet.clear(replyButton.getId(), ConstraintSet.END); constraintSet.clear(moreButton.getId(), ConstraintSet.START); constraintSet.clear(moreButton.getId(), ConstraintSet.END); constraintSet.connect(upvoteButton.getId(), ConstraintSet.END, scoreTextView.getId(), ConstraintSet.START); constraintSet.connect(upvoteButton.getId(), ConstraintSet.START, placeholder.getId(), ConstraintSet.END); constraintSet.connect(scoreTextView.getId(), ConstraintSet.END, downvoteButton.getId(), ConstraintSet.START); constraintSet.connect(scoreTextView.getId(), ConstraintSet.START, upvoteButton.getId(), ConstraintSet.END); constraintSet.connect(downvoteButton.getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect(downvoteButton.getId(), ConstraintSet.START, scoreTextView.getId(), ConstraintSet.END); constraintSet.connect(placeholder.getId(), ConstraintSet.END, upvoteButton.getId(), ConstraintSet.START); constraintSet.connect(placeholder.getId(), ConstraintSet.START, moreButton.getId(), ConstraintSet.END); constraintSet.connect(moreButton.getId(), ConstraintSet.START, expandButton.getId(), ConstraintSet.END); constraintSet.connect(moreButton.getId(), ConstraintSet.END, placeholder.getId(), ConstraintSet.START); constraintSet.connect(expandButton.getId(), ConstraintSet.START, saveButton.getId(), ConstraintSet.END); constraintSet.connect(expandButton.getId(), ConstraintSet.END, moreButton.getId(), ConstraintSet.START); constraintSet.connect(saveButton.getId(), ConstraintSet.START, replyButton.getId(), ConstraintSet.END); constraintSet.connect(saveButton.getId(), ConstraintSet.END, expandButton.getId(), ConstraintSet.START); constraintSet.connect(replyButton.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.connect(replyButton.getId(), ConstraintSet.END, saveButton.getId(), ConstraintSet.START); constraintSet.applyTo(bottomConstraintLayout); } linearLayout.getLayoutTransition().setAnimateParentHierarchy(false); commentIndentationView.setVisibility(View.GONE); if (mShowCommentDivider) { commentDivider.setVisibility(View.VISIBLE); } if (mActivity.typeface != null) { authorTextView.setTypeface(mActivity.typeface); authorFlairTextView.setTypeface(mActivity.typeface); commentTimeTextView.setTypeface(mActivity.typeface); upvoteButton.setTypeface(mActivity.typeface); } itemView.setBackgroundColor(mCommentBackgroundColor); authorTextView.setTextColor(mUsernameColor); authorFlairTextView.setTextColor(mAuthorFlairColor); commentTimeTextView.setTextColor(mSecondaryTextColor); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); moreButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); saveButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); replyButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); commentDivider.setBackgroundColor(mDividerColor); authorTextView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Comment comment = getItem(getBindingAdapterPosition()); if (comment != null) { Intent intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, comment.getSubredditName()); mActivity.startActivity(intent); } }); moreButton.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Comment comment = getItem(getBindingAdapterPosition()); if (comment != null) { Bundle bundle = new Bundle(); if (comment.getAuthor().equals(mAccountName)) { bundle.putBoolean(CommentMoreBottomSheetFragment.EXTRA_EDIT_AND_DELETE_AVAILABLE, true); } bundle.putParcelable(CommentMoreBottomSheetFragment.EXTRA_COMMENT, comment); bundle.putInt(CommentMoreBottomSheetFragment.EXTRA_POSITION, getBindingAdapterPosition()); CommentMoreBottomSheetFragment commentMoreBottomSheetFragment = new CommentMoreBottomSheetFragment(); commentMoreBottomSheetFragment.setArguments(bundle); commentMoreBottomSheetFragment.show(mFragment.getChildFragmentManager(), commentMoreBottomSheetFragment.getTag()); } }); itemView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Comment comment = getItem(getBindingAdapterPosition()); if (comment != null) { Intent intent = new Intent(mActivity, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_ID, comment.getLinkId()); intent.putExtra(ViewPostDetailActivity.EXTRA_SINGLE_COMMENT_ID, comment.getId()); mActivity.startActivity(intent); } }); commentMarkdownView.setRecycledViewPool(recycledViewPool); LinearLayoutManagerBugFixed linearLayoutManager = new SwipeLockLinearLayoutManager(mActivity, new SwipeLockInterface() { @Override public void lockSwipe() { mActivity.lockSwipeRightToGoBack(); } @Override public void unlockSwipe() { mActivity.unlockSwipeRightToGoBack(); } }); commentMarkdownView.setLayoutManager(linearLayoutManager); markwonAdapter = MarkdownUtils.createCustomTablesAndImagesAdapter(mActivity, mImageAndGifEntry); markwonAdapter.setOnClickListener(view -> { if (view instanceof SpoilerOnClickTextView) { if (((SpoilerOnClickTextView) view).isSpoilerOnClick()) { ((SpoilerOnClickTextView) view).setSpoilerOnClick(false); return; } } if (canStartActivity) { canStartActivity = false; itemView.performClick(); } }); commentMarkdownView.setAdapter(markwonAdapter); upvoteButton.setOnClickListener(view -> { if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } int position = getBindingAdapterPosition(); if (position < 0) { return; } Comment comment = getItem(getBindingAdapterPosition()); if (comment != null) { int previousVoteType = comment.getVoteType(); String newVoteType; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (previousVoteType != Comment.VOTE_TYPE_UPVOTE) { //Not upvoted before comment.setVoteType(Comment.VOTE_TYPE_UPVOTE); newVoteType = APIUtils.DIR_UPVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); } else { //Upvoted before comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); newVoteType = APIUtils.DIR_UNVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); } if (!comment.isScoreHidden()) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); } VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingListener() { @Override public void onVoteThingSuccess(int position1) { int currentPosition = getBindingAdapterPosition(); if (newVoteType.equals(APIUtils.DIR_UPVOTE)) { comment.setVoteType(Comment.VOTE_TYPE_UPVOTE); if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); } } else { comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); } } if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (!comment.isScoreHidden()) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); } } } @Override public void onVoteThingFail(int position) { } }, comment.getFullName(), newVoteType, getBindingAdapterPosition()); } }); scoreTextView.setOnClickListener(view -> { upvoteButton.performClick(); }); downvoteButton.setOnClickListener(view -> { if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } int position = getBindingAdapterPosition(); if (position < 0) { return; } Comment comment = getItem(getBindingAdapterPosition()); if (comment != null) { int previousVoteType = comment.getVoteType(); String newVoteType; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (previousVoteType != Comment.VOTE_TYPE_DOWNVOTE) { //Not downvoted before comment.setVoteType(Comment.VOTE_TYPE_DOWNVOTE); newVoteType = APIUtils.DIR_DOWNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); } else { //Downvoted before comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); newVoteType = APIUtils.DIR_UNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); } if (!comment.isScoreHidden()) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); } VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingListener() { @Override public void onVoteThingSuccess(int position1) { int currentPosition = getBindingAdapterPosition(); if (newVoteType.equals(APIUtils.DIR_DOWNVOTE)) { comment.setVoteType(Comment.VOTE_TYPE_DOWNVOTE); if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); } } else { comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); } } if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (!comment.isScoreHidden()) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); } } } @Override public void onVoteThingFail(int position1) { } }, comment.getFullName(), newVoteType, getBindingAdapterPosition()); } }); saveButton.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Comment comment = getItem(position); if (comment != null) { if (comment.isSaved()) { comment.setSaved(false); SaveThing.unsaveThing(mOauthRetrofit, mAccessToken, comment.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { comment.setSaved(false); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } Toast.makeText(mActivity, R.string.comment_unsaved_success, Toast.LENGTH_SHORT).show(); } @Override public void failed() { comment.setSaved(true); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } Toast.makeText(mActivity, R.string.comment_unsaved_failed, Toast.LENGTH_SHORT).show(); } }); } else { comment.setSaved(true); SaveThing.saveThing(mOauthRetrofit, mAccessToken, comment.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { comment.setSaved(true); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } Toast.makeText(mActivity, R.string.comment_saved_success, Toast.LENGTH_SHORT).show(); } @Override public void failed() { comment.setSaved(false); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } Toast.makeText(mActivity, R.string.comment_saved_failed, Toast.LENGTH_SHORT).show(); } }); } } }); } } class CommentViewHolder extends CommentBaseViewHolder { ItemCommentBinding binding; CommentViewHolder(ItemCommentBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.linearLayoutItemComment, binding.authorTextViewItemPostComment, binding.authorFlairTextViewItemPostComment, binding.commentTimeTextViewItemPostComment, binding.commentMarkdownViewItemPostComment, binding.bottomConstraintLayoutItemPostComment, binding.upvoteButtonItemPostComment, binding.scoreTextViewItemPostComment, binding.downvoteButtonItemPostComment, binding.placeholderItemPostComment, binding.moreButtonItemPostComment, binding.saveButtonItemPostComment, binding.expandButtonItemPostComment, binding.replyButtonItemPostComment, binding.verticalBlockIndentationItemComment, binding.dividerItemComment); } } class ErrorViewHolder extends RecyclerView.ViewHolder { ErrorViewHolder(@NonNull ItemFooterErrorBinding binding) { super(binding.getRoot()); if (mActivity.typeface != null) { binding.errorTextViewItemFooterError.setTypeface(mActivity.typeface); binding.retryButtonItemFooterError.setTypeface(mActivity.typeface); } binding.errorTextViewItemFooterError.setText(R.string.load_comments_failed); binding.retryButtonItemFooterError.setOnClickListener(view -> mRetryLoadingMoreCallback.retryLoadingMore()); binding.errorTextViewItemFooterError.setTextColor(mSecondaryTextColor); binding.retryButtonItemFooterError.setBackgroundTintList(ColorStateList.valueOf(mColorPrimaryLightTheme)); binding.retryButtonItemFooterError.setTextColor(mButtonTextColor); } } class LoadingViewHolder extends RecyclerView.ViewHolder { LoadingViewHolder(@NonNull ItemFooterLoadingBinding binding) { super(binding.getRoot()); binding.progressBarItemFooterLoading.setIndicatorColor(mColorAccent); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.InsetDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.Spanned; 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 android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.button.MaterialButton; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.Executor; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CommentActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CommentMoreBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.comment.FetchComment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.CommentIndentationView; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView; import ml.docilealligator.infinityforreddit.customviews.SwipeLockInterface; import ml.docilealligator.infinityforreddit.customviews.SwipeLockLinearLayoutManager; import ml.docilealligator.infinityforreddit.databinding.ItemCommentBinding; import ml.docilealligator.infinityforreddit.databinding.ItemCommentFooterErrorBinding; import ml.docilealligator.infinityforreddit.databinding.ItemCommentFooterLoadingBinding; import ml.docilealligator.infinityforreddit.databinding.ItemCommentFullyCollapsedBinding; import ml.docilealligator.infinityforreddit.databinding.ItemLoadCommentsBinding; import ml.docilealligator.infinityforreddit.databinding.ItemLoadCommentsFailedPlaceholderBinding; import ml.docilealligator.infinityforreddit.databinding.ItemLoadMoreCommentsPlaceholderBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNoCommentPlaceholderBinding; import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment; import ml.docilealligator.infinityforreddit.markdown.CustomMarkwonAdapter; import ml.docilealligator.infinityforreddit.markdown.EmoteCloseBracketInlineProcessor; import ml.docilealligator.infinityforreddit.markdown.EmotePlugin; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.thing.SaveThing; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.VoteThing; import ml.docilealligator.infinityforreddit.user.UserProfileImagesBatchLoader; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter { public static final int DIVIDER_NORMAL = 0; public static final int DIVIDER_PARENT = 1; private static final int VIEW_TYPE_FIRST_LOADING = 9; private static final int VIEW_TYPE_FIRST_LOADING_FAILED = 10; private static final int VIEW_TYPE_NO_COMMENT_PLACEHOLDER = 11; private static final int VIEW_TYPE_COMMENT = 12; private static final int VIEW_TYPE_COMMENT_FULLY_COLLAPSED = 13; private static final int VIEW_TYPE_LOAD_MORE_CHILD_COMMENTS = 14; private static final int VIEW_TYPE_IS_LOADING_MORE_COMMENTS = 15; private static final int VIEW_TYPE_LOAD_MORE_COMMENTS_FAILED = 16; private static final int VIEW_TYPE_VIEW_ALL_COMMENTS = 17; private static final Object PAYLOAD_EXPANSION_CHANGED = new Object(); private final BaseActivity mActivity; private final ViewPostDetailFragment mFragment; private final Executor mExecutor; private final Retrofit mRetrofit; private final Retrofit mOauthRetrofit; private final EmoteCloseBracketInlineProcessor mEmoteCloseBracketInlineProcessor; private final EmotePlugin mEmotePlugin; private final ImageAndGifPlugin mImageAndGifPlugin; private final Markwon mCommentMarkwon; private final ImageAndGifEntry mImageAndGifEntry; private final String mAccessToken; private final String mAccountName; private final Post mPost; private final ArrayList mVisibleComments; private final Locale mLocale; private final RequestManager mGlide; private final RecyclerView.RecycledViewPool recycledViewPool; private String mSingleCommentId; private boolean mIsSingleCommentThreadMode; private final boolean mVoteButtonsOnTheRight; private final boolean mShowElapsedTime; private final String mTimeFormatPattern; private final boolean mExpandChildren; private final boolean mCommentToolbarHidden; private final boolean mCommentToolbarHideOnClick; private final boolean mSwapTapAndLong; private final boolean mShowCommentDivider; private final int mDividerType; private final boolean mShowAbsoluteNumberOfVotes; private final boolean mFullyCollapseComment; private final boolean mShowOnlyOneCommentLevelIndicator; private final boolean mShowAuthorAvatar; private final boolean mDisableProfileAvatarAnimation; private final boolean mShowUserPrefix; private final boolean mHideTheNumberOfVotes; private final int mDepthThreshold; private final CommentRecyclerViewAdapterCallback mCommentRecyclerViewAdapterCallback; private boolean isInitiallyLoading; private boolean isInitiallyLoadingFailed; private boolean mHasMoreComments; private boolean loadMoreCommentsFailed; private final Drawable expandDrawable; private final Drawable collapseDrawable; private final int mColorPrimaryLightTheme; private final int mColorAccent; private final int mCircularProgressBarBackgroundColor; private final int mSecondaryTextColor; private final int mPrimaryTextColor; private final int mCommentTextColor; private final int mCommentBackgroundColor; private final int mDividerColor; private final int mUsernameColor; private final int mSubmitterColor; private final int mModeratorColor; private final int mCurrentUserColor; private final int mAuthorFlairTextColor; private final int mUpvotedColor; private final int mDownvotedColor; private final int mSingleCommentThreadBackgroundColor; private final int mVoteAndReplyUnavailableVoteButtonColor; private final int mButtonTextColor; private final int mCommentIconAndInfoColor; private final int mFullyCollapsedCommentBackgroundColor; private final int[] verticalBlockColors; private int mSearchCommentIndex = -1; private boolean canStartActivity = true; public CommentsRecyclerViewAdapter(BaseActivity activity, ViewPostDetailFragment fragment, CustomThemeWrapper customThemeWrapper, Executor executor, Retrofit retrofit, Retrofit oauthRetrofit, @Nullable String accessToken, @NonNull String accountName, Post post, Locale locale, String singleCommentId, boolean isSingleCommentThreadMode, SharedPreferences sharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferences, CommentRecyclerViewAdapterCallback commentRecyclerViewAdapterCallback) { mActivity = activity; mFragment = fragment; mExecutor = executor; mRetrofit = retrofit; mOauthRetrofit = oauthRetrofit; mAccessToken = accessToken; mAccountName = accountName; mGlide = Glide.with(activity); mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); mCommentTextColor = customThemeWrapper.getCommentColor(); int commentSpoilerBackgroundColor = mCommentTextColor | 0xFF000000; int linkColor = customThemeWrapper.getLinkColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (mActivity.contentTypeface != null) { textView.setTypeface(mActivity.contentTypeface); } textView.setTextColor(mCommentTextColor); textView.setHighlightColor(Color.TRANSPARENT); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_IS_NSFW, mPost.isNSFW()); mActivity.startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(linkColor); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { if (!activity.isDestroyed() && !activity.isFinishing()) { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(activity.getSupportFragmentManager(), null); } return true; }; mEmoteCloseBracketInlineProcessor = new EmoteCloseBracketInlineProcessor(); mEmotePlugin = EmotePlugin.create(activity, Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.EMBEDDED_MEDIA_TYPE, "15")), mediaMetadata -> { Intent intent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); if (canStartActivity) { canStartActivity = false; activity.startActivity(intent); } }); mImageAndGifPlugin = new ImageAndGifPlugin(); mCommentMarkwon = MarkdownUtils.createFullRedditMarkwon(mActivity, miscPlugin, mEmoteCloseBracketInlineProcessor, mEmotePlugin, mImageAndGifPlugin, mCommentTextColor, commentSpoilerBackgroundColor, onLinkLongClickListener); boolean needBlurNsfw = nsfwAndSpoilerSharedPreferences.getBoolean((mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mAccountName) + SharedPreferencesUtils.BLUR_NSFW_BASE, true); boolean doNotBlurNsfwInNsfwSubreddits = nsfwAndSpoilerSharedPreferences.getBoolean((mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mAccountName) + SharedPreferencesUtils.DO_NOT_BLUR_NSFW_IN_NSFW_SUBREDDITS, false); boolean needBlurSpoiler = nsfwAndSpoilerSharedPreferences.getBoolean((mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mAccountName) + SharedPreferencesUtils.BLUR_SPOILER_BASE, false); boolean blurImage = (post.isNSFW() && needBlurNsfw && !(doNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (post.isSpoiler() && needBlurSpoiler); mImageAndGifEntry = new ImageAndGifEntry(activity, mGlide, Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.EMBEDDED_MEDIA_TYPE, "15")), blurImage, (mediaMetadata, commentId, postId) -> { Intent intent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); if (commentId != null && !commentId.isEmpty()) { intent.putExtra(ViewImageOrGifActivity.EXTRA_COMMENT_ID_KEY, commentId); } if (canStartActivity) { canStartActivity = false; activity.startActivity(intent); } }); recycledViewPool = new RecyclerView.RecycledViewPool(); mPost = post; mVisibleComments = new ArrayList<>(); mLocale = locale; mSingleCommentId = singleCommentId; mIsSingleCommentThreadMode = isSingleCommentThreadMode; mVoteButtonsOnTheRight = sharedPreferences.getBoolean(SharedPreferencesUtils.VOTE_BUTTONS_ON_THE_RIGHT_KEY, false); mShowElapsedTime = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ELAPSED_TIME_KEY, false); mTimeFormatPattern = sharedPreferences.getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE); mExpandChildren = !sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_TOP_LEVEL_COMMENTS_FIRST, false); mCommentToolbarHidden = sharedPreferences.getBoolean(SharedPreferencesUtils.COMMENT_TOOLBAR_HIDDEN, true); mCommentToolbarHideOnClick = sharedPreferences.getBoolean(SharedPreferencesUtils.COMMENT_TOOLBAR_HIDE_ON_CLICK, true); mSwapTapAndLong = sharedPreferences.getBoolean(SharedPreferencesUtils.SWAP_TAP_AND_LONG_COMMENTS, true); mShowCommentDivider = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_COMMENT_DIVIDER, false); mDividerType = Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.COMMENT_DIVIDER_TYPE, "0")); mShowAbsoluteNumberOfVotes = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ABSOLUTE_NUMBER_OF_VOTES, true); mFullyCollapseComment = sharedPreferences.getBoolean(SharedPreferencesUtils.FULLY_COLLAPSE_COMMENT, false); mShowOnlyOneCommentLevelIndicator = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ONLY_ONE_COMMENT_LEVEL_INDICATOR, false); mShowAuthorAvatar = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_AUTHOR_AVATAR, false); mDisableProfileAvatarAnimation = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_PROFILE_AVATAR_ANIMATION, false); mShowUserPrefix = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_USER_PREFIX, false); mHideTheNumberOfVotes = sharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_VOTES_IN_COMMENTS, false); mDepthThreshold = sharedPreferences.getInt(SharedPreferencesUtils.SHOW_FEWER_TOOLBAR_OPTIONS_THRESHOLD, 5); mCommentRecyclerViewAdapterCallback = commentRecyclerViewAdapterCallback; isInitiallyLoading = true; isInitiallyLoadingFailed = false; mHasMoreComments = false; loadMoreCommentsFailed = false; expandDrawable = Utils.getTintedDrawable(activity, R.drawable.ic_expand_more_grey_24dp, customThemeWrapper.getCommentIconAndInfoColor()); collapseDrawable = Utils.getTintedDrawable(activity, R.drawable.ic_expand_less_grey_24dp, customThemeWrapper.getCommentIconAndInfoColor()); mColorPrimaryLightTheme = customThemeWrapper.getColorPrimaryLightTheme(); mColorAccent = customThemeWrapper.getColorAccent(); mCircularProgressBarBackgroundColor = customThemeWrapper.getCircularProgressBarBackground(); mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); mDividerColor = customThemeWrapper.getDividerColor(); mCommentBackgroundColor = customThemeWrapper.getCommentBackgroundColor(); mSubmitterColor = customThemeWrapper.getSubmitter(); mModeratorColor = customThemeWrapper.getModerator(); mCurrentUserColor = customThemeWrapper.getCurrentUser(); mAuthorFlairTextColor = customThemeWrapper.getAuthorFlairTextColor(); mUsernameColor = customThemeWrapper.getUsername(); mUpvotedColor = customThemeWrapper.getUpvoted(); mDownvotedColor = customThemeWrapper.getDownvoted(); mSingleCommentThreadBackgroundColor = customThemeWrapper.getSingleCommentThreadBackgroundColor(); mVoteAndReplyUnavailableVoteButtonColor = customThemeWrapper.getVoteAndReplyUnavailableButtonColor(); mButtonTextColor = customThemeWrapper.getButtonTextColor(); mCommentIconAndInfoColor = customThemeWrapper.getCommentIconAndInfoColor(); mFullyCollapsedCommentBackgroundColor = customThemeWrapper.getFullyCollapsedCommentBackgroundColor(); verticalBlockColors = new int[] { customThemeWrapper.getCommentVerticalBarColor1(), customThemeWrapper.getCommentVerticalBarColor2(), customThemeWrapper.getCommentVerticalBarColor3(), customThemeWrapper.getCommentVerticalBarColor4(), customThemeWrapper.getCommentVerticalBarColor5(), customThemeWrapper.getCommentVerticalBarColor6(), customThemeWrapper.getCommentVerticalBarColor7(), }; } @Override public int getItemViewType(int position) { if (mVisibleComments.isEmpty()) { if (isInitiallyLoading) { return VIEW_TYPE_FIRST_LOADING; } else if (isInitiallyLoadingFailed) { if(mIsSingleCommentThreadMode && position == 0) { return VIEW_TYPE_VIEW_ALL_COMMENTS; } return VIEW_TYPE_FIRST_LOADING_FAILED; } else { if(mIsSingleCommentThreadMode && position == 0) { return VIEW_TYPE_VIEW_ALL_COMMENTS; } return VIEW_TYPE_NO_COMMENT_PLACEHOLDER; } } if (mIsSingleCommentThreadMode) { if (position == 0) { return VIEW_TYPE_VIEW_ALL_COMMENTS; } if (position == mVisibleComments.size() + 1) { if (mHasMoreComments) { return VIEW_TYPE_IS_LOADING_MORE_COMMENTS; } else { return VIEW_TYPE_LOAD_MORE_COMMENTS_FAILED; } } Comment comment = mVisibleComments.get(position - 1); if (comment.getPlaceholderType() == Comment.NOT_PLACEHOLDER) { if ((mFullyCollapseComment && !comment.isExpanded() && comment.hasExpandedBefore()) || (comment.isFilteredOut() && !comment.hasExpandedBefore())) { return VIEW_TYPE_COMMENT_FULLY_COLLAPSED; } return VIEW_TYPE_COMMENT; } else { return VIEW_TYPE_LOAD_MORE_CHILD_COMMENTS; } } else { if (position == mVisibleComments.size()) { if (mHasMoreComments) { return VIEW_TYPE_IS_LOADING_MORE_COMMENTS; } else { return VIEW_TYPE_LOAD_MORE_COMMENTS_FAILED; } } Comment comment = mVisibleComments.get(position); if (comment.getPlaceholderType() == Comment.NOT_PLACEHOLDER) { if ((mFullyCollapseComment && !comment.isExpanded() && comment.hasExpandedBefore()) || (comment.isFilteredOut() && !comment.hasExpandedBefore())) { return VIEW_TYPE_COMMENT_FULLY_COLLAPSED; } return VIEW_TYPE_COMMENT; } else { return VIEW_TYPE_LOAD_MORE_CHILD_COMMENTS; } } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_FIRST_LOADING: return new LoadCommentsViewHolder(ItemLoadCommentsBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_FIRST_LOADING_FAILED: return new LoadCommentsFailedViewHolder(ItemLoadCommentsFailedPlaceholderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_NO_COMMENT_PLACEHOLDER: return new NoCommentViewHolder(ItemNoCommentPlaceholderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_COMMENT: return new CommentViewHolder(ItemCommentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_COMMENT_FULLY_COLLAPSED: return new CommentFullyCollapsedViewHolder(ItemCommentFullyCollapsedBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_LOAD_MORE_CHILD_COMMENTS: return new LoadMoreChildCommentsViewHolder(ItemLoadMoreCommentsPlaceholderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_IS_LOADING_MORE_COMMENTS: return new IsLoadingMoreCommentsViewHolder(ItemCommentFooterLoadingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_LOAD_MORE_COMMENTS_FAILED: return new LoadMoreCommentsFailedViewHolder(ItemCommentFooterErrorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); default: return new ViewAllCommentsViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view_all_comments, parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof CommentBaseViewHolder) { Comment comment = getCurrentComment(position); if (comment != null) { if (mIsSingleCommentThreadMode && comment.getId().equals(mSingleCommentId)) { holder.itemView.setBackgroundColor(mSingleCommentThreadBackgroundColor); } String authorText = comment.getAuthor(); if (mShowUserPrefix) { //adding prefix authorText = "u/" + authorText; } ((CommentBaseViewHolder) holder).authorTextView.setText(authorText); if (comment.getAuthorFlairHTML() != null && !comment.getAuthorFlairHTML().equals("")) { ((CommentBaseViewHolder) holder).authorFlairTextView.setVisibility(View.VISIBLE); Utils.setHTMLWithImageToTextView(((CommentBaseViewHolder) holder).authorFlairTextView, comment.getAuthorFlairHTML(), true); } else if (comment.getAuthorFlair() != null && !comment.getAuthorFlair().equals("")) { ((CommentBaseViewHolder) holder).authorFlairTextView.setVisibility(View.VISIBLE); ((CommentBaseViewHolder) holder).authorFlairTextView.setText(comment.getAuthorFlair()); } if (comment.isSubmitter()) { ((CommentBaseViewHolder) holder).authorTextView.setTextColor(mSubmitterColor); Drawable submitterDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_mic_14dp, mSubmitterColor); ((CommentBaseViewHolder) holder).authorTextView.setCompoundDrawablesWithIntrinsicBounds( submitterDrawable, null, null, null); } else if (comment.isModerator()) { ((CommentBaseViewHolder) holder).authorTextView.setTextColor(mModeratorColor); Drawable moderatorDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_verified_user_14dp, mModeratorColor); ((CommentBaseViewHolder) holder).authorTextView.setCompoundDrawablesWithIntrinsicBounds( moderatorDrawable, null, null, null); } else if (comment.getAuthor().equals(mAccountName)) { ((CommentBaseViewHolder) holder).authorTextView.setTextColor(mCurrentUserColor); Drawable currentUserDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_current_user_14dp, mCurrentUserColor); ((CommentBaseViewHolder) holder).authorTextView.setCompoundDrawablesWithIntrinsicBounds( currentUserDrawable, null, null, null); } if (mShowAuthorAvatar) { if (comment.getAuthorIconUrl() == null) { int startIndex = translatePositionToCommentIndex(position); if (startIndex >= 0) { List commentBatch = mVisibleComments.subList(startIndex, Math.min(mVisibleComments.size(), UserProfileImagesBatchLoader.BATCH_SIZE + startIndex)); mFragment.loadIcon(commentBatch, (authorFullName, iconUrl) -> { if (authorFullName.equals(comment.getAuthorFullName())) { comment.setAuthorIconUrl(iconUrl); } Comment currentComment = getCurrentComment(holder); if (currentComment != null && authorFullName.equals(currentComment.getAuthorFullName())) { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentBaseViewHolder) holder).authorIconImageView); } else { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentBaseViewHolder) holder).authorIconImageView); } } }); } } else { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(comment.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentBaseViewHolder) holder).authorIconImageView); } else { mGlide.load(comment.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentBaseViewHolder) holder).authorIconImageView); } } } if (mShowElapsedTime) { ((CommentBaseViewHolder) holder).commentTimeTextView.setText( Utils.getElapsedTime(mActivity, comment.getCommentTimeMillis())); } else { ((CommentBaseViewHolder) holder).commentTimeTextView.setText(Utils.getFormattedTime(mLocale, comment.getCommentTimeMillis(), mTimeFormatPattern)); } if (mCommentToolbarHidden) { ((CommentBaseViewHolder) holder).bottomConstraintLayout.getLayoutParams().height = 0; if (!mHideTheNumberOfVotes) { ((CommentBaseViewHolder) holder).topScoreTextView.setVisibility(View.VISIBLE); } } else { ((CommentBaseViewHolder) holder).bottomConstraintLayout.getLayoutParams().height = LinearLayout.LayoutParams.WRAP_CONTENT; ((CommentBaseViewHolder) holder).topScoreTextView.setVisibility(View.GONE); } mEmoteCloseBracketInlineProcessor.setMediaMetadataMap(comment.getMediaMetadataMap()); mImageAndGifPlugin.setMediaMetadataMap(comment.getMediaMetadataMap()); mImageAndGifEntry.setCurrentCommentId(comment.getId()); mImageAndGifEntry.setCurrentPostId(mPost.getId()); ((CommentBaseViewHolder) holder).mMarkwonAdapter.setMarkdown(mCommentMarkwon, comment.getCommentMarkdown()); // noinspection NotifyDataSetChanged ((CommentBaseViewHolder) holder).mMarkwonAdapter.notifyDataSetChanged(); if (!mHideTheNumberOfVotes) { String commentText = ""; String topScoreText = ""; if (comment.isScoreHidden()) { commentText = mActivity.getString(R.string.hidden); topScoreText = mActivity.getString(R.string.hidden); ((CommentBaseViewHolder) holder).scoreTextView.setTextColor(mCommentIconAndInfoColor); // Use default color when hidden ((CommentBaseViewHolder) holder).topScoreTextView.setTextColor(mSecondaryTextColor); } else { commentText = Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType()); topScoreText = mActivity.getString(R.string.top_score, Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); // Set score text color based on vote type int scoreColor = mCommentIconAndInfoColor; int topScoreColor = mSecondaryTextColor; switch (comment.getVoteType()) { case Comment.VOTE_TYPE_UPVOTE: scoreColor = mUpvotedColor; topScoreColor = mUpvotedColor; break; case Comment.VOTE_TYPE_DOWNVOTE: scoreColor = mDownvotedColor; topScoreColor = mDownvotedColor; break; } if (mPost.isArchived()) { // Archived posts override vote color scoreColor = mVoteAndReplyUnavailableVoteButtonColor; topScoreColor = mVoteAndReplyUnavailableVoteButtonColor; } ((CommentBaseViewHolder) holder).scoreTextView.setTextColor(scoreColor); ((CommentBaseViewHolder) holder).topScoreTextView.setTextColor(topScoreColor); } ((CommentBaseViewHolder) holder).scoreTextView.setText(commentText); // Set text without prefix ((CommentBaseViewHolder) holder).topScoreTextView.setText(topScoreText); // Set text without prefix // Handle Child Count Views // Restore original visibility logic (to be applied next) if (comment.hasReply() && comment.getChildCount() > 0) { String childCountString = "+" + comment.getChildCount(); ((CommentBaseViewHolder) holder).topChildCountTextView.setText(childCountString); // Set visibility for both views based on collapsed state boolean showCount = !comment.isExpanded(); ((CommentBaseViewHolder) holder).topChildCountTextView.setVisibility(showCount ? View.VISIBLE : View.INVISIBLE); } else { ((CommentBaseViewHolder) holder).topChildCountTextView.setVisibility(View.INVISIBLE); } } else { ((CommentBaseViewHolder) holder).scoreTextView.setText(mActivity.getString(R.string.vote)); ((CommentBaseViewHolder) holder).topScoreTextView.setText(""); // Hide top score if all votes hidden ((CommentBaseViewHolder) holder).topChildCountTextView.setVisibility(View.INVISIBLE); // Hide child count too } if (comment.isEdited()) { ((CommentBaseViewHolder) holder).editedTextView.setVisibility(View.VISIBLE); } else { ((CommentBaseViewHolder) holder).editedTextView.setVisibility(View.GONE); } ((CommentBaseViewHolder) holder).commentIndentationView.setShowOnlyOneDivider(mShowOnlyOneCommentLevelIndicator); ((CommentBaseViewHolder) holder).commentIndentationView.setLevelAndColors(comment.getDepth(), verticalBlockColors); if (comment.getDepth() >= mDepthThreshold) { ((CommentBaseViewHolder) holder).saveButton.setVisibility(View.GONE); ((CommentBaseViewHolder) holder).replyButton.setVisibility(View.GONE); } else { ((CommentBaseViewHolder) holder).saveButton.setVisibility(View.VISIBLE); ((CommentBaseViewHolder) holder).replyButton.setVisibility(View.VISIBLE); } if (comment.hasReply()) { if (comment.isExpanded()) { ((CommentBaseViewHolder) holder).expandButton.setCompoundDrawablesWithIntrinsicBounds(collapseDrawable, null, null, null); } else { ((CommentBaseViewHolder) holder).expandButton.setCompoundDrawablesWithIntrinsicBounds(expandDrawable, null, null, null); } ((CommentBaseViewHolder) holder).expandButton.setVisibility(View.VISIBLE); } switch (comment.getVoteType()) { case Comment.VOTE_TYPE_UPVOTE: ((CommentBaseViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); ((CommentBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); break; case Comment.VOTE_TYPE_DOWNVOTE: ((CommentBaseViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); ((CommentBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); break; } if (mPost.isArchived()) { ((CommentBaseViewHolder) holder).replyButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); ((CommentBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); ((CommentBaseViewHolder) holder).scoreTextView.setTextColor(mVoteAndReplyUnavailableVoteButtonColor); ((CommentBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); } if (mPost.isLocked() || comment.isLocked()) { ((CommentBaseViewHolder) holder).replyButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); } if (comment.isSaved()) { ((CommentBaseViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } else { ((CommentBaseViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } if (position == mSearchCommentIndex) { holder.itemView.setBackgroundColor(Color.parseColor("#03A9F4")); } if (mShowCommentDivider) { if (mDividerType == DIVIDER_PARENT && comment.getDepth() == 0) { RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams(); params.setMargins(0, (int) Utils.convertDpToPixel(16, mActivity), 0, 0); } } } } else if (holder instanceof CommentFullyCollapsedViewHolder) { Comment comment = getCurrentComment(position); if (comment != null) { String authorText = comment.getAuthor(); if (mShowUserPrefix) { //adding prefix authorText = "u/" + authorText; } ((CommentFullyCollapsedViewHolder) holder).binding.userNameTextViewItemCommentFullyCollapsed.setText(authorText); if (mShowAuthorAvatar) { if (comment.getAuthorIconUrl() == null) { int startIndex = translatePositionToCommentIndex(position); if (startIndex >= 0) { List commentBatch = mVisibleComments.subList(startIndex, Math.min(mVisibleComments.size(), UserProfileImagesBatchLoader.BATCH_SIZE + startIndex)); mFragment.loadIcon(commentBatch, (authorFullName, iconUrl) -> { if (authorFullName.equals(comment.getAuthorFullName())) { comment.setAuthorIconUrl(iconUrl); } Comment currentComment = getCurrentComment(holder); if (currentComment != null && authorFullName.equals(currentComment.getAuthorFullName())) { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentFullyCollapsedViewHolder) holder).binding.authorIconImageViewItemCommentFullyCollapsed); } else { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentFullyCollapsedViewHolder) holder).binding.authorIconImageViewItemCommentFullyCollapsed); } } }); } } else { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(comment.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentFullyCollapsedViewHolder) holder).binding.authorIconImageViewItemCommentFullyCollapsed); } else { mGlide.load(comment.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((CommentFullyCollapsedViewHolder) holder).binding.authorIconImageViewItemCommentFullyCollapsed); } } } if (comment.getChildCount() > 0) { ((CommentFullyCollapsedViewHolder) holder).binding.childCountTextViewItemCommentFullyCollapsed.setVisibility(View.VISIBLE); ((CommentFullyCollapsedViewHolder) holder).binding.childCountTextViewItemCommentFullyCollapsed.setText("+" + comment.getChildCount()); } else { ((CommentFullyCollapsedViewHolder) holder).binding.childCountTextViewItemCommentFullyCollapsed.setVisibility(View.GONE); } if (mShowElapsedTime) { ((CommentFullyCollapsedViewHolder) holder).binding.timeTextViewItemCommentFullyCollapsed.setText(Utils.getElapsedTime(mActivity, comment.getCommentTimeMillis())); } else { ((CommentFullyCollapsedViewHolder) holder).binding.timeTextViewItemCommentFullyCollapsed.setText(Utils.getFormattedTime(mLocale, comment.getCommentTimeMillis(), mTimeFormatPattern)); } if (!comment.isScoreHidden() && !mHideTheNumberOfVotes) { String scoreText = mActivity.getString(R.string.top_score, Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); if (comment.getChildCount() > 0) { scoreText = "+" + comment.getChildCount() + " | " + scoreText; } ((CommentFullyCollapsedViewHolder) holder).binding.scoreTextViewItemCommentFullyCollapsed.setText(scoreText); } else if (mHideTheNumberOfVotes) { ((CommentFullyCollapsedViewHolder) holder).binding.scoreTextViewItemCommentFullyCollapsed.setText(mActivity.getString(R.string.vote)); } else { ((CommentFullyCollapsedViewHolder) holder).binding.scoreTextViewItemCommentFullyCollapsed.setText(mActivity.getString(R.string.hidden)); } ((CommentFullyCollapsedViewHolder) holder).binding.verticalBlockIndentationItemCommentFullyCollapsed.setShowOnlyOneDivider(mShowOnlyOneCommentLevelIndicator); ((CommentFullyCollapsedViewHolder) holder).binding.verticalBlockIndentationItemCommentFullyCollapsed.setLevelAndColors(comment.getDepth(), verticalBlockColors); if (mShowCommentDivider) { if (mDividerType == DIVIDER_PARENT && comment.getDepth() == 0) { RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams(); params.setMargins(0, (int) Utils.convertDpToPixel(16, mActivity), 0, 0); } } } } else if (holder instanceof LoadMoreChildCommentsViewHolder) { Comment placeholder; placeholder = mIsSingleCommentThreadMode ? mVisibleComments.get(holder.getBindingAdapterPosition() - 1) : mVisibleComments.get(holder.getBindingAdapterPosition()); ((LoadMoreChildCommentsViewHolder) holder).binding.verticalBlockIndentationItemLoadMoreCommentsPlaceholder.setShowOnlyOneDivider(mShowOnlyOneCommentLevelIndicator); ((LoadMoreChildCommentsViewHolder) holder).binding.verticalBlockIndentationItemLoadMoreCommentsPlaceholder.setLevelAndColors(placeholder.getDepth(), verticalBlockColors); if (placeholder.getPlaceholderType() == Comment.PLACEHOLDER_LOAD_MORE_COMMENTS) { if (placeholder.isLoadingMoreChildren()) { ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.loading); } else if (placeholder.isLoadMoreChildrenFailed()) { ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.comment_load_more_comments_failed); } else { ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.comment_load_more_comments); } } else { ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.comment_continue_thread); } if (placeholder.getPlaceholderType() == Comment.PLACEHOLDER_LOAD_MORE_COMMENTS) { ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setOnClickListener(view -> { int commentPosition = mIsSingleCommentThreadMode ? holder.getBindingAdapterPosition() - 1 : holder.getBindingAdapterPosition(); int parentPosition = getParentPosition(commentPosition); if (parentPosition >= 0) { Comment parentComment = mVisibleComments.get(parentPosition); mVisibleComments.get(commentPosition).setLoadingMoreChildren(true); mVisibleComments.get(commentPosition).setLoadMoreChildrenFailed(false); ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.loading); Retrofit retrofit = mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit; SortType.Type sortType = mCommentRecyclerViewAdapterCallback.getSortType(); FetchComment.fetchMoreComment(mExecutor, new Handler(), retrofit, mAccessToken, mAccountName, parentComment.getMoreChildrenIds(), mExpandChildren, mPost.getFullName(), sortType, new FetchComment.FetchMoreCommentListener() { @Override public void onFetchMoreCommentSuccess(ArrayList topLevelComments, ArrayList expandedComments, ArrayList moreChildrenIds) { if (mVisibleComments.size() > parentPosition && parentComment.getFullName().equals(mVisibleComments.get(parentPosition).getFullName())) { if (mVisibleComments.get(parentPosition).isExpanded()) { if (!moreChildrenIds.isEmpty()) { mVisibleComments.get(parentPosition).setMoreChildrenIds(moreChildrenIds); mVisibleComments.get(parentPosition).getChildren().get(mVisibleComments.get(parentPosition).getChildren().size() - 1) .setLoadingMoreChildren(false); mVisibleComments.get(parentPosition).getChildren().get(mVisibleComments.get(parentPosition).getChildren().size() - 1) .setLoadMoreChildrenFailed(false); int placeholderPosition = findLoadMoreCommentsPlaceholderPosition(parentComment.getFullName(), commentPosition); if (placeholderPosition != -1) { mVisibleComments.get(placeholderPosition).setLoadingMoreChildren(false); mVisibleComments.get(placeholderPosition).setLoadMoreChildrenFailed(false); ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.comment_load_more_comments); mVisibleComments.addAll(placeholderPosition, expandedComments); if (mIsSingleCommentThreadMode) { notifyItemRangeInserted(placeholderPosition + 1, expandedComments.size()); } else { notifyItemRangeInserted(placeholderPosition, expandedComments.size()); } } } else { mVisibleComments.get(parentPosition).getChildren() .remove(mVisibleComments.get(parentPosition).getChildren().size() - 1); mVisibleComments.get(parentPosition).removeMoreChildrenIds(); int placeholderPosition = findLoadMoreCommentsPlaceholderPosition(parentComment.getFullName(), commentPosition); if (placeholderPosition != -1) { mVisibleComments.remove(placeholderPosition); if (mIsSingleCommentThreadMode) { notifyItemRemoved(placeholderPosition + 1); } else { notifyItemRemoved(placeholderPosition); } mVisibleComments.addAll(placeholderPosition, expandedComments); if (mIsSingleCommentThreadMode) { notifyItemRangeInserted(placeholderPosition + 1, expandedComments.size()); } else { notifyItemRangeInserted(placeholderPosition, expandedComments.size()); } } } } else { if (mVisibleComments.get(parentPosition).hasReply() && moreChildrenIds.isEmpty()) { mVisibleComments.get(parentPosition).getChildren() .remove(mVisibleComments.get(parentPosition).getChildren().size() - 1); mVisibleComments.get(parentPosition).removeMoreChildrenIds(); } } mVisibleComments.get(parentPosition).addChildren(topLevelComments); if (mIsSingleCommentThreadMode) { notifyItemChanged(parentPosition + 1); } else { notifyItemChanged(parentPosition); } } else { for (int i = 0; i < mVisibleComments.size(); i++) { if (mVisibleComments.get(i).getFullName().equals(parentComment.getFullName())) { if (mVisibleComments.get(i).isExpanded()) { int placeholderPositionHint = i + mVisibleComments.get(i).getChildren().size(); int placeholderPosition = findLoadMoreCommentsPlaceholderPosition(parentComment.getFullName(), placeholderPositionHint); if (placeholderPosition != -1) { mVisibleComments.get(placeholderPosition).setLoadingMoreChildren(false); mVisibleComments.get(placeholderPosition).setLoadMoreChildrenFailed(false); ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.comment_load_more_comments); mVisibleComments.addAll(placeholderPosition, expandedComments); if (mIsSingleCommentThreadMode) { notifyItemRangeInserted(placeholderPosition + 1, expandedComments.size()); } else { notifyItemRangeInserted(placeholderPosition, expandedComments.size()); } } } mVisibleComments.get(i).getChildren().get(mVisibleComments.get(i).getChildren().size() - 1) .setLoadingMoreChildren(false); mVisibleComments.get(i).getChildren().get(mVisibleComments.get(i).getChildren().size() - 1) .setLoadMoreChildrenFailed(false); mVisibleComments.get(i).addChildren(topLevelComments); if (mIsSingleCommentThreadMode) { notifyItemChanged(i + 1); } else { notifyItemChanged(i); } break; } } } } @Override public void onFetchMoreCommentFailed() { int currentParentPosition = findCommentPosition(parentComment.getFullName(), parentPosition); if (currentParentPosition == -1) { // note: returning here is probably a mistake, because // parent is just not visible, but it can still exist in the comments tree. return; } Comment currentParentComment = mVisibleComments.get(currentParentPosition); if (currentParentComment.isExpanded()) { int placeholderPositionHint = currentParentPosition + currentParentComment.getChildren().size(); int placeholderPosition = findLoadMoreCommentsPlaceholderPosition(parentComment.getFullName(), placeholderPositionHint); if (placeholderPosition != -1) { mVisibleComments.get(placeholderPosition).setLoadingMoreChildren(false); mVisibleComments.get(placeholderPosition).setLoadMoreChildrenFailed(true); } ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setText(R.string.comment_load_more_comments_failed); } currentParentComment.getChildren().get(currentParentComment.getChildren().size() - 1) .setLoadingMoreChildren(false); currentParentComment.getChildren().get(currentParentComment.getChildren().size() - 1) .setLoadMoreChildrenFailed(true); } }); } }); } else { ((LoadMoreChildCommentsViewHolder) holder).binding.placeholderTextViewItemLoadMoreComments.setOnClickListener(view -> { Comment comment = getCurrentComment(holder); if (comment != null) { Intent intent = new Intent(mActivity, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_DATA, mPost); intent.putExtra(ViewPostDetailActivity.EXTRA_SINGLE_COMMENT_ID, comment.getParentId()); intent.putExtra(ViewPostDetailActivity.EXTRA_CONTEXT_NUMBER, "0"); mActivity.startActivity(intent); } }); } } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { if (!payloads.isEmpty() && payloads.contains(PAYLOAD_EXPANSION_CHANGED) && holder instanceof CommentBaseViewHolder) { // Partial bind: Only update expansion-related views Comment comment = getCurrentComment(position); CommentBaseViewHolder baseViewHolder = (CommentBaseViewHolder) holder; if (comment != null) { // Update expand button drawable if (comment.hasReply()) { baseViewHolder.expandButton.setCompoundDrawablesWithIntrinsicBounds( comment.isExpanded() ? collapseDrawable : expandDrawable, null, null, null); baseViewHolder.expandButton.setVisibility(View.VISIBLE); } else { baseViewHolder.expandButton.setVisibility(View.GONE); } // Update child count visibility if (!mHideTheNumberOfVotes && comment.hasReply() && comment.getChildCount() > 0) { boolean showCount = !comment.isExpanded(); baseViewHolder.topChildCountTextView.setVisibility(showCount ? View.VISIBLE : View.INVISIBLE); // Need to re-check toolbar hidden state for top count if (mCommentToolbarHidden) { boolean shouldShowTopChildCount = showCount; baseViewHolder.topChildCountTextView.setVisibility(shouldShowTopChildCount ? View.VISIBLE : View.INVISIBLE); } else { baseViewHolder.topChildCountTextView.setVisibility(View.INVISIBLE); } } else { baseViewHolder.topChildCountTextView.setVisibility(View.INVISIBLE); } } } else { // Full bind: Call the existing two-argument version onBindViewHolder(holder, position); } } public void setCanStartActivity(boolean canStartActivity) { this.canStartActivity = canStartActivity; } @Nullable private Comment getCurrentComment(RecyclerView.ViewHolder holder) { return getCurrentComment(holder.getBindingAdapterPosition()); } @Nullable private Comment getCurrentComment(int position) { if (mIsSingleCommentThreadMode) { if (position - 1 >= 0 && position - 1 < mVisibleComments.size()) { return mVisibleComments.get(position - 1); } } else { if (position >= 0 && position < mVisibleComments.size()) { return mVisibleComments.get(position); } } return null; } private int translatePositionToCommentIndex(int position) { if (mIsSingleCommentThreadMode) { if (position - 1 >= 0 && position - 1 < mVisibleComments.size()) { return position - 1; } } else { if (position >= 0 && position < mVisibleComments.size()) { return position; } } return -1; } private int getParentPosition(int position) { if (position >= 0 && position < mVisibleComments.size()) { int childDepth = mVisibleComments.get(position).getDepth(); for (int i = position; i >= 0; i--) { if (mVisibleComments.get(i).getDepth() < childDepth) { return i; } } } return -1; } /** * Find position of comment with given {@code fullName} and * {@link Comment#NOT_PLACEHOLDER} placeholder type * @return position of the placeholder or -1 if not found */ private int findCommentPosition(String fullName, int positionHint) { return findCommentPosition(fullName, positionHint, Comment.NOT_PLACEHOLDER); } private int findCommentPosition(String fullName, int positionHint, int placeholderType) { if (0 <= positionHint && positionHint < mVisibleComments.size() && mVisibleComments.get(positionHint).getFullName().equals(fullName) && mVisibleComments.get(positionHint).getPlaceholderType() == placeholderType) { return positionHint; } for (int i = 0; i < mVisibleComments.size(); i++) { Comment comment = mVisibleComments.get(i); if (comment.getFullName().equals(fullName) && comment.getPlaceholderType() == placeholderType) { return i; } } return -1; } /** * Find position of comment with given {@code fullName} and * {@link Comment#PLACEHOLDER_LOAD_MORE_COMMENTS} placeholder type * @return position of the placeholder or -1 if not found */ private int findLoadMoreCommentsPlaceholderPosition(String fullName, int positionHint) { return findCommentPosition(fullName, positionHint, Comment.PLACEHOLDER_LOAD_MORE_COMMENTS); } private void expandChildren(ArrayList comments, ArrayList newList) { if (comments != null && !comments.isEmpty()) { for (Comment comment : comments) { newList.add(comment); expandChildren(comment.getChildren(), newList); comment.setExpanded(true); } } } private void collapseChildren(int position) { mVisibleComments.get(position).setExpanded(false); int depth = mVisibleComments.get(position).getDepth(); int allChildrenSize = 0; for (int i = position + 1; i < mVisibleComments.size(); i++) { if (mVisibleComments.get(i).getDepth() > depth) { allChildrenSize++; } else { break; } } if (allChildrenSize > 0) { mVisibleComments.subList(position + 1, position + 1 + allChildrenSize).clear(); } if (mIsSingleCommentThreadMode) { notifyItemRangeRemoved(position + 2, allChildrenSize); if (mFullyCollapseComment) { notifyItemChanged(position + 1); } } else { notifyItemRangeRemoved(position + 1, allChildrenSize); if (mFullyCollapseComment) { notifyItemChanged(position); } } } public void addComments(@NonNull ArrayList comments, boolean hasMoreComments) { if (mVisibleComments.isEmpty()) { isInitiallyLoading = false; isInitiallyLoadingFailed = false; if (comments.isEmpty() || mIsSingleCommentThreadMode) { notifyItemChanged(0); } else { notifyItemRemoved(0); } } int sizeBefore = mVisibleComments.size(); mVisibleComments.addAll(comments); if (mIsSingleCommentThreadMode) { notifyItemRangeInserted(sizeBefore + 1, comments.size()); } else { notifyItemRangeInserted(sizeBefore, comments.size()); } if (mHasMoreComments != hasMoreComments) { if (hasMoreComments) { if (mIsSingleCommentThreadMode) { notifyItemInserted(mVisibleComments.size() + 1); } else { notifyItemInserted(mVisibleComments.size()); } } else { if (mIsSingleCommentThreadMode) { notifyItemRemoved(mVisibleComments.size() + 1); } else { notifyItemRemoved(mVisibleComments.size()); } } } mHasMoreComments = hasMoreComments; } public void addComment(Comment comment) { if (mVisibleComments.size() == 0 || isInitiallyLoadingFailed) { notifyItemRemoved(1); } mVisibleComments.add(0, comment); if (isInitiallyLoading) { notifyItemInserted(1); } else { notifyItemInserted(0); } } public void addChildComment(Comment comment, String parentFullname, int parentPosition) { if (!parentFullname.equals(mVisibleComments.get(parentPosition).getFullName())) { for (int i = 0; i < mVisibleComments.size(); i++) { if (parentFullname.equals(mVisibleComments.get(i).getFullName())) { parentPosition = i; break; } } } mVisibleComments.get(parentPosition).addChild(comment); mVisibleComments.get(parentPosition).setHasReply(true); if (!mVisibleComments.get(parentPosition).isExpanded()) { ArrayList newList = new ArrayList<>(); expandChildren(mVisibleComments.get(parentPosition).getChildren(), newList); mVisibleComments.get(parentPosition).setExpanded(true); mVisibleComments.addAll(parentPosition + 1, newList); if (mIsSingleCommentThreadMode) { notifyItemChanged(parentPosition + 1); notifyItemRangeInserted(parentPosition + 2, newList.size()); } else { notifyItemChanged(parentPosition); notifyItemRangeInserted(parentPosition + 1, newList.size()); } } else { mVisibleComments.add(parentPosition + 1, comment); if (mIsSingleCommentThreadMode) { notifyItemChanged(parentPosition + 1); notifyItemInserted(parentPosition + 2); } else { notifyItemChanged(parentPosition); notifyItemInserted(parentPosition + 1); } } } public void setSingleComment(String singleCommentId, boolean isSingleCommentThreadMode) { mSingleCommentId = singleCommentId; mIsSingleCommentThreadMode = isSingleCommentThreadMode; } public ArrayList getVisibleComments() { return mVisibleComments; } public void initiallyLoading() { resetCommentSearchIndex(); int removedItemCount = getItemCount(); mVisibleComments.clear(); notifyItemRangeRemoved(0, removedItemCount); isInitiallyLoading = true; isInitiallyLoadingFailed = false; notifyItemInserted(0); } public void initiallyLoadCommentsFailed() { isInitiallyLoading = false; isInitiallyLoadingFailed = true; notifyItemChanged(0); } public void loadMoreCommentsFailed() { loadMoreCommentsFailed = true; if (mIsSingleCommentThreadMode) { notifyItemChanged(mVisibleComments.size() + 1); } else { notifyItemChanged(mVisibleComments.size()); } } public void editComment(Comment comment, int position) { if (position < mVisibleComments.size() && position >= 0) { Comment oldComment = mVisibleComments.get(position); if (oldComment.getId().equals(comment.getId())) { oldComment.setCommentMarkdown(comment.getCommentMarkdown()); oldComment.setMediaMetadataMap(comment.getMediaMetadataMap()); if (mIsSingleCommentThreadMode) { notifyItemChanged(position + 1); } else { notifyItemChanged(position); } } } } public void editComment(String commentContentMarkdown, int position) { if (position < mVisibleComments.size() && position >= 0) { mVisibleComments.get(position).setCommentMarkdown(commentContentMarkdown); if (mIsSingleCommentThreadMode) { notifyItemChanged(position + 1); } else { notifyItemChanged(position); } } } public void deleteComment(int position) { if (mVisibleComments != null && position >= 0 && position < mVisibleComments.size()) { if (mVisibleComments.get(position).hasReply()) { mVisibleComments.get(position).setAuthor("[deleted]"); mVisibleComments.get(position).setCommentMarkdown("[deleted]"); if (mIsSingleCommentThreadMode) { notifyItemChanged(position + 1); } else { notifyItemChanged(position); } } else { mVisibleComments.remove(position); if (mIsSingleCommentThreadMode) { notifyItemRemoved(position + 1); } else { notifyItemRemoved(position); } } } } public void toggleReplyNotifications(String fullName, int position) { if (mVisibleComments != null && position >= 0 && position < mVisibleComments.size()) { if (mVisibleComments.get(position).getFullName().equals(fullName)) { mVisibleComments.get(position).toggleSendReplies(); } } //TODO The comment's position may change } public void updateModdedStatus(Comment comment, int position) { Comment originalComment = getCurrentComment(position); if (originalComment != null && originalComment.getFullName().equals(comment.getFullName())) { originalComment.setApproved(comment.isApproved()); originalComment.setApprovedAtUTC(comment.getApprovedAtUTC()); originalComment.setApprovedBy(comment.getApprovedBy()); originalComment.setRemoved(comment.isRemoved(), comment.isSpam()); originalComment.setLocked(comment.isLocked()); if (mIsSingleCommentThreadMode) { notifyItemChanged(position + 1); } else { notifyItemChanged(position); } } else { for (int i = 0; i < mVisibleComments.size(); i++) { Comment currentComment = mVisibleComments.get(i); if (currentComment.getFullName().equals(comment.getFullName()) && currentComment.getPlaceholderType() == comment.getPlaceholderType()) { currentComment.setApproved(comment.isApproved()); currentComment.setApprovedAtUTC(comment.getApprovedAtUTC()); currentComment.setApprovedBy(comment.getApprovedBy()); currentComment.setRemoved(comment.isRemoved(), comment.isSpam()); currentComment.setLocked(comment.isLocked()); if (mIsSingleCommentThreadMode) { notifyItemChanged(i + 1); } else { notifyItemChanged(i); } } } } } public int getNextParentCommentPosition(int currentPosition) { if (mVisibleComments != null && !mVisibleComments.isEmpty()) { if (mIsSingleCommentThreadMode) { for (int i = currentPosition + 1; i - 1 < mVisibleComments.size() && i - 1 >= 0; i++) { if (mVisibleComments.get(i - 1).getDepth() == 0) { return i + 1; } } } else { for (int i = currentPosition + 1; i < mVisibleComments.size(); i++) { if (mVisibleComments.get(i).getDepth() == 0) { return i; } } } } return -1; } public int getPreviousParentCommentPosition(int currentPosition) { if (mVisibleComments != null && !mVisibleComments.isEmpty()) { if (mIsSingleCommentThreadMode) { for (int i = currentPosition - 1; i - 1 >= 0; i--) { if (mVisibleComments.get(i - 1).getDepth() == 0) { return i + 1; } } } else { for (int i = currentPosition - 1; i >= 0; i--) { if (mVisibleComments.get(i).getDepth() == 0) { return i; } } } } return -1; } public int getParentCommentPosition(int currentPosition, int currentDepth) { if (mVisibleComments != null && !mVisibleComments.isEmpty()) { if (mIsSingleCommentThreadMode) { for (int i = currentPosition - 1; i - 1 >= 0; i--) { if (mVisibleComments.get(i - 1).getDepth() == currentDepth - 1) { return i + 1; } } } else { for (int i = currentPosition - 1; i >= 0; i--) { if (mVisibleComments.get(i).getDepth() == currentDepth - 1) { return i; } } } } return -1; } public void onItemSwipe(RecyclerView.ViewHolder viewHolder, int direction, int swipeLeftAction, int swipeRightAction) { if (viewHolder instanceof CommentBaseViewHolder) { if (direction == ItemTouchHelper.LEFT || direction == ItemTouchHelper.START) { if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((CommentBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((CommentBaseViewHolder) viewHolder).downvoteButton.performClick(); } } else { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((CommentBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((CommentBaseViewHolder) viewHolder).downvoteButton.performClick(); } } } } public void setSaveComment(int position, boolean isSaved) { Comment comment = getCurrentComment(position); if (comment != null) { comment.setSaved(isSaved); } } public int getSearchCommentIndex() { return mSearchCommentIndex; } public void highlightSearchResult(int searchCommentIndex) { mSearchCommentIndex = searchCommentIndex; } public void resetCommentSearchIndex() { mSearchCommentIndex = -1; } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof CommentBaseViewHolder) { holder.itemView.setBackgroundColor(mCommentBackgroundColor); ((CommentBaseViewHolder) holder).authorTextView.setTextColor(mUsernameColor); ((CommentBaseViewHolder) holder).authorFlairTextView.setVisibility(View.GONE); ((CommentBaseViewHolder) holder).authorTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); mGlide.clear(((CommentBaseViewHolder) holder).authorIconImageView); ((CommentBaseViewHolder) holder).topScoreTextView.setTextColor(mSecondaryTextColor); ((CommentBaseViewHolder) holder).expandButton.setVisibility(View.GONE); ((CommentBaseViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); ((CommentBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); ((CommentBaseViewHolder) holder).scoreTextView.setTextColor(mCommentIconAndInfoColor); ((CommentBaseViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); ((CommentBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); ((CommentBaseViewHolder) holder).expandButton.setText(""); ((CommentBaseViewHolder) holder).replyButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams(); params.setMargins(0, 0, 0, 0); } } @Override public int getItemCount() { if (isInitiallyLoading) { return 1; } if (isInitiallyLoadingFailed || mVisibleComments.isEmpty()) { return mIsSingleCommentThreadMode ? 2 : 1; } if (mHasMoreComments || loadMoreCommentsFailed) { if (mIsSingleCommentThreadMode) { return mVisibleComments.size() + 2; } else { return mVisibleComments.size() + 1; } } if (mIsSingleCommentThreadMode) { return mVisibleComments.size() + 1; } else { return mVisibleComments.size(); } } public void setDataSavingMode(boolean dataSavingMode) { mEmotePlugin.setDataSavingMode(dataSavingMode); mImageAndGifEntry.setDataSavingMode(dataSavingMode); } public interface CommentRecyclerViewAdapterCallback { void retryFetchingComments(); void retryFetchingMoreComments(); SortType.Type getSortType(); } public class CommentBaseViewHolder extends RecyclerView.ViewHolder { LinearLayout linearLayout; ImageView authorIconImageView; TextView authorTextView; TextView authorFlairTextView; TextView commentTimeTextView; TextView topScoreTextView; TextView topChildCountTextView; RecyclerView commentMarkdownView; TextView editedTextView; ConstraintLayout bottomConstraintLayout; MaterialButton upvoteButton; TextView scoreTextView; MaterialButton downvoteButton; View placeholder; MaterialButton moreButton; MaterialButton saveButton; TextView expandButton; MaterialButton replyButton; CommentIndentationView commentIndentationView; View commentDivider; CustomMarkwonAdapter mMarkwonAdapter; CommentBaseViewHolder(@NonNull View itemView) { super(itemView); } void setBaseView(LinearLayout linearLayout, ImageView authorIconImageView, TextView authorTextView, TextView authorFlairTextView, TextView commentTimeTextView, TextView topScoreTextView, TextView topChildCountTextView, // Pass new view RecyclerView commentMarkdownView, TextView editedTextView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, View placeholder, MaterialButton moreButton, MaterialButton saveButton, TextView expandButton, MaterialButton replyButton, CommentIndentationView commentIndentationView, View commentDivider) { this.linearLayout = linearLayout; this.authorIconImageView = authorIconImageView; this.authorTextView = authorTextView; this.authorFlairTextView = authorFlairTextView; this.commentTimeTextView = commentTimeTextView; this.topScoreTextView = topScoreTextView; this.topChildCountTextView = topChildCountTextView; this.commentMarkdownView = commentMarkdownView; this.editedTextView = editedTextView; this.bottomConstraintLayout = bottomConstraintLayout; this.upvoteButton = upvoteButton; this.scoreTextView = scoreTextView; this.downvoteButton = downvoteButton; this.placeholder = placeholder; this.moreButton = moreButton; this.saveButton = saveButton; this.expandButton = expandButton; this.replyButton = replyButton; this.commentIndentationView = commentIndentationView; this.commentDivider = commentDivider; if (mVoteButtonsOnTheRight) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(bottomConstraintLayout); constraintSet.clear(upvoteButton.getId(), ConstraintSet.START); constraintSet.clear(upvoteButton.getId(), ConstraintSet.END); constraintSet.clear(scoreTextView.getId(), ConstraintSet.START); constraintSet.clear(scoreTextView.getId(), ConstraintSet.END); constraintSet.clear(downvoteButton.getId(), ConstraintSet.START); constraintSet.clear(downvoteButton.getId(), ConstraintSet.END); constraintSet.clear(expandButton.getId(), ConstraintSet.START); constraintSet.clear(expandButton.getId(), ConstraintSet.END); constraintSet.clear(saveButton.getId(), ConstraintSet.START); constraintSet.clear(saveButton.getId(), ConstraintSet.END); constraintSet.clear(replyButton.getId(), ConstraintSet.START); constraintSet.clear(replyButton.getId(), ConstraintSet.END); constraintSet.clear(moreButton.getId(), ConstraintSet.START); constraintSet.clear(moreButton.getId(), ConstraintSet.END); constraintSet.connect(upvoteButton.getId(), ConstraintSet.END, scoreTextView.getId(), ConstraintSet.START); constraintSet.connect(upvoteButton.getId(), ConstraintSet.START, placeholder.getId(), ConstraintSet.END); constraintSet.connect(scoreTextView.getId(), ConstraintSet.END, downvoteButton.getId(), ConstraintSet.START); constraintSet.connect(scoreTextView.getId(), ConstraintSet.START, upvoteButton.getId(), ConstraintSet.END); constraintSet.connect(downvoteButton.getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect(downvoteButton.getId(), ConstraintSet.START, scoreTextView.getId(), ConstraintSet.END); constraintSet.connect(placeholder.getId(), ConstraintSet.END, upvoteButton.getId(), ConstraintSet.START); constraintSet.connect(placeholder.getId(), ConstraintSet.START, moreButton.getId(), ConstraintSet.END); constraintSet.connect(moreButton.getId(), ConstraintSet.START, expandButton.getId(), ConstraintSet.END); constraintSet.connect(moreButton.getId(), ConstraintSet.END, placeholder.getId(), ConstraintSet.START); constraintSet.connect(expandButton.getId(), ConstraintSet.START, saveButton.getId(), ConstraintSet.END); constraintSet.connect(expandButton.getId(), ConstraintSet.END, moreButton.getId(), ConstraintSet.START); constraintSet.connect(saveButton.getId(), ConstraintSet.START, replyButton.getId(), ConstraintSet.END); constraintSet.connect(saveButton.getId(), ConstraintSet.END, expandButton.getId(), ConstraintSet.START); constraintSet.connect(replyButton.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.connect(replyButton.getId(), ConstraintSet.END, saveButton.getId(), ConstraintSet.START); constraintSet.applyTo(bottomConstraintLayout); } if (linearLayout.getLayoutTransition() != null) { linearLayout.getLayoutTransition().setAnimateParentHierarchy(false); } if (mShowCommentDivider) { if (mDividerType == DIVIDER_NORMAL) { commentDivider.setBackgroundColor(mDividerColor); commentDivider.setVisibility(View.VISIBLE); } } if (mActivity.typeface != null) { authorTextView.setTypeface(mActivity.typeface); commentTimeTextView.setTypeface(mActivity.typeface); authorFlairTextView.setTypeface(mActivity.typeface); topScoreTextView.setTypeface(mActivity.typeface); editedTextView.setTypeface(mActivity.typeface); scoreTextView.setTypeface(mActivity.typeface); expandButton.setTypeface(mActivity.typeface); } if (mShowAuthorAvatar) { authorIconImageView.setVisibility(View.VISIBLE); } else { ((ConstraintLayout.LayoutParams) authorTextView.getLayoutParams()).leftMargin = 0; ((ConstraintLayout.LayoutParams) authorFlairTextView.getLayoutParams()).leftMargin = 0; } commentMarkdownView.setRecycledViewPool(recycledViewPool); LinearLayoutManagerBugFixed linearLayoutManager = new SwipeLockLinearLayoutManager(mActivity, new SwipeLockInterface() { @Override public void lockSwipe() { mActivity.lockSwipeRightToGoBack(); } @Override public void unlockSwipe() { mActivity.unlockSwipeRightToGoBack(); } }); commentMarkdownView.setLayoutManager(linearLayoutManager); mMarkwonAdapter = MarkdownUtils.createCustomTablesAndImagesAdapter(mActivity, mImageAndGifEntry); commentMarkdownView.setAdapter(mMarkwonAdapter); // Create rounded corner background GradientDrawable childCountBackgroundDrawable = new GradientDrawable(); childCountBackgroundDrawable.setShape(GradientDrawable.RECTANGLE); childCountBackgroundDrawable.setCornerRadius(Utils.convertDpToPixel(8, mActivity)); // 8dp radius childCountBackgroundDrawable.setColor(mUsernameColor); // Calculate padding int horizontalPadding = (int) Utils.convertDpToPixel(4, mActivity); // 4dp int verticalPadding = (int) Utils.convertDpToPixel(2, mActivity); // 2dp itemView.setBackgroundColor(mCommentBackgroundColor); authorTextView.setTextColor(mUsernameColor); commentTimeTextView.setTextColor(mSecondaryTextColor); authorFlairTextView.setTextColor(mAuthorFlairTextColor); editedTextView.setTextColor(mSecondaryTextColor); commentDivider.setBackgroundColor(mDividerColor); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); moreButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); expandButton.setTextColor(mCommentIconAndInfoColor); saveButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); replyButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); // Apply background and padding to top child count view with inset int insetPx = (int) Utils.convertDpToPixel(1, mActivity); // 1dp inset InsetDrawable insetChildCountBackground = new InsetDrawable(childCountBackgroundDrawable, insetPx); topChildCountTextView.setBackground(insetChildCountBackground); topChildCountTextView.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); topChildCountTextView.setTextColor(mCommentBackgroundColor); // Set text color for contrast // Restore default text colors for score views (will be set in onBindViewHolder based on vote) scoreTextView.setTextColor(mCommentIconAndInfoColor); topScoreTextView.setTextColor(mSecondaryTextColor); authorFlairTextView.setOnClickListener(view -> authorTextView.performClick()); editedTextView.setOnClickListener(view -> { Comment comment = getCurrentComment(this); if (comment != null) { Toast.makeText(view.getContext(), view.getContext().getString(R.string.edited_time, mShowElapsedTime ? Utils.getElapsedTime(mActivity, comment.getEditedTimeMillis()) : Utils.getFormattedTime(mLocale, comment.getEditedTimeMillis(), mTimeFormatPattern) ), Toast.LENGTH_SHORT).show(); } }); moreButton.setOnClickListener(view -> { Comment comment = getCurrentComment(this); if (comment != null) { Bundle bundle = new Bundle(); if (!mPost.isArchived() && !mPost.isLocked() && comment.getAuthor().equals(mAccountName)) { bundle.putBoolean(CommentMoreBottomSheetFragment.EXTRA_EDIT_AND_DELETE_AVAILABLE, true); } bundle.putParcelable(CommentMoreBottomSheetFragment.EXTRA_COMMENT, comment); if (mIsSingleCommentThreadMode) { bundle.putInt(CommentMoreBottomSheetFragment.EXTRA_POSITION, getBindingAdapterPosition() - 1); } else { bundle.putInt(CommentMoreBottomSheetFragment.EXTRA_POSITION, getBindingAdapterPosition()); } bundle.putBoolean(CommentMoreBottomSheetFragment.EXTRA_IS_NSFW, mPost.isNSFW()); if (comment.getDepth() >= mDepthThreshold) { bundle.putBoolean(CommentMoreBottomSheetFragment.EXTRA_SHOW_REPLY_AND_SAVE_OPTION, true); } bundle.putParcelable(CommentMoreBottomSheetFragment.EXTRA_POST, mPost); int commentPos = mIsSingleCommentThreadMode ? getBindingAdapterPosition() - 1 : getBindingAdapterPosition(); ArrayList thread = new ArrayList<>(); thread.add(comment); for (int i = commentPos + 1; i < mVisibleComments.size() && thread.size() < 10; i++) { Comment child = mVisibleComments.get(i); if (child.getDepth() <= comment.getDepth()) break; thread.add(child); } bundle.putParcelableArrayList(CommentMoreBottomSheetFragment.EXTRA_THREAD_COMMENTS, thread); CommentMoreBottomSheetFragment commentMoreBottomSheetFragment = new CommentMoreBottomSheetFragment(); commentMoreBottomSheetFragment.setArguments(bundle); commentMoreBottomSheetFragment.show(mFragment.getChildFragmentManager(), commentMoreBottomSheetFragment.getTag()); } }); replyButton.setOnClickListener(view -> { if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } if (mPost.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_reply_unavailable, Toast.LENGTH_SHORT).show(); return; } if (mPost.isLocked()) { Toast.makeText(mActivity, R.string.locked_post_reply_unavailable, Toast.LENGTH_SHORT).show(); return; } Comment comment = getCurrentComment(this); if (comment != null) { if (comment.isLocked()) { Toast.makeText(mActivity, R.string.locked_comment_reply_unavailable, Toast.LENGTH_SHORT).show(); return; } Intent intent = new Intent(mActivity, CommentActivity.class); intent.putExtra(CommentActivity.EXTRA_PARENT_DEPTH_KEY, comment.getDepth() + 1); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_MARKDOWN_KEY, comment.getCommentMarkdown()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_KEY, comment.getCommentRawText()); intent.putExtra(CommentActivity.EXTRA_PARENT_FULLNAME_KEY, comment.getFullName()); intent.putExtra(CommentActivity.EXTRA_SUBREDDIT_NAME_KEY, mPost.getSubredditName()); intent.putExtra(CommentActivity.EXTRA_IS_REPLYING_KEY, true); int parentPosition = mIsSingleCommentThreadMode ? getBindingAdapterPosition() - 1 : getBindingAdapterPosition(); intent.putExtra(CommentActivity.EXTRA_PARENT_POSITION_KEY, parentPosition); mFragment.startActivityForResult(intent, CommentActivity.WRITE_COMMENT_REQUEST_CODE); } }); upvoteButton.setOnClickListener(view -> { if (mPost.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_vote_unavailable, Toast.LENGTH_SHORT).show(); return; } if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } Comment comment = getCurrentComment(this); if (comment != null) { int previousVoteType = comment.getVoteType(); String newVoteType; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (previousVoteType != Comment.VOTE_TYPE_UPVOTE) { //Not upvoted before comment.setVoteType(Comment.VOTE_TYPE_UPVOTE); newVoteType = APIUtils.DIR_UPVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); topScoreTextView.setTextColor(mUpvotedColor); } else { //Upvoted before comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); newVoteType = APIUtils.DIR_UNVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); topScoreTextView.setTextColor(mSecondaryTextColor); } if (!comment.isScoreHidden() && !mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); topScoreTextView.setText(mActivity.getString(R.string.top_score, Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType()))); } VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingListener() { @Override public void onVoteThingSuccess(int position) { int currentPosition = getBindingAdapterPosition(); if (newVoteType.equals(APIUtils.DIR_UPVOTE)) { comment.setVoteType(Comment.VOTE_TYPE_UPVOTE); if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); topScoreTextView.setTextColor(mUpvotedColor); } } else { comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); topScoreTextView.setTextColor(mSecondaryTextColor); } } if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (!comment.isScoreHidden() && !mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); topScoreTextView.setText(mActivity.getString(R.string.top_score, Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType()))); } } } @Override public void onVoteThingFail(int position) { } }, comment.getFullName(), newVoteType, getBindingAdapterPosition()); } }); scoreTextView.setOnClickListener(view -> { upvoteButton.performClick(); }); downvoteButton.setOnClickListener(view -> { if (mPost.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_vote_unavailable, Toast.LENGTH_SHORT).show(); return; } if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } Comment comment = getCurrentComment(this); if (comment != null) { int previousVoteType = comment.getVoteType(); String newVoteType; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (previousVoteType != Comment.VOTE_TYPE_DOWNVOTE) { //Not downvoted before comment.setVoteType(Comment.VOTE_TYPE_DOWNVOTE); newVoteType = APIUtils.DIR_DOWNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); topScoreTextView.setTextColor(mDownvotedColor); } else { //Downvoted before comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); newVoteType = APIUtils.DIR_UNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); topScoreTextView.setTextColor(mSecondaryTextColor); } if (!comment.isScoreHidden() && !mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); topScoreTextView.setText(mActivity.getString(R.string.top_score, Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType()))); } int position = getBindingAdapterPosition(); VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingListener() { @Override public void onVoteThingSuccess(int position1) { int currentPosition = getBindingAdapterPosition(); if (newVoteType.equals(APIUtils.DIR_DOWNVOTE)) { comment.setVoteType(Comment.VOTE_TYPE_DOWNVOTE); if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); topScoreTextView.setTextColor(mDownvotedColor); } } else { comment.setVoteType(Comment.VOTE_TYPE_NO_VOTE); if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); scoreTextView.setTextColor(mCommentIconAndInfoColor); topScoreTextView.setTextColor(mSecondaryTextColor); } } if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mCommentIconAndInfoColor)); if (!comment.isScoreHidden() && !mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType())); topScoreTextView.setText(mActivity.getString(R.string.top_score, Utils.getNVotes(mShowAbsoluteNumberOfVotes, comment.getScore() + comment.getVoteType()))); } } } @Override public void onVoteThingFail(int position1) { } }, comment.getFullName(), newVoteType, getBindingAdapterPosition()); } }); saveButton.setOnClickListener(view -> { Comment comment = getCurrentComment(this); if (comment != null) { int position = getBindingAdapterPosition(); if (comment.isSaved()) { comment.setSaved(false); SaveThing.unsaveThing(mOauthRetrofit, mAccessToken, comment.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { comment.setSaved(false); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } Toast.makeText(mActivity, R.string.comment_unsaved_success, Toast.LENGTH_SHORT).show(); } @Override public void failed() { comment.setSaved(true); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } Toast.makeText(mActivity, R.string.comment_unsaved_failed, Toast.LENGTH_SHORT).show(); } }); } else { comment.setSaved(true); SaveThing.saveThing(mOauthRetrofit, mAccessToken, comment.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { comment.setSaved(true); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } Toast.makeText(mActivity, R.string.comment_saved_success, Toast.LENGTH_SHORT).show(); } @Override public void failed() { comment.setSaved(false); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } Toast.makeText(mActivity, R.string.comment_saved_failed, Toast.LENGTH_SHORT).show(); } }); } } }); authorTextView.setOnClickListener(view -> { Comment comment = getCurrentComment(this); if (comment == null || comment.isAuthorDeleted()) { return; } Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, comment.getAuthor()); mActivity.startActivity(intent); }); authorIconImageView.setOnClickListener(view -> { authorTextView.performClick(); }); expandButton.setOnClickListener(view -> { if (expandButton.getVisibility() == View.VISIBLE) { int commentPosition = mIsSingleCommentThreadMode ? getBindingAdapterPosition() - 1 : getBindingAdapterPosition(); Comment comment = getCurrentComment(this); if (comment != null) { if (mVisibleComments.get(commentPosition).isExpanded()) { collapseChildren(commentPosition); // Notify that the parent item itself changed (its expanded state), using payload notifyItemChanged(mIsSingleCommentThreadMode ? commentPosition + 1 : commentPosition, PAYLOAD_EXPANSION_CHANGED); if (comment.getChildCount() > 0) { expandButton.setText("+" + comment.getChildCount()); } expandButton.setCompoundDrawablesWithIntrinsicBounds(expandDrawable, null, null, null); } else { comment.setExpanded(true); ArrayList newList = new ArrayList<>(); expandChildren(mVisibleComments.get(commentPosition).getChildren(), newList); mVisibleComments.get(commentPosition).setExpanded(true); mVisibleComments.addAll(commentPosition + 1, newList); if (mIsSingleCommentThreadMode) { notifyItemRangeInserted(commentPosition + 2, newList.size()); } else { notifyItemRangeInserted(commentPosition + 1, newList.size()); } // Notify that the parent item itself changed (its expanded state), using payload notifyItemChanged(mIsSingleCommentThreadMode ? commentPosition + 1 : commentPosition, PAYLOAD_EXPANSION_CHANGED); expandButton.setText(""); expandButton.setCompoundDrawablesWithIntrinsicBounds(collapseDrawable, null, null, null); } } } else if (mFullyCollapseComment) { int commentPosition = mIsSingleCommentThreadMode ? getBindingAdapterPosition() - 1 : getBindingAdapterPosition(); if (commentPosition >= 0 && commentPosition < mVisibleComments.size()) { collapseChildren(commentPosition); } } }); if (mSwapTapAndLong) { if (mCommentToolbarHideOnClick) { View.OnLongClickListener hideToolbarOnLongClickListener = view -> hideToolbar(); itemView.setOnLongClickListener(hideToolbarOnLongClickListener); commentTimeTextView.setOnLongClickListener(hideToolbarOnLongClickListener); mMarkwonAdapter.setOnLongClickListener(v -> { if (v instanceof TextView) { if (((TextView) v).getSelectionStart() == -1 && ((TextView) v).getSelectionEnd() == -1) { hideToolbar(); } } return true; }); } mMarkwonAdapter.setOnClickListener(v -> { if (v instanceof SpoilerOnClickTextView) { if (((SpoilerOnClickTextView) v).isSpoilerOnClick()) { ((SpoilerOnClickTextView) v).setSpoilerOnClick(false); return; } } expandComments(); }); itemView.setOnClickListener(view -> expandComments()); } else { if (mCommentToolbarHideOnClick) { mMarkwonAdapter.setOnClickListener(view -> { if (view instanceof SpoilerOnClickTextView) { if (((SpoilerOnClickTextView) view).isSpoilerOnClick()) { ((SpoilerOnClickTextView) view).setSpoilerOnClick(false); return; } } hideToolbar(); }); View.OnClickListener hideToolbarOnClickListener = view -> hideToolbar(); itemView.setOnClickListener(hideToolbarOnClickListener); commentTimeTextView.setOnClickListener(hideToolbarOnClickListener); } mMarkwonAdapter.setOnLongClickListener(view -> { if (view instanceof TextView) { if (((TextView) view).getSelectionStart() == -1 && ((TextView) view).getSelectionEnd() == -1) { expandComments(); } } return true; }); itemView.setOnLongClickListener(view -> { expandComments(); return true; }); } } private boolean expandComments() { expandButton.performClick(); return true; } private boolean hideToolbar() { if (bottomConstraintLayout.getLayoutParams().height == 0) { bottomConstraintLayout.getLayoutParams().height = LinearLayout.LayoutParams.WRAP_CONTENT; topScoreTextView.setVisibility(View.GONE); mFragment.delayTransition(); } else { mFragment.delayTransition(); bottomConstraintLayout.getLayoutParams().height = 0; if (!mHideTheNumberOfVotes) { topScoreTextView.setVisibility(View.VISIBLE); } } return true; } } class CommentViewHolder extends CommentBaseViewHolder { ItemCommentBinding binding; CommentViewHolder(ItemCommentBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.linearLayoutItemComment, binding.authorIconImageViewItemPostComment, binding.authorTextViewItemPostComment, binding.authorFlairTextViewItemPostComment, binding.commentTimeTextViewItemPostComment, binding.topScoreTextViewItemPostComment, binding.topChildCountTextViewItemPostComment, // Pass new view binding.commentMarkdownViewItemPostComment, binding.editedTextViewItemPostComment, binding.bottomConstraintLayoutItemPostComment, binding.upvoteButtonItemPostComment, binding.scoreTextViewItemPostComment, binding.downvoteButtonItemPostComment, binding.placeholderItemPostComment, binding.moreButtonItemPostComment, binding.saveButtonItemPostComment, binding.expandButtonItemPostComment, binding.replyButtonItemPostComment, binding.verticalBlockIndentationItemComment, binding.dividerItemComment); } } class CommentFullyCollapsedViewHolder extends RecyclerView.ViewHolder { ItemCommentFullyCollapsedBinding binding; public CommentFullyCollapsedViewHolder(@NonNull ItemCommentFullyCollapsedBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.userNameTextViewItemCommentFullyCollapsed.setTypeface(mActivity.typeface); binding.childCountTextViewItemCommentFullyCollapsed.setTypeface(mActivity.typeface); binding.scoreTextViewItemCommentFullyCollapsed.setTypeface(mActivity.typeface); binding.timeTextViewItemCommentFullyCollapsed.setTypeface(mActivity.typeface); } itemView.setBackgroundColor(mFullyCollapsedCommentBackgroundColor); binding.userNameTextViewItemCommentFullyCollapsed.setTextColor(mUsernameColor); binding.childCountTextViewItemCommentFullyCollapsed.setTextColor(mSecondaryTextColor); binding.scoreTextViewItemCommentFullyCollapsed.setTextColor(mSecondaryTextColor); binding.timeTextViewItemCommentFullyCollapsed.setTextColor(mSecondaryTextColor); if (mShowCommentDivider) { if (mDividerType == DIVIDER_NORMAL) { binding.dividerItemCommentFullyCollapsed.setBackgroundColor(mDividerColor); binding.dividerItemCommentFullyCollapsed.setVisibility(View.VISIBLE); } } if (mShowAuthorAvatar) { binding.authorIconImageViewItemCommentFullyCollapsed.setVisibility(View.VISIBLE); } else { binding.userNameTextViewItemCommentFullyCollapsed.setPaddingRelative(0, binding.userNameTextViewItemCommentFullyCollapsed.getPaddingTop(), binding.userNameTextViewItemCommentFullyCollapsed.getPaddingEnd(), binding.userNameTextViewItemCommentFullyCollapsed.getPaddingBottom()); } itemView.setOnClickListener(view -> { int commentPosition = mIsSingleCommentThreadMode ? getBindingAdapterPosition() - 1 : getBindingAdapterPosition(); if (commentPosition >= 0 && commentPosition < mVisibleComments.size()) { Comment comment = getCurrentComment(this); if (comment != null) { comment.setExpanded(true); ArrayList newList = new ArrayList<>(); expandChildren(mVisibleComments.get(commentPosition).getChildren(), newList); mVisibleComments.get(commentPosition).setExpanded(true); mVisibleComments.addAll(commentPosition + 1, newList); if (mIsSingleCommentThreadMode) { notifyItemChanged(commentPosition + 1); notifyItemRangeInserted(commentPosition + 2, newList.size()); } else { notifyItemChanged(commentPosition); notifyItemRangeInserted(commentPosition + 1, newList.size()); } } } }); itemView.setOnLongClickListener(view -> { itemView.performClick(); return true; }); } } class LoadMoreChildCommentsViewHolder extends RecyclerView.ViewHolder { ItemLoadMoreCommentsPlaceholderBinding binding; LoadMoreChildCommentsViewHolder(@NonNull ItemLoadMoreCommentsPlaceholderBinding binding) { super(binding.getRoot()); this.binding = binding; if (mShowCommentDivider) { if (mDividerType == DIVIDER_NORMAL) { binding.dividerItemLoadMoreCommentsPlaceholder.setBackgroundColor(mDividerColor); binding.dividerItemLoadMoreCommentsPlaceholder.setVisibility(View.VISIBLE); } } if (mActivity.typeface != null) { binding.placeholderTextViewItemLoadMoreComments.setTypeface(mActivity.typeface); } itemView.setBackgroundColor(mCommentBackgroundColor); binding.placeholderTextViewItemLoadMoreComments.setTextColor(mPrimaryTextColor); } } class LoadCommentsViewHolder extends RecyclerView.ViewHolder { ItemLoadCommentsBinding binding; LoadCommentsViewHolder(@NonNull ItemLoadCommentsBinding binding) { super(binding.getRoot()); this.binding = binding; binding.commentProgressBarItemLoadComments.setBackgroundTintList(ColorStateList.valueOf(mCircularProgressBarBackgroundColor)); binding.commentProgressBarItemLoadComments.setColorSchemeColors(mColorAccent); } } class LoadCommentsFailedViewHolder extends RecyclerView.ViewHolder { ItemLoadCommentsFailedPlaceholderBinding binding; LoadCommentsFailedViewHolder(@NonNull ItemLoadCommentsFailedPlaceholderBinding binding) { super(binding.getRoot()); this.binding = binding; itemView.setOnClickListener(view -> mCommentRecyclerViewAdapterCallback.retryFetchingComments()); if (mActivity.typeface != null) { binding.errorTextViewItemLoadCommentsFailedPlaceholder.setTypeface(mActivity.typeface); } binding.errorTextViewItemLoadCommentsFailedPlaceholder.setTextColor(mSecondaryTextColor); } } class NoCommentViewHolder extends RecyclerView.ViewHolder { ItemNoCommentPlaceholderBinding binding; NoCommentViewHolder(@NonNull ItemNoCommentPlaceholderBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.errorTextViewItemNoCommentPlaceholder.setTypeface(mActivity.typeface); } binding.errorTextViewItemNoCommentPlaceholder.setTextColor(mSecondaryTextColor); } } class IsLoadingMoreCommentsViewHolder extends RecyclerView.ViewHolder { ItemCommentFooterLoadingBinding binding; IsLoadingMoreCommentsViewHolder(@NonNull ItemCommentFooterLoadingBinding binding) { super(binding.getRoot()); this.binding = binding; binding.progressBarItemCommentFooterLoading.setIndicatorColor(mColorAccent); } } class LoadMoreCommentsFailedViewHolder extends RecyclerView.ViewHolder { ItemCommentFooterErrorBinding binding; LoadMoreCommentsFailedViewHolder(@NonNull ItemCommentFooterErrorBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.errorTextViewItemCommentFooterError.setTypeface(mActivity.typeface); binding.retryButtonItemCommentFooterError.setTypeface(mActivity.typeface); } binding.errorTextViewItemCommentFooterError.setText(R.string.load_comments_failed); binding.retryButtonItemCommentFooterError.setOnClickListener(view -> mCommentRecyclerViewAdapterCallback.retryFetchingMoreComments()); binding.errorTextViewItemCommentFooterError.setTextColor(mSecondaryTextColor); binding.retryButtonItemCommentFooterError.setBackgroundTintList(ColorStateList.valueOf(mColorPrimaryLightTheme)); binding.retryButtonItemCommentFooterError.setTextColor(mButtonTextColor); } } class ViewAllCommentsViewHolder extends RecyclerView.ViewHolder { ViewAllCommentsViewHolder(@NonNull View itemView) { super(itemView); itemView.setOnClickListener(view -> { if (mActivity != null && mActivity instanceof ViewPostDetailActivity) { mIsSingleCommentThreadMode = false; mSingleCommentId = null; notifyItemRemoved(0); mFragment.changeToNormalThreadMode(); } }); if (mActivity.typeface != null) { ((TextView) itemView).setTypeface(mActivity.typeface); } itemView.setBackgroundTintList(ColorStateList.valueOf(mCommentBackgroundColor)); ((TextView) itemView).setTextColor(mColorAccent); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CrashReportsRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; public class CrashReportsRecyclerViewAdapter extends RecyclerView.Adapter { private final SettingsActivity activity; private final List crashReports; public CrashReportsRecyclerViewAdapter(SettingsActivity activity, List crashReports) { this.activity = activity; this.crashReports = crashReports; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new CrashReportViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_crash_report, parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { ((CrashReportViewHolder) holder).crashReportTextView.setText(crashReports.get(position)); } @Override public int getItemCount() { return crashReports == null ? 0 : crashReports.size(); } private class CrashReportViewHolder extends RecyclerView.ViewHolder { TextView crashReportTextView; public CrashReportViewHolder(@NonNull View itemView) { super(itemView); crashReportTextView = (TextView) itemView; crashReportTextView.setTextColor(activity.customThemeWrapper.getPrimaryTextColor()); if (activity.typeface != null) { crashReportTextView.setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CustomThemeListingRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.content.res.ColorStateList; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CustomThemeListingActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CustomThemeOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.databinding.ItemPredefinedCustomThemeBinding; import ml.docilealligator.infinityforreddit.databinding.ItemUserCustomThemeBinding; public class CustomThemeListingRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_PREDEFINED_THEME = 0; private static final int VIEW_TYPE_USER_THME = 1; private static final int VIEW_TYPE_PREDEFINED_THEME_DIVIDER = 2; private static final int VIEW_TYPE_USER_THEME_DIVIDER = 3; private final BaseActivity activity; private final ArrayList predefinedCustomThemes; private ArrayList userCustomThemes; public CustomThemeListingRecyclerViewAdapter(BaseActivity activity, ArrayList predefinedCustomThemes) { this.activity = activity; this.predefinedCustomThemes = predefinedCustomThemes; userCustomThemes = new ArrayList<>(); } @Override public int getItemViewType(int position) { if (position == 0) { return VIEW_TYPE_PREDEFINED_THEME_DIVIDER; } else if (position < 1 + predefinedCustomThemes.size()) { return VIEW_TYPE_PREDEFINED_THEME; } else if (position == 1 + predefinedCustomThemes.size()) { return VIEW_TYPE_USER_THEME_DIVIDER; } else { return VIEW_TYPE_USER_THME; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_PREDEFINED_THEME_DIVIDER: return new PreDefinedThemeDividerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_theme_type_divider, parent, false)); case VIEW_TYPE_PREDEFINED_THEME: return new PredefinedCustomThemeViewHolder(ItemPredefinedCustomThemeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_USER_THEME_DIVIDER: return new UserThemeDividerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_theme_type_divider, parent, false)); default: return new UserCustomThemeViewHolder(ItemUserCustomThemeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof PredefinedCustomThemeViewHolder) { CustomTheme customTheme = predefinedCustomThemes.get(position - 1); ((PredefinedCustomThemeViewHolder) holder).binding.colorPrimaryItemPredefinedCustomTheme.setBackgroundTintList(ColorStateList.valueOf(customTheme.colorPrimary)); ((PredefinedCustomThemeViewHolder) holder).binding.nameTextViewItemPredefinedCustomTheme.setText(customTheme.name); holder.itemView.setOnClickListener(view -> { Intent intent = new Intent(activity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_NAME, customTheme.name); intent.putExtra(CustomizeThemeActivity.EXTRA_IS_PREDEFIINED_THEME, true); intent.putExtra(CustomizeThemeActivity.EXTRA_CREATE_THEME, true); activity.startActivity(intent); }); } else if (holder instanceof UserCustomThemeViewHolder) { CustomTheme customTheme = userCustomThemes.get(position - predefinedCustomThemes.size() - 2); ((UserCustomThemeViewHolder) holder).binding.colorPrimaryItemUserCustomTheme.setBackgroundTintList(ColorStateList.valueOf(customTheme.colorPrimary)); ((UserCustomThemeViewHolder) holder).binding.nameTextViewItemUserCustomTheme.setText(customTheme.name); ((UserCustomThemeViewHolder) holder).binding.addImageViewItemUserCustomTheme.setOnClickListener(view -> { Intent intent = new Intent(activity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_NAME, customTheme.name); intent.putExtra(CustomizeThemeActivity.EXTRA_CREATE_THEME, true); activity.startActivity(intent); }); ((UserCustomThemeViewHolder) holder).binding.shareImageViewItemUserCustomTheme.setOnClickListener(view -> { ((CustomThemeListingActivity) activity).shareTheme(customTheme); }); holder.itemView.setOnClickListener(view -> { CustomThemeOptionsBottomSheetFragment customThemeOptionsBottomSheetFragment = new CustomThemeOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(CustomThemeOptionsBottomSheetFragment.EXTRA_THEME_NAME, customTheme.name); customThemeOptionsBottomSheetFragment.setArguments(bundle); customThemeOptionsBottomSheetFragment.show(activity.getSupportFragmentManager(), customThemeOptionsBottomSheetFragment.getTag()); }); holder.itemView.setOnLongClickListener(view -> { holder.itemView.performClick(); return true; }); } else if (holder instanceof PreDefinedThemeDividerViewHolder) { ((TextView) holder.itemView).setText(R.string.predefined_themes); } else if (holder instanceof UserThemeDividerViewHolder) { ((TextView) holder.itemView).setText(R.string.user_themes); } } @Override public int getItemCount() { return predefinedCustomThemes.size() + userCustomThemes.size() + 2; } public void setUserThemes(List userThemes) { userCustomThemes = (ArrayList) userThemes; notifyDataSetChanged(); } class PredefinedCustomThemeViewHolder extends RecyclerView.ViewHolder { ItemPredefinedCustomThemeBinding binding; PredefinedCustomThemeViewHolder(@NonNull ItemPredefinedCustomThemeBinding binding) { super(binding.getRoot()); this.binding = binding; binding.nameTextViewItemPredefinedCustomTheme.setTextColor(activity.customThemeWrapper.getPrimaryTextColor()); binding.addImageViewItemPredefinedCustomTheme.setColorFilter(activity.customThemeWrapper.getPrimaryIconColor()); if (activity.typeface != null) { binding.nameTextViewItemPredefinedCustomTheme.setTypeface(activity.typeface); } } } class UserCustomThemeViewHolder extends RecyclerView.ViewHolder { ItemUserCustomThemeBinding binding; UserCustomThemeViewHolder(@NonNull ItemUserCustomThemeBinding binding) { super(binding.getRoot()); this.binding = binding; binding.nameTextViewItemUserCustomTheme.setTextColor(activity.customThemeWrapper.getPrimaryTextColor()); binding.addImageViewItemUserCustomTheme.setColorFilter(activity.customThemeWrapper.getPrimaryIconColor()); binding.shareImageViewItemUserCustomTheme.setColorFilter(activity.customThemeWrapper.getPrimaryIconColor()); if (activity.typeface != null) { binding.nameTextViewItemUserCustomTheme.setTypeface(activity.typeface); } } } class PreDefinedThemeDividerViewHolder extends RecyclerView.ViewHolder { PreDefinedThemeDividerViewHolder(@NonNull View itemView) { super(itemView); ((TextView) itemView).setTextColor(activity.customThemeWrapper.getSecondaryTextColor()); if (activity.typeface != null) { ((TextView) itemView).setTypeface(activity.typeface); } } } class UserThemeDividerViewHolder extends RecyclerView.ViewHolder { UserThemeDividerViewHolder(@NonNull View itemView) { super(itemView); ((TextView) itemView).setTextColor(activity.customThemeWrapper.getSecondaryTextColor()); if (activity.typeface != null) { ((TextView) itemView).setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CustomizeThemeRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeSettingsItem; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.ColorPickerDialog; import ml.docilealligator.infinityforreddit.databinding.ItemCustomThemeColorItemBinding; import ml.docilealligator.infinityforreddit.databinding.ItemCustomThemeSwitchItemBinding; import ml.docilealligator.infinityforreddit.databinding.ItemThemeNameBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class CustomizeThemeRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_COLOR = 1; private static final int VIEW_TYPE_SWITCH = 2; private static final int VIEW_TYPE_THEME_NAME = 3; private final BaseActivity activity; private final CustomThemeWrapper customThemeWrapper; private final ArrayList customThemeSettingsItems; private String themeName; public CustomizeThemeRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, String themeName) { this.activity = activity; this.customThemeWrapper = customThemeWrapper; customThemeSettingsItems = new ArrayList<>(); this.themeName = themeName; } @Override public int getItemViewType(int position) { if (position == 0) { return VIEW_TYPE_THEME_NAME; } else if (position > 3 && position < customThemeSettingsItems.size() - 2) { return VIEW_TYPE_COLOR; } return VIEW_TYPE_SWITCH; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_SWITCH) { return new ThemeSwitchItemViewHolder(ItemCustomThemeSwitchItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_THEME_NAME) { return new ThemeNameItemViewHolder(ItemThemeNameBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new ThemeColorItemViewHolder(ItemCustomThemeColorItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof ThemeColorItemViewHolder) { CustomThemeSettingsItem customThemeSettingsItem = customThemeSettingsItems.get(position - 1); ((ThemeColorItemViewHolder) holder).binding.themeItemNameTextViewItemCustomThemeColorItem.setText(customThemeSettingsItem.itemName); ((ThemeColorItemViewHolder) holder).binding.themeItemInfoTextViewItemCustomThemeColorItem.setText(customThemeSettingsItem.itemDetails); ((ThemeColorItemViewHolder) holder).binding.colorImageViewItemCustomThemeColorItem.setBackgroundTintList(ColorStateList.valueOf(customThemeSettingsItem.colorValue)); holder.itemView.setOnClickListener(view -> { new ColorPickerDialog(activity, customThemeSettingsItem.colorValue, color -> { customThemeSettingsItem.colorValue = color; ((ThemeColorItemViewHolder) holder).binding.colorImageViewItemCustomThemeColorItem.setBackgroundTintList(ColorStateList.valueOf(color)); }).show(); }); } else if (holder instanceof ThemeSwitchItemViewHolder) { CustomThemeSettingsItem customThemeSettingsItem = customThemeSettingsItems.get(position - 1); ((ThemeSwitchItemViewHolder) holder).binding.themeItemNameTextViewItemCustomThemeSwitchItem.setText(customThemeSettingsItem.itemName); ((ThemeSwitchItemViewHolder) holder).binding.themeItemInfoTextViewItemCustomThemeSwitchItem.setText(customThemeSettingsItem.itemName); ((ThemeSwitchItemViewHolder) holder).binding.themeItemSwitchItemCustomThemeSwitchItem.setChecked(customThemeSettingsItem.isEnabled); ((ThemeSwitchItemViewHolder) holder).binding.themeItemSwitchItemCustomThemeSwitchItem.setOnClickListener(view -> customThemeSettingsItem.isEnabled = ((ThemeSwitchItemViewHolder) holder).binding.themeItemSwitchItemCustomThemeSwitchItem.isChecked()); holder.itemView.setOnClickListener(view -> ((ThemeSwitchItemViewHolder) holder).binding.themeItemSwitchItemCustomThemeSwitchItem.performClick()); } else if (holder instanceof ThemeNameItemViewHolder) { ((ThemeNameItemViewHolder) holder).binding.themeNameTextViewItemThemeName.setText(themeName); holder.itemView.setOnClickListener(view -> { View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_edit_name, null); EditText themeNameEditText = dialogView.findViewById(R.id.name_edit_text_edit_name_dialog); themeNameEditText.setText(themeName); themeNameEditText.requestFocus(); Utils.showKeyboard(activity, new Handler(), themeNameEditText); new MaterialAlertDialogBuilder(activity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.edit_theme_name) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Utils.hideKeyboard(activity); themeName = themeNameEditText.getText().toString(); ((ThemeNameItemViewHolder) holder).binding.themeNameTextViewItemThemeName.setText(themeName); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(activity); }) .setOnDismissListener(dialogInterface -> { Utils.hideKeyboard(activity); }) .show(); }); } } @Override public int getItemCount() { return customThemeSettingsItems.size() + 1; } public void setCustomThemeSettingsItem(ArrayList customThemeSettingsItems) { this.customThemeSettingsItems.clear(); this.customThemeSettingsItems.addAll(customThemeSettingsItems); notifyDataSetChanged(); } public String getThemeName() { return themeName; } class ThemeColorItemViewHolder extends RecyclerView.ViewHolder { ItemCustomThemeColorItemBinding binding; ThemeColorItemViewHolder(@NonNull ItemCustomThemeColorItemBinding binding) { super(binding.getRoot()); this.binding = binding; binding.themeItemNameTextViewItemCustomThemeColorItem.setTextColor(customThemeWrapper.getPrimaryTextColor()); binding.themeItemInfoTextViewItemCustomThemeColorItem.setTextColor(customThemeWrapper.getSecondaryTextColor()); if (activity.typeface != null) { binding.themeItemNameTextViewItemCustomThemeColorItem.setTypeface(activity.typeface); binding.themeItemInfoTextViewItemCustomThemeColorItem.setTypeface(activity.typeface); } } } class ThemeSwitchItemViewHolder extends RecyclerView.ViewHolder { ItemCustomThemeSwitchItemBinding binding; ThemeSwitchItemViewHolder(@NonNull ItemCustomThemeSwitchItemBinding binding) { super(binding.getRoot()); this.binding = binding; binding.themeItemNameTextViewItemCustomThemeSwitchItem.setTextColor(customThemeWrapper.getPrimaryTextColor()); binding.themeItemInfoTextViewItemCustomThemeSwitchItem.setTextColor(customThemeWrapper.getSecondaryTextColor()); if (activity.typeface != null) { binding.themeItemNameTextViewItemCustomThemeSwitchItem.setTypeface(activity.typeface); binding.themeItemInfoTextViewItemCustomThemeSwitchItem.setTypeface(activity.typeface); } } } class ThemeNameItemViewHolder extends RecyclerView.ViewHolder { ItemThemeNameBinding binding; public ThemeNameItemViewHolder(@NonNull ItemThemeNameBinding binding) { super(binding.getRoot()); this.binding = binding; binding.themeNameTextViewItemThemeName.setTextColor(customThemeWrapper.getPrimaryTextColor()); binding.descriptionTextViewItemThemeName.setTextColor(customThemeWrapper.getSecondaryTextColor()); if (activity.typeface != null) { binding.themeNameTextViewItemThemeName.setTypeface(activity.typeface); binding.descriptionTextViewItemThemeName.setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/FlairBottomSheetRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemFlairBinding; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.utils.Utils; public class FlairBottomSheetRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private List flairs; private final int flairTextColor; private final ItemClickListener itemClickListener; public FlairBottomSheetRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, ItemClickListener itemClickListener) { this.activity = activity; flairTextColor = customThemeWrapper.getPrimaryTextColor(); this.itemClickListener = itemClickListener; } @NonNull @Override public FlairViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new FlairViewHolder(ItemFlairBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull FlairViewHolder holder, int position) { if (flairs.get(holder.getBindingAdapterPosition()).isEditable()) { holder.binding.editFlairImageViewItemFlair.setVisibility(View.VISIBLE); holder.binding.editFlairImageViewItemFlair.setOnClickListener(view -> { View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_edit_flair, null); EditText flairEditText = dialogView.findViewById(R.id.flair_edit_text_edit_flair_dialog); flairEditText.requestFocus(); Utils.showKeyboard(activity, new Handler(), flairEditText); new MaterialAlertDialogBuilder(activity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.edit_flair) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { Flair flair = flairs.get(holder.getBindingAdapterPosition()); flair.setText(flairEditText.getText().toString()); itemClickListener.onClick(flair); }) .setNegativeButton(R.string.cancel, null) .show(); }); } if (flairs.get(holder.getBindingAdapterPosition()).isEditable() && flairs.get(holder.getBindingAdapterPosition()).getText().isEmpty()) { holder.itemView.setOnClickListener(view -> holder.binding.editFlairImageViewItemFlair.performClick()); } else { holder.itemView.setOnClickListener(view -> itemClickListener.onClick(flairs.get(holder.getBindingAdapterPosition()))); } holder.binding.flairTextViewItemFlair.setText(flairs.get(holder.getBindingAdapterPosition()).getText()); } @Override public int getItemCount() { return flairs == null ? 0 : flairs.size(); } @Override public void onViewRecycled(@NonNull FlairViewHolder holder) { super.onViewRecycled(holder); holder.binding.editFlairImageViewItemFlair.setVisibility(View.GONE); } public void changeDataset(List flairs) { this.flairs = flairs; notifyDataSetChanged(); } public interface ItemClickListener { void onClick(Flair flair); } class FlairViewHolder extends RecyclerView.ViewHolder { ItemFlairBinding binding; FlairViewHolder(@NonNull ItemFlairBinding binding) { super(binding.getRoot()); this.binding = binding; binding.flairTextViewItemFlair.setTextColor(flairTextColor); if (activity.typeface != null) { binding.flairTextViewItemFlair.setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/FollowedUsersRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.List; import java.util.concurrent.Executor; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import me.zhanghai.android.fastscroll.PopupTextProvider; import ml.docilealligator.infinityforreddit.thing.FavoriteThing; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemFavoriteThingDividerBinding; import ml.docilealligator.infinityforreddit.databinding.ItemSubscribedThingBinding; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import retrofit2.Retrofit; public class FollowedUsersRecyclerViewAdapter extends RecyclerView.Adapter implements PopupTextProvider { private static final int VIEW_TYPE_FAVORITE_USER_DIVIDER = 0; private static final int VIEW_TYPE_FAVORITE_USER = 1; private static final int VIEW_TYPE_USER_DIVIDER = 2; private static final int VIEW_TYPE_USER = 3; private List mSubscribedUserData; private List mFavoriteSubscribedUserData; private final BaseActivity mActivity; private final Executor mExecutor; private final Retrofit mOauthRetrofit; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mAccessToken; private final String mAccountName; private final RequestManager glide; private final int mPrimaryTextColor; private final int mSecondaryTextColor; private final ItemOnClickListener itemOnClickListener; public FollowedUsersRecyclerViewAdapter(BaseActivity activity, Executor executor, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, CustomThemeWrapper customThemeWrapper, @Nullable String accessToken, @NonNull String accountName, ItemOnClickListener itemOnClickListener) { mActivity = activity; mExecutor = executor; mOauthRetrofit = oauthRetrofit; mRedditDataRoomDatabase = redditDataRoomDatabase; mAccessToken = accessToken; mAccountName = accountName; glide = Glide.with(activity); mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); this.itemOnClickListener = itemOnClickListener; } @Override public int getItemViewType(int position) { if (mFavoriteSubscribedUserData != null && mFavoriteSubscribedUserData.size() > 0) { if (position == 0) { return VIEW_TYPE_FAVORITE_USER_DIVIDER; } else if (position == mFavoriteSubscribedUserData.size() + 1) { return VIEW_TYPE_USER_DIVIDER; } else if (position <= mFavoriteSubscribedUserData.size()) { return VIEW_TYPE_FAVORITE_USER; } else { return VIEW_TYPE_USER; } } else { return VIEW_TYPE_USER; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { switch (i) { case VIEW_TYPE_FAVORITE_USER_DIVIDER: return new FavoriteUsersDividerViewHolder(ItemFavoriteThingDividerBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); case VIEW_TYPE_FAVORITE_USER: return new FavoriteUserViewHolder(ItemSubscribedThingBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); case VIEW_TYPE_USER_DIVIDER: return new AllUsersDividerViewHolder(ItemFavoriteThingDividerBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); default: return new UserViewHolder(ItemSubscribedThingBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); } } @Override public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, int i) { if (viewHolder instanceof UserViewHolder) { int offset = (mFavoriteSubscribedUserData != null && mFavoriteSubscribedUserData.size() > 0) ? mFavoriteSubscribedUserData.size() + 2 : 0; if (!mSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - offset).getIconUrl().equals("")) { glide.load(mSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - offset).getIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((UserViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((UserViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } ((UserViewHolder) viewHolder).binding.thingNameTextViewItemSubscribedThing.setText(mSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - offset).getName()); if(mSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - offset).isFavorite()) { ((UserViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } else { ((UserViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } } else if (viewHolder instanceof FavoriteUserViewHolder) { if (!mFavoriteSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - 1).getIconUrl().equals("")) { glide.load(mFavoriteSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - 1).getIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((FavoriteUserViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((FavoriteUserViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } ((FavoriteUserViewHolder) viewHolder).binding.thingNameTextViewItemSubscribedThing.setText(mFavoriteSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - 1).getName()); if(mFavoriteSubscribedUserData.get(viewHolder.getBindingAdapterPosition() - 1).isFavorite()) { ((FavoriteUserViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } else { ((FavoriteUserViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } } } @Override public int getItemCount() { if (mSubscribedUserData != null && mSubscribedUserData.size() > 0) { if(mFavoriteSubscribedUserData != null && mFavoriteSubscribedUserData.size() > 0) { return mSubscribedUserData.size() + mFavoriteSubscribedUserData.size() + 2; } return mSubscribedUserData.size(); } return 0; } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if(holder instanceof UserViewHolder) { glide.clear(((UserViewHolder) holder).binding.thingIconGifImageViewItemSubscribedThing); } else if (holder instanceof FavoriteUserViewHolder) { glide.clear(((FavoriteUserViewHolder) holder).binding.thingIconGifImageViewItemSubscribedThing); } } public void setSubscribedUsers(List subscribedUsers) { mSubscribedUserData = subscribedUsers; notifyDataSetChanged(); } public void setFavoriteSubscribedUsers(List favoriteSubscribedUsers) { mFavoriteSubscribedUserData = favoriteSubscribedUsers; notifyDataSetChanged(); } @NonNull @Override public CharSequence getPopupText(@NonNull View view, int position) { switch (getItemViewType(position)) { case VIEW_TYPE_USER: int offset = (mFavoriteSubscribedUserData != null && !mFavoriteSubscribedUserData.isEmpty()) ? mFavoriteSubscribedUserData.size() + 2 : 0; return mSubscribedUserData.get(position - offset).getName().substring(0, 1).toUpperCase(); case VIEW_TYPE_FAVORITE_USER: return mFavoriteSubscribedUserData.get(position - 1).getName().substring(0, 1).toUpperCase(); default: return ""; } } class FavoriteUserViewHolder extends RecyclerView.ViewHolder { ItemSubscribedThingBinding binding; FavoriteUserViewHolder(ItemSubscribedThingBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.thingNameTextViewItemSubscribedThing.setTypeface(mActivity.typeface); } binding.thingNameTextViewItemSubscribedThing.setTextColor(mPrimaryTextColor); itemView.setOnClickListener(view -> { int position = getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedUserData.size() > position) { itemOnClickListener.onClick(mFavoriteSubscribedUserData.get(position)); } }); binding.favoriteImageViewItemSubscribedThing.setOnClickListener(view -> { int position = getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedUserData.size() > position) { if(mFavoriteSubscribedUserData.get(position).isFavorite()) { binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); mFavoriteSubscribedUserData.get(position).setFavorite(false); FavoriteThing.unfavoriteUser(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, mAccountName, mFavoriteSubscribedUserData.get(position), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedUserData.size() > position) { mFavoriteSubscribedUserData.get(position).setFavorite(false); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_unfavorite_failed, Toast.LENGTH_SHORT).show(); int position = getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedUserData.size() > position) { mFavoriteSubscribedUserData.get(position).setFavorite(true); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } }); } else { binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); mFavoriteSubscribedUserData.get(position).setFavorite(true); FavoriteThing.favoriteUser(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, mAccountName, mFavoriteSubscribedUserData.get(position), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedUserData.size() > position) { mFavoriteSubscribedUserData.get(position).setFavorite(true); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_favorite_failed, Toast.LENGTH_SHORT).show(); int position = getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedUserData.size() > position) { mFavoriteSubscribedUserData.get(position).setFavorite(false); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } }); } } }); } } class UserViewHolder extends RecyclerView.ViewHolder { ItemSubscribedThingBinding binding; UserViewHolder(@NonNull ItemSubscribedThingBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.thingNameTextViewItemSubscribedThing.setTypeface(mActivity.typeface); } binding.thingNameTextViewItemSubscribedThing.setTextColor(mPrimaryTextColor); itemView.setOnClickListener(view -> { int offset = (mFavoriteSubscribedUserData != null && mFavoriteSubscribedUserData.size() > 0) ? mFavoriteSubscribedUserData.size() + 2 : 0; int position = getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedUserData.size() > position) { itemOnClickListener.onClick(mSubscribedUserData.get(position)); } }); binding.favoriteImageViewItemSubscribedThing.setOnClickListener(view -> { int offset = (mFavoriteSubscribedUserData != null && mFavoriteSubscribedUserData.size() > 0) ? mFavoriteSubscribedUserData.size() + 2 : 0; int position = getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedUserData.size() > position) { if(mSubscribedUserData.get(position).isFavorite()) { binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); mSubscribedUserData.get(position).setFavorite(false); FavoriteThing.unfavoriteUser(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, mAccountName, mSubscribedUserData.get(position), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedUserData.size() > position) { mSubscribedUserData.get(position).setFavorite(false); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_unfavorite_failed, Toast.LENGTH_SHORT).show(); int position = getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedUserData.size() > position) { mSubscribedUserData.get(position).setFavorite(true); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } }); } else { binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); mSubscribedUserData.get(position).setFavorite(true); FavoriteThing.favoriteUser(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, mAccountName, mSubscribedUserData.get(position), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedUserData.size() > position) { mSubscribedUserData.get(position).setFavorite(true); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_favorite_failed, Toast.LENGTH_SHORT).show(); int position = getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedUserData.size() > position) { mSubscribedUserData.get(position).setFavorite(false); } binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } }); } } }); } } class FavoriteUsersDividerViewHolder extends RecyclerView.ViewHolder { ItemFavoriteThingDividerBinding binding; FavoriteUsersDividerViewHolder(@NonNull ItemFavoriteThingDividerBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.dividerTextViewItemFavoriteThingDivider.setTypeface(mActivity.typeface); } binding.dividerTextViewItemFavoriteThingDivider.setText(R.string.favorites); binding.dividerTextViewItemFavoriteThingDivider.setTextColor(mSecondaryTextColor); } } class AllUsersDividerViewHolder extends RecyclerView.ViewHolder { ItemFavoriteThingDividerBinding binding; AllUsersDividerViewHolder(@NonNull ItemFavoriteThingDividerBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.dividerTextViewItemFavoriteThingDivider.setTypeface(mActivity.typeface); } binding.dividerTextViewItemFavoriteThingDivider.setText(R.string.all); binding.dividerTextViewItemFavoriteThingDivider.setTextColor(mSecondaryTextColor); } } public interface ItemOnClickListener { void onClick(SubscribedUserData subscribedUserData); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/MarkdownBottomBarRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.app.Activity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.slider.Slider; import com.google.android.material.textfield.TextInputEditText; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; public class MarkdownBottomBarRecyclerViewAdapter extends RecyclerView.Adapter { public static final int BOLD = 10; public static final int ITALIC = 9; public static final int LINK = 8; public static final int STRIKE_THROUGH = 7; public static final int SUPERSCRIPT = 6; public static final int HEADER = 5; public static final int ORDERED_LIST = 4; public static final int UNORDERED_LIST = 3; public static final int SPOILER = 2; public static final int QUOTE = 1; public static final int CODE_BLOCK = 0; public static final int UPLOAD_IMAGE = 11; public static final int GIPHY_GIF = 12; private static final int REGULAR_ITEM_COUNT = 11; private final CustomThemeWrapper customThemeWrapper; private final boolean canUploadImage; private final boolean canSendGiphyGIf; private final ItemClickListener itemClickListener; public interface ItemClickListener { void onClick(int item); void onUploadImage(); default void onSelectGiphyGif() {} } public MarkdownBottomBarRecyclerViewAdapter(CustomThemeWrapper customThemeWrapper, ItemClickListener itemClickListener) { this(customThemeWrapper, false, false, itemClickListener); } public MarkdownBottomBarRecyclerViewAdapter(CustomThemeWrapper customThemeWrapper, boolean canUploadImage, ItemClickListener itemClickListener) { this(customThemeWrapper, canUploadImage, false, itemClickListener); } public MarkdownBottomBarRecyclerViewAdapter(CustomThemeWrapper customThemeWrapper, boolean canUploadImage, boolean canSendGiphyGif, ItemClickListener itemClickListener) { this.customThemeWrapper = customThemeWrapper; this.canUploadImage = canUploadImage; this.canSendGiphyGIf = canSendGiphyGif; this.itemClickListener = itemClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MarkdownBottomBarItemViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_markdown_bottom_bar, parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MarkdownBottomBarItemViewHolder) { switch (position) { case BOLD: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_bold_black_24dp); break; case ITALIC: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_italic_black_24dp); break; case LINK: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_link_round_black_24dp); break; case STRIKE_THROUGH: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_strikethrough_black_24dp); break; case SUPERSCRIPT: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_superscript_24dp); break; case HEADER: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_title_24dp); break; case ORDERED_LIST: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_ordered_list_black_24dp); break; case UNORDERED_LIST: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_unordered_list_24dp); break; case SPOILER: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_spoiler_black_24dp); break; case QUOTE: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_quote_24dp); break; case CODE_BLOCK: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_code_24dp); break; case UPLOAD_IMAGE: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_image_day_night_24dp); break; case GIPHY_GIF: ((MarkdownBottomBarItemViewHolder) holder).imageView.setImageResource(R.drawable.ic_gif_24dp); break; } } } @Override public int getItemCount() { return canUploadImage ? (canSendGiphyGIf ? REGULAR_ITEM_COUNT + 2 : REGULAR_ITEM_COUNT + 1) : REGULAR_ITEM_COUNT; } public static void bindEditTextWithItemClickListener(Activity activity, EditText commentEditText, int item) { switch (item) { case MarkdownBottomBarRecyclerViewAdapter.BOLD: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "**" + currentSelection + "**", 0, "****".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "****", 0, "****".length()); commentEditText.setSelection(start + "**".length()); } break; } case MarkdownBottomBarRecyclerViewAdapter.ITALIC: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "*" + currentSelection + "*", 0, "**".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "**", 0, "**".length()); commentEditText.setSelection(start + "*".length()); } break; } case MarkdownBottomBarRecyclerViewAdapter.LINK: { View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_insert_link, null); TextInputEditText textEditText = dialogView.findViewById(R.id.edit_text_insert_link_dialog); TextInputEditText linkEditText = dialogView.findViewById(R.id.edit_link_insert_link_dialog); int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); textEditText.setText(currentSelection); } new MaterialAlertDialogBuilder(activity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.insert_link) .setView(dialogView) .setPositiveButton(R.string.ok, (editTextDialogInterface, i1) -> { String text = textEditText.getText().toString(); String link = linkEditText.getText().toString(); if (text.equals("")) { text = link; } commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "[" + text + "](" + link + ")", 0, "[]()".length() + text.length() + link.length()); }) .setNegativeButton(R.string.cancel, null) .show(); break; } case MarkdownBottomBarRecyclerViewAdapter.STRIKE_THROUGH: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "~~" + currentSelection + "~~", 0, "~~~~".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "~~~~", 0, "~~~~".length()); commentEditText.setSelection(start + "~~".length()); } break; } case MarkdownBottomBarRecyclerViewAdapter.SUPERSCRIPT: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "^(" + currentSelection + ")", 0, "^()".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "^()", 0, "^()".length()); commentEditText.setSelection(start + "^(".length()); } break; } case MarkdownBottomBarRecyclerViewAdapter.HEADER: { View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_select_header, null); Slider seekBar = dialogView.findViewById(R.id.seek_bar_dialog_select_header); new MaterialAlertDialogBuilder(activity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.select_header_size) .setView(dialogView) .setPositiveButton(R.string.ok, (editTextDialogInterface, i1) -> { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); String hashTags; switch ((int) seekBar.getValue()) { case 1: hashTags = "# "; break; case 2: hashTags = "## "; break; case 3: hashTags = "### "; break; case 4: hashTags = "#### "; break; case 5: hashTags = "##### "; break; default: hashTags = "###### "; break; } if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), hashTags + currentSelection, 0, hashTags.length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, hashTags, 0, hashTags.length()); } }) .setNegativeButton(R.string.cancel, null) .show(); break; } case MarkdownBottomBarRecyclerViewAdapter.ORDERED_LIST: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "1. " + currentSelection, 0, "1. ".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "1. ", 0, "1. ".length()); } break; } case MarkdownBottomBarRecyclerViewAdapter.UNORDERED_LIST: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "* " + currentSelection, 0, "* ".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "* ", 0, "* ".length()); } break; } case MarkdownBottomBarRecyclerViewAdapter.SPOILER: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), ">!" + currentSelection + "!<", 0, ">!!<".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, ">!!<", 0, ">!!<".length()); commentEditText.setSelection(start + ">!".length()); } break; } case QUOTE: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "> " + currentSelection + "\n\n", 0, "> \n\n".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "> \n\n", 0, "> \n\n".length()); commentEditText.setSelection(start + "> ".length()); } break; } case CODE_BLOCK: { int start = Math.max(commentEditText.getSelectionStart(), 0); int end = Math.max(commentEditText.getSelectionEnd(), 0); if (end != start) { String currentSelection = commentEditText.getText().subSequence(start, end).toString(); commentEditText.getText().replace(Math.min(start, end), Math.max(start, end), "```\n" + currentSelection + "\n```\n", 0, "```\n\n```\n".length() + currentSelection.length()); } else { commentEditText.getText().replace(start, end, "```\n\n```\n", 0, "```\n\n```\n".length()); commentEditText.setSelection(start + "```\n".length()); } break; } } } class MarkdownBottomBarItemViewHolder extends RecyclerView.ViewHolder { ImageView imageView; public MarkdownBottomBarItemViewHolder(@NonNull View itemView) { super(itemView); imageView = (ImageView) itemView; itemView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position == UPLOAD_IMAGE) { itemClickListener.onUploadImage(); } else if (position == GIPHY_GIF) { itemClickListener.onSelectGiphyGif(); } else { itemClickListener.onClick(position); } }); imageView.setColorFilter(customThemeWrapper.getPrimaryIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/MessageRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.content.res.ColorStateList; import android.net.Uri; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.paging.PagedListAdapter; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import org.greenrobot.eventbus.EventBus; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; import io.noties.markwon.inlineparser.BangInlineProcessor; import io.noties.markwon.inlineparser.HtmlInlineProcessor; import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.movement.MovementMethodPlugin; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewPrivateMessagesActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemFooterErrorBinding; import ml.docilealligator.infinityforreddit.databinding.ItemFooterLoadingBinding; import ml.docilealligator.infinityforreddit.databinding.ItemMessageBinding; import ml.docilealligator.infinityforreddit.events.ChangeInboxCountEvent; import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin; import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod; import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin; import ml.docilealligator.infinityforreddit.markdown.SuperscriptPlugin; import ml.docilealligator.infinityforreddit.message.FetchMessage; import ml.docilealligator.infinityforreddit.message.Message; import ml.docilealligator.infinityforreddit.message.ReadMessage; import retrofit2.Retrofit; public class MessageRecyclerViewAdapter extends PagedListAdapter { private static final int VIEW_TYPE_DATA = 0; private static final int VIEW_TYPE_ERROR = 1; private static final int VIEW_TYPE_LOADING = 2; private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback<>() { @Override public boolean areItemsTheSame(@NonNull Message message, @NonNull Message t1) { return message.getId().equals(t1.getId()); } @Override public boolean areContentsTheSame(@NonNull Message message, @NonNull Message t1) { return message.getBody().equals(t1.getBody()); } }; private final BaseActivity mActivity; private final Retrofit mOauthRetrofit; private final Markwon mMarkwon; private final String mAccessToken; private final String mAccountName; private final int mMessageType; private NetworkState networkState; private final RetryLoadingMoreCallback mRetryLoadingMoreCallback; private final int mColorAccent; private final int mMessageBackgroundColor; private final int mUsernameColor; private final int mSubredditColor; private final int mPrimaryTextColor; private final int mSecondaryTextColor; private final int mUnreadMessageBackgroundColor; private final int mColorPrimaryLightTheme; private final int mButtonTextColor; private boolean markAllMessagesAsRead = false; public MessageRecyclerViewAdapter(BaseActivity activity, Retrofit oauthRetrofit, CustomThemeWrapper customThemeWrapper, String accessToken, String accountName, String where, RetryLoadingMoreCallback retryLoadingMoreCallback) { super(DIFF_CALLBACK); mActivity = activity; mOauthRetrofit = oauthRetrofit; mRetryLoadingMoreCallback = retryLoadingMoreCallback; mColorAccent = customThemeWrapper.getColorAccent(); mMessageBackgroundColor = customThemeWrapper.getCardViewBackgroundColor(); mUsernameColor = customThemeWrapper.getUsername(); mSubredditColor = customThemeWrapper.getSubreddit(); mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); int spoilerBackgroundColor = mSecondaryTextColor | 0xFF000000; mUnreadMessageBackgroundColor = customThemeWrapper.getUnreadMessageBackgroundColor(); mColorPrimaryLightTheme = customThemeWrapper.getColorPrimaryLightTheme(); mButtonTextColor = customThemeWrapper.getButtonTextColor(); // todo:https://github.com/Docile-Alligator/Infinity-For-Reddit/issues/1027 // add tables support and replace with MarkdownUtils#commonPostMarkwonBuilder mMarkwon = Markwon.builder(mActivity) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); mActivity.startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(customThemeWrapper.getLinkColor()); } }) .usePlugin(SuperscriptPlugin.create()) .usePlugin(SpoilerParserPlugin.create(mSecondaryTextColor, spoilerBackgroundColor)) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod())) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .build(); mAccessToken = accessToken; mAccountName = accountName; if (where.equals(FetchMessage.WHERE_MESSAGES)) { mMessageType = FetchMessage.MESSAGE_TYPE_PRIVATE_MESSAGE; } else { mMessageType = FetchMessage.MESSAGE_TYPE_INBOX; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_DATA) { return new DataViewHolder(ItemMessageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_ERROR) { return new ErrorViewHolder(ItemFooterErrorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new LoadingViewHolder(ItemFooterLoadingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof DataViewHolder) { Message message = getItem(holder.getBindingAdapterPosition()); if (message != null) { Message displayedMessage = message.getDisplayedMessage(); String recipientUsername = message.getRecipient(mAccountName); ((DataViewHolder) holder).binding.authorTextViewItemMessage.setTextColor(message.isRecipientASubreddit() ? mSubredditColor : mUsernameColor); if (message.isNew()) { if (markAllMessagesAsRead) { message.setNew(false); } else { holder.itemView.setBackgroundColor( mUnreadMessageBackgroundColor); } } if (message.wasComment()) { ((DataViewHolder) holder).binding.titleTextViewItemMessage.setText(message.getTitle()); } else { ((DataViewHolder) holder).binding.titleTextViewItemMessage.setVisibility(View.GONE); } ((DataViewHolder) holder).binding.authorTextViewItemMessage.setText(recipientUsername); String subject = displayedMessage.getSubject().substring(0, 1).toUpperCase() + displayedMessage.getSubject().substring(1); ((DataViewHolder) holder).binding.subjectTextViewItemMessage.setText(subject); mMarkwon.setMarkdown(((DataViewHolder) holder).binding.contentCustomMarkwonViewItemMessage, displayedMessage.getBody()); } } } @Override public int getItemViewType(int position) { // Reached at the end if (hasExtraRow() && position == getItemCount() - 1) { if (networkState.getStatus() == NetworkState.Status.LOADING) { return VIEW_TYPE_LOADING; } else { return VIEW_TYPE_ERROR; } } else { return VIEW_TYPE_DATA; } } @Override public int getItemCount() { if (hasExtraRow()) { return super.getItemCount() + 1; } return super.getItemCount(); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof DataViewHolder) { holder.itemView.setBackgroundColor(mMessageBackgroundColor); ((DataViewHolder) holder).binding.titleTextViewItemMessage.setVisibility(View.VISIBLE); } } private boolean hasExtraRow() { return networkState != null && networkState.getStatus() != NetworkState.Status.SUCCESS; } public void setNetworkState(NetworkState newNetworkState) { NetworkState previousState = this.networkState; boolean previousExtraRow = hasExtraRow(); this.networkState = newNetworkState; boolean newExtraRow = hasExtraRow(); if (previousExtraRow != newExtraRow) { if (previousExtraRow) { notifyItemRemoved(super.getItemCount()); } else { notifyItemInserted(super.getItemCount()); } } else if (newExtraRow && !previousState.equals(newNetworkState)) { notifyItemChanged(getItemCount() - 1); } } public void updateMessageReply(Message newReply, int position) { if (position >= 0 && position < super.getItemCount()) { Message message = getItem(position); if (message != null) { notifyItemChanged(position); } } } public void setMarkAllMessagesAsRead(boolean markAllMessagesAsRead) { this.markAllMessagesAsRead = markAllMessagesAsRead; } public interface RetryLoadingMoreCallback { void retryLoadingMore(); } class DataViewHolder extends RecyclerView.ViewHolder { ItemMessageBinding binding; DataViewHolder(@NonNull ItemMessageBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.authorTextViewItemMessage.setTypeface(mActivity.typeface); binding.subjectTextViewItemMessage.setTypeface(mActivity.typeface); binding.titleTextViewItemMessage.setTypeface(mActivity.titleTypeface); binding.contentCustomMarkwonViewItemMessage.setTypeface(mActivity.contentTypeface); } itemView.setBackgroundColor(mMessageBackgroundColor); binding.subjectTextViewItemMessage.setTextColor(mPrimaryTextColor); binding.titleTextViewItemMessage.setTextColor(mPrimaryTextColor); binding.contentCustomMarkwonViewItemMessage.setTextColor(mSecondaryTextColor); binding.contentCustomMarkwonViewItemMessage.setMovementMethod(LinkMovementMethod.getInstance()); itemView.setOnClickListener(view -> { Message message = getItem(getBindingAdapterPosition()); if (message == null) { return; } if (mMessageType == FetchMessage.MESSAGE_TYPE_INBOX && message.getContext() != null && !message.getContext().equals("")) { Uri uri = Uri.parse(message.getContext()); Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(uri); mActivity.startActivity(intent); } else if (mMessageType == FetchMessage.MESSAGE_TYPE_PRIVATE_MESSAGE) { Intent intent = new Intent(mActivity, ViewPrivateMessagesActivity.class); intent.putExtra(ViewPrivateMessagesActivity.EXTRA_PRIVATE_MESSAGE_INDEX, getBindingAdapterPosition()); intent.putExtra(ViewPrivateMessagesActivity.EXTRA_MESSAGE_POSITION, getBindingAdapterPosition()); mActivity.startActivity(intent); } if (message.getDisplayedMessage().isNew()) { itemView.setBackgroundColor(mMessageBackgroundColor); message.setNew(false); ReadMessage.readMessage(mOauthRetrofit, mAccessToken, message.getFullname(), new ReadMessage.ReadMessageListener() { @Override public void readSuccess() { EventBus.getDefault().post(new ChangeInboxCountEvent(-1)); } @Override public void readFailed() { message.setNew(true); itemView.setBackgroundColor(mUnreadMessageBackgroundColor); } }); } }); binding.authorTextViewItemMessage.setOnClickListener(view -> { Message message = getItem(getBindingAdapterPosition()); if (message == null || message.isAuthorDeleted()) { return; } Intent intent; if (message.getAuthor() == null || message.getAuthor().equals("null")) { if (message.getSubredditName() == null || message.getSubredditName().equals("null")) { intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, message.getDestination()); } else { intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, message.getSubredditName()); } } else { if (message.getAuthor().equals(mAccountName)) { if (message.getDestination().startsWith("#")) { intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, message.getSubredditName()); } else { intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, message.getDestination()); } } else { intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, message.getAuthor()); } } mActivity.startActivity(intent); }); binding.contentCustomMarkwonViewItemMessage.setOnClickListener(view -> { if (binding.contentCustomMarkwonViewItemMessage.getSelectionStart() == -1 && binding.contentCustomMarkwonViewItemMessage.getSelectionEnd() == -1) { itemView.performClick(); } }); } } class ErrorViewHolder extends RecyclerView.ViewHolder { ItemFooterErrorBinding binding; ErrorViewHolder(@NonNull ItemFooterErrorBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.errorTextViewItemFooterError.setTypeface(mActivity.typeface); binding.retryButtonItemFooterError.setTypeface(mActivity.typeface); } binding.errorTextViewItemFooterError.setText(R.string.load_comments_failed); binding.errorTextViewItemFooterError.setTextColor(mSecondaryTextColor); binding.retryButtonItemFooterError.setOnClickListener(view -> mRetryLoadingMoreCallback.retryLoadingMore()); binding.retryButtonItemFooterError.setBackgroundTintList(ColorStateList.valueOf(mColorPrimaryLightTheme)); binding.retryButtonItemFooterError.setTextColor(mButtonTextColor); } } class LoadingViewHolder extends RecyclerView.ViewHolder { ItemFooterLoadingBinding binding; LoadingViewHolder(@NonNull ItemFooterLoadingBinding binding) { super(binding.getRoot()); this.binding = binding; binding.progressBarItemFooterLoading.setIndicatorColor(mColorAccent); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/MultiRedditListingRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.List; import java.util.concurrent.Executor; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import me.zhanghai.android.fastscroll.PopupTextProvider; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.asynctasks.InsertMultireddit; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemFavoriteThingDividerBinding; import ml.docilealligator.infinityforreddit.databinding.ItemMultiRedditBinding; import ml.docilealligator.infinityforreddit.multireddit.FavoriteMultiReddit; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import retrofit2.Retrofit; public class MultiRedditListingRecyclerViewAdapter extends RecyclerView.Adapter implements PopupTextProvider { private static final int VIEW_TYPE_FAVORITE_MULTI_REDDIT_DIVIDER = 0; private static final int VIEW_TYPE_FAVORITE_MULTI_REDDIT = 1; private static final int VIEW_TYPE_MULTI_REDDIT_DIVIDER = 2; private static final int VIEW_TYPE_MULTI_REDDIT = 3; private final BaseActivity mActivity; private final Executor mExecutor; private final Retrofit mOauthRetrofit; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final RequestManager mGlide; private final String mAccessToken; private final String mAccountName; private List mMultiReddits; private List mFavoriteMultiReddits; private final int mPrimaryTextColor; private final int mSecondaryTextColor; private final OnItemClickListener mOnItemClickListener; public interface OnItemClickListener { void onClick(MultiReddit multiReddit); void onLongClick(MultiReddit multiReddit); } public MultiRedditListingRecyclerViewAdapter(BaseActivity activity, Executor executor, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, CustomThemeWrapper customThemeWrapper, @Nullable String accessToken, @NonNull String accountName, OnItemClickListener onItemClickListener) { mActivity = activity; mExecutor = executor; mGlide = Glide.with(activity); mOauthRetrofit = oauthRetrofit; mRedditDataRoomDatabase = redditDataRoomDatabase; mAccessToken = accessToken; mAccountName = accountName; mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); mOnItemClickListener = onItemClickListener; } @Override public int getItemViewType(int position) { if (mFavoriteMultiReddits != null && mFavoriteMultiReddits.size() > 0) { if (position == 0) { return VIEW_TYPE_FAVORITE_MULTI_REDDIT_DIVIDER; } else if (position == mFavoriteMultiReddits.size() + 1) { return VIEW_TYPE_MULTI_REDDIT_DIVIDER; } else if (position <= mFavoriteMultiReddits.size()) { return VIEW_TYPE_FAVORITE_MULTI_REDDIT; } else { return VIEW_TYPE_MULTI_REDDIT; } } else { return VIEW_TYPE_MULTI_REDDIT; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_FAVORITE_MULTI_REDDIT_DIVIDER: return new FavoriteMultiRedditsDividerViewHolder(ItemFavoriteThingDividerBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_FAVORITE_MULTI_REDDIT: return new FavoriteMultiRedditViewHolder(ItemMultiRedditBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_MULTI_REDDIT_DIVIDER: return new AllMultiRedditsDividerViewHolder(ItemFavoriteThingDividerBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); default: return new MultiRedditViewHolder(ItemMultiRedditBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MultiRedditViewHolder) { String name; String iconUrl; int offset = (mFavoriteMultiReddits != null && mFavoriteMultiReddits.size() > 0) ? mFavoriteMultiReddits.size() + 2 : 0; MultiReddit multiReddit = mMultiReddits.get(holder.getBindingAdapterPosition() - offset); name = multiReddit.getDisplayName(); iconUrl = multiReddit.getIconUrl(); if(multiReddit.isFavorite()) { ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); } else { ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); } ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setOnClickListener(view -> { if(multiReddit.isFavorite()) { ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); multiReddit.setFavorite(false); if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertMultireddit.insertMultireddit(mExecutor, new Handler(), mRedditDataRoomDatabase, multiReddit, () -> { //Do nothing }); } else { FavoriteMultiReddit.favoriteMultiReddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, false, multiReddit, new FavoriteMultiReddit.FavoriteMultiRedditListener() { @Override public void success() { int position = holder.getBindingAdapterPosition() - offset; if(position >= 0 && mMultiReddits.size() > position) { mMultiReddits.get(position).setFavorite(false); } ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_unfavorite_failed, Toast.LENGTH_SHORT).show(); int position = holder.getBindingAdapterPosition() - offset; if(position >= 0 && mMultiReddits.size() > position) { mMultiReddits.get(position).setFavorite(true); } ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); } } ); } } else { ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); multiReddit.setFavorite(true); if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertMultireddit.insertMultireddit(mExecutor, new Handler(), mRedditDataRoomDatabase, multiReddit, () -> { //Do nothing }); } else { FavoriteMultiReddit.favoriteMultiReddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, true, multiReddit, new FavoriteMultiReddit.FavoriteMultiRedditListener() { @Override public void success() { int position = holder.getBindingAdapterPosition() - offset; if(position >= 0 && mMultiReddits.size() > position) { mMultiReddits.get(position).setFavorite(true); } ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_favorite_failed, Toast.LENGTH_SHORT).show(); int position = holder.getBindingAdapterPosition() - offset; if(position >= 0 && mMultiReddits.size() > position) { mMultiReddits.get(position).setFavorite(false); } ((MultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); } } ); } } }); holder.itemView.setOnClickListener(view -> { mOnItemClickListener.onClick(multiReddit); }); holder.itemView.setOnLongClickListener(view -> { mOnItemClickListener.onLongClick(multiReddit); return true; }); if (iconUrl != null && !iconUrl.equals("")) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((MultiRedditViewHolder) holder).binding.multiRedditIconGifImageViewItemMultiReddit); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((MultiRedditViewHolder) holder).binding.multiRedditIconGifImageViewItemMultiReddit); } ((MultiRedditViewHolder) holder).binding.multiRedditNameTextViewItemMultiReddit.setText(name); } else if (holder instanceof FavoriteMultiRedditViewHolder) { MultiReddit multiReddit = mFavoriteMultiReddits.get(holder.getBindingAdapterPosition() - 1); String name = multiReddit.getDisplayName(); String iconUrl = multiReddit.getIconUrl(); if(multiReddit.isFavorite()) { ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); } else { ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); } ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setOnClickListener(view -> { if(multiReddit.isFavorite()) { ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); multiReddit.setFavorite(false); if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertMultireddit.insertMultireddit(mExecutor, new Handler(), mRedditDataRoomDatabase, multiReddit, () -> { //Do nothing }); } else { FavoriteMultiReddit.favoriteMultiReddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, false, multiReddit, new FavoriteMultiReddit.FavoriteMultiRedditListener() { @Override public void success() { int position = holder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteMultiReddits.size() > position) { mFavoriteMultiReddits.get(position).setFavorite(false); } ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_unfavorite_failed, Toast.LENGTH_SHORT).show(); int position = holder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteMultiReddits.size() > position) { mFavoriteMultiReddits.get(position).setFavorite(true); } ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); } } ); } } else { ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); multiReddit.setFavorite(true); if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertMultireddit.insertMultireddit(mExecutor, new Handler(), mRedditDataRoomDatabase, multiReddit, () -> { //Do nothing }); } else { FavoriteMultiReddit.favoriteMultiReddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, mAccessToken, true, multiReddit, new FavoriteMultiReddit.FavoriteMultiRedditListener() { @Override public void success() { int position = holder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteMultiReddits.size() > position) { mFavoriteMultiReddits.get(position).setFavorite(true); } ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_favorite_failed, Toast.LENGTH_SHORT).show(); int position = holder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteMultiReddits.size() > position) { mFavoriteMultiReddits.get(position).setFavorite(false); } ((FavoriteMultiRedditViewHolder) holder).binding.favoriteImageViewItemMultiReddit.setImageResource(R.drawable.ic_favorite_border_24dp); } } ); } } }); holder.itemView.setOnClickListener(view -> { mOnItemClickListener.onClick(multiReddit); }); holder.itemView.setOnLongClickListener(view -> { mOnItemClickListener.onLongClick(multiReddit); return true; }); if (iconUrl != null && !iconUrl.equals("")) { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((FavoriteMultiRedditViewHolder) holder).binding.multiRedditIconGifImageViewItemMultiReddit); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((FavoriteMultiRedditViewHolder) holder).binding.multiRedditIconGifImageViewItemMultiReddit); } ((FavoriteMultiRedditViewHolder) holder).binding.multiRedditNameTextViewItemMultiReddit.setText(name); } } @Override public int getItemCount() { if (mMultiReddits != null) { if(mFavoriteMultiReddits != null && mFavoriteMultiReddits.size() > 0) { return mMultiReddits.size() > 0 ? mFavoriteMultiReddits.size() + mMultiReddits.size() + 2 : 0; } return mMultiReddits.size(); } return 0; } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if(holder instanceof MultiRedditViewHolder) { mGlide.clear(((MultiRedditViewHolder) holder).binding.multiRedditIconGifImageViewItemMultiReddit); } else if (holder instanceof FavoriteMultiRedditViewHolder) { mGlide.clear(((FavoriteMultiRedditViewHolder) holder).binding.multiRedditIconGifImageViewItemMultiReddit); } } public void setMultiReddits(List multiReddits) { mMultiReddits = multiReddits; notifyDataSetChanged(); } public void setFavoriteMultiReddits(List favoriteMultiReddits) { mFavoriteMultiReddits = favoriteMultiReddits; notifyDataSetChanged(); } @NonNull @Override public String getPopupText(@NonNull View view, int position) { switch (getItemViewType(position)) { case VIEW_TYPE_MULTI_REDDIT: int offset = (mFavoriteMultiReddits != null && mFavoriteMultiReddits.size() > 0) ? mFavoriteMultiReddits.size() + 2 : 0; return mMultiReddits.get(position - offset).getDisplayName().substring(0, 1).toUpperCase(); case VIEW_TYPE_FAVORITE_MULTI_REDDIT: return mFavoriteMultiReddits.get(position - 1).getDisplayName().substring(0, 1).toUpperCase(); default: return ""; } } class MultiRedditViewHolder extends RecyclerView.ViewHolder { ItemMultiRedditBinding binding; MultiRedditViewHolder(@NonNull ItemMultiRedditBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.multiRedditNameTextViewItemMultiReddit.setTypeface(mActivity.typeface); } binding.multiRedditNameTextViewItemMultiReddit.setTextColor(mPrimaryTextColor); } } class FavoriteMultiRedditViewHolder extends RecyclerView.ViewHolder { ItemMultiRedditBinding binding; FavoriteMultiRedditViewHolder(@NonNull ItemMultiRedditBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.multiRedditNameTextViewItemMultiReddit.setTypeface(mActivity.typeface); } binding.multiRedditNameTextViewItemMultiReddit.setTextColor(mPrimaryTextColor); } } class FavoriteMultiRedditsDividerViewHolder extends RecyclerView.ViewHolder { ItemFavoriteThingDividerBinding binding; FavoriteMultiRedditsDividerViewHolder(@NonNull ItemFavoriteThingDividerBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.dividerTextViewItemFavoriteThingDivider.setTypeface(mActivity.typeface); } binding.dividerTextViewItemFavoriteThingDivider.setText(R.string.favorites); binding.dividerTextViewItemFavoriteThingDivider.setTextColor(mSecondaryTextColor); } } class AllMultiRedditsDividerViewHolder extends RecyclerView.ViewHolder { ItemFavoriteThingDividerBinding binding; AllMultiRedditsDividerViewHolder(@NonNull ItemFavoriteThingDividerBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.dividerTextViewItemFavoriteThingDivider.setTypeface(mActivity.typeface); } binding.dividerTextViewItemFavoriteThingDivider.setText(R.string.all); binding.dividerTextViewItemFavoriteThingDivider.setTextColor(mSecondaryTextColor); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/OnlineCustomThemeListingRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.Color; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.paging.ItemSnapshotList; import androidx.paging.PagingDataAdapter; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CustomThemeListingActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CustomThemeOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.OnlineCustomThemeMetadata; import ml.docilealligator.infinityforreddit.databinding.ItemUserCustomThemeBinding; public class OnlineCustomThemeListingRecyclerViewAdapter extends PagingDataAdapter { private static final int VIEW_TYPE_USER_THEME = 1; private static final int VIEW_TYPE_USER_THEME_DIVIDER = 2; private final BaseActivity activity; public OnlineCustomThemeListingRecyclerViewAdapter(BaseActivity activity) { super(new DiffUtil.ItemCallback<>() { @Override public boolean areItemsTheSame(@NonNull OnlineCustomThemeMetadata oldItem, @NonNull OnlineCustomThemeMetadata newItem) { return oldItem.name.equals(newItem.name) && oldItem.username.equals(newItem.username); } @Override public boolean areContentsTheSame(@NonNull OnlineCustomThemeMetadata oldItem, @NonNull OnlineCustomThemeMetadata newItem) { return true; } }); this.activity = activity; } @Override public int getItemViewType(int position) { return VIEW_TYPE_USER_THEME; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_USER_THEME_DIVIDER: return new OnlineCustomThemeDividerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_theme_type_divider, parent, false)); default: return new OnlineCustomThemeViewHolder(ItemUserCustomThemeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof OnlineCustomThemeViewHolder) { OnlineCustomThemeMetadata onlineCustomThemeMetadata = getItem(position); ((OnlineCustomThemeViewHolder) holder).binding.colorPrimaryItemUserCustomTheme.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor(onlineCustomThemeMetadata.colorPrimary))); ((OnlineCustomThemeViewHolder) holder).binding.nameTextViewItemUserCustomTheme.setText(onlineCustomThemeMetadata.name); ((OnlineCustomThemeViewHolder) holder).binding.addImageViewItemUserCustomTheme.setOnClickListener(view -> { Intent intent = new Intent(activity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_NAME, onlineCustomThemeMetadata.name); intent.putExtra(CustomizeThemeActivity.EXTRA_ONLINE_CUSTOM_THEME_METADATA, onlineCustomThemeMetadata); intent.putExtra(CustomizeThemeActivity.EXTRA_CREATE_THEME, true); activity.startActivity(intent); }); ((OnlineCustomThemeViewHolder) holder).binding.shareImageViewItemUserCustomTheme.setOnClickListener(view -> { ((CustomThemeListingActivity) activity).shareTheme(onlineCustomThemeMetadata); }); holder.itemView.setOnClickListener(view -> { CustomThemeOptionsBottomSheetFragment customThemeOptionsBottomSheetFragment = new CustomThemeOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(CustomThemeOptionsBottomSheetFragment.EXTRA_THEME_NAME, onlineCustomThemeMetadata.name); bundle.putParcelable(CustomThemeOptionsBottomSheetFragment.EXTRA_ONLINE_CUSTOM_THEME_METADATA, onlineCustomThemeMetadata); bundle.putInt(CustomThemeOptionsBottomSheetFragment.EXTRA_INDEX_IN_THEME_LIST, holder.getBindingAdapterPosition()); customThemeOptionsBottomSheetFragment.setArguments(bundle); customThemeOptionsBottomSheetFragment.show(activity.getSupportFragmentManager(), customThemeOptionsBottomSheetFragment.getTag()); }); holder.itemView.setOnLongClickListener(view -> { holder.itemView.performClick(); return true; }); } } public void updateMetadata(int index, String themeName, String primaryColor) { if (index >= 0) { ItemSnapshotList list = snapshot(); if (index < list.size()) { OnlineCustomThemeMetadata metadata = list.get(index); if (metadata != null) { metadata.name = themeName; metadata.colorPrimary = primaryColor; } } notifyItemChanged(index); } } class OnlineCustomThemeViewHolder extends RecyclerView.ViewHolder { ItemUserCustomThemeBinding binding; OnlineCustomThemeViewHolder(@NonNull ItemUserCustomThemeBinding binding) { super(binding.getRoot()); this.binding = binding; if (activity.typeface != null) { binding.nameTextViewItemUserCustomTheme.setTypeface(activity.typeface); } } } class OnlineCustomThemeDividerViewHolder extends RecyclerView.ViewHolder { OnlineCustomThemeDividerViewHolder(@NonNull View itemView) { super(itemView); if (activity.typeface != null) { ((TextView) itemView).setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/Paging3LoadingStateAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.paging.LoadState; import androidx.paging.LoadStateAdapter; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.loadingindicator.LoadingIndicator; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; public class Paging3LoadingStateAdapter extends LoadStateAdapter { private final BaseActivity activity; private final CustomThemeWrapper mCustomThemeWrapper; private final int mErrorStringId; private final View.OnClickListener mRetryCallback; public Paging3LoadingStateAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, int errorStringId, View.OnClickListener retryCallback) { this.activity = activity; this.mCustomThemeWrapper = customThemeWrapper; this.mErrorStringId = errorStringId; this.mRetryCallback = retryCallback; } @Override public void onBindViewHolder(@NonNull LoadStateViewHolder loadStateViewHolder, @NonNull LoadState loadState) { loadStateViewHolder.bind(loadState); } @NonNull @Override public LoadStateViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, @NonNull LoadState loadState) { return new LoadStateViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_paging_3_load_state, viewGroup, false), mRetryCallback); } class LoadStateViewHolder extends RecyclerView.ViewHolder { private final LoadingIndicator mLoadingIndicator; private final RelativeLayout mErrorView; private final TextView mErrorMsg; private final MaterialButton mRetry; LoadStateViewHolder(@NonNull View itemView, @NonNull View.OnClickListener retryCallback) { super(itemView); mLoadingIndicator = itemView.findViewById(R.id.progress_bar_item_paging_3_load_state); mErrorView = itemView.findViewById(R.id.error_view_relative_layout_item_paging_3_load_state); mErrorMsg = itemView.findViewById(R.id.error_text_view_item_paging_3_load_state); mRetry = itemView.findViewById(R.id.retry_button_item_paging_3_load_state); mErrorMsg.setText(mErrorStringId); mErrorMsg.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); mRetry.setBackgroundColor(mCustomThemeWrapper.getColorPrimaryLightTheme()); mRetry.setTextColor(mCustomThemeWrapper.getButtonTextColor()); mRetry.setOnClickListener(retryCallback); mErrorView.setOnClickListener(retryCallback); if (activity.typeface != null) { mErrorMsg.setTypeface(activity.typeface); mRetry.setTypeface(activity.typeface); } } public void bind(LoadState loadState) { mLoadingIndicator.setVisibility(loadState instanceof LoadState.Loading ? View.VISIBLE : View.GONE); mErrorView.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import static ml.docilealligator.infinityforreddit.activities.CommentActivity.WRITE_COMMENT_REQUEST_CODE; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.Spanned; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.content.res.AppCompatResources; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.media3.common.C; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.Tracks; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.ui.AspectRatioFrameLayout; import androidx.media3.ui.DefaultTimeBar; import androidx.media3.ui.PlayerView; import androidx.media3.ui.TimeBar; import androidx.media3.ui.TrackSelectionDialogBuilder; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.PagerSnapHelper; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.google.android.material.button.MaterialButton; import com.google.common.collect.ImmutableList; import com.libRG.CustomTextView; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.Executor; import java.util.function.Supplier; import javax.inject.Provider; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import jp.wasabeef.glide.transformations.BlurTransformation; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.FetchVideoLinkListener; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CommentActivity; import ml.docilealligator.infinityforreddit.activities.FilteredPostsActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.asynctasks.LoadUserData; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CopyTextBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.ShareBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.AspectRatioGifImageView; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.customviews.SwipeLockInterface; import ml.docilealligator.infinityforreddit.customviews.SwipeLockLinearLayoutManager; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailGalleryBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailImageAndGifAutoplayBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailLinkBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailNoPreviewBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailTextBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailVideoAndGifPreviewBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailVideoAutoplayBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostDetailVideoAutoplayLegacyControllerBinding; import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment; import ml.docilealligator.infinityforreddit.markdown.CustomMarkwonAdapter; import ml.docilealligator.infinityforreddit.markdown.EmoteCloseBracketInlineProcessor; import ml.docilealligator.infinityforreddit.markdown.EmotePlugin; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.post.FetchStreamableVideo; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.thing.SaveThing; import ml.docilealligator.infinityforreddit.thing.StreamableVideo; import ml.docilealligator.infinityforreddit.thing.VoteThing; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager; import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator; import ml.docilealligator.infinityforreddit.videoautoplay.ExoPlayerViewHelper; import ml.docilealligator.infinityforreddit.videoautoplay.Playable; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; import pl.droidsonroids.gif.GifImageView; import retrofit2.Call; import retrofit2.Retrofit; public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter implements CacheManager { private static final int VIEW_TYPE_POST_DETAIL_VIDEO_AUTOPLAY = 1; private static final int VIEW_TYPE_POST_DETAIL_VIDEO_AND_GIF_PREVIEW = 2; private static final int VIEW_TYPE_POST_DETAIL_IMAGE = 3; private static final int VIEW_TYPE_POST_DETAIL_GIF_AUTOPLAY = 4; private static final int VIEW_TYPE_POST_DETAIL_LINK = 5; private static final int VIEW_TYPE_POST_DETAIL_NO_PREVIEW_LINK = 6; private static final int VIEW_TYPE_POST_DETAIL_GALLERY = 7; private static final int VIEW_TYPE_POST_DETAIL_TEXT_TYPE = 8; private final BaseActivity mActivity; private final ViewPostDetailFragment mFragment; private final Executor mExecutor; private final Retrofit mRetrofit; private final Retrofit mOauthRetrofit; private final Retrofit mRedgifsRetrofit; private final Provider mStreamableApiProvider; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final SharedPreferences mCurrentAccountSharedPreferences; private final RequestManager mGlide; private final SaveMemoryCenterInisdeDownsampleStrategy mSaveMemoryCenterInsideDownsampleStrategy; private final EmoteCloseBracketInlineProcessor mEmoteCloseBracketInlineProcessor; private final EmotePlugin mEmotePlugin; private final ImageAndGifPlugin mImageAndGifPlugin; private final Markwon mPostDetailMarkwon; private final ImageAndGifEntry mImageAndGifEntry; private final CustomMarkwonAdapter mMarkwonAdapter; private final String mAccessToken; private final String mAccountName; private Post mPost; private final String mSubredditNamePrefixed; private final Locale mLocale; private boolean mNeedBlurNsfw; private boolean mDoNotBlurNsfwInNsfwSubreddits; private boolean mNeedBlurSpoiler; private final boolean mVoteButtonsOnTheRight; private final boolean mShowElapsedTime; private final String mTimeFormatPattern; private final boolean mShowAbsoluteNumberOfVotes; private boolean mAutoplay = false; private final boolean mAutoplayNsfwVideos; private final boolean mMuteAutoplayingVideos; private final double mStartAutoplayVisibleAreaOffset; private final boolean mMuteNSFWVideo; private boolean mDataSavingMode; private final boolean mDisableImagePreview; private final boolean mOnlyDisablePreviewInVideoAndGifPosts; private final boolean mHidePostType; private final boolean mHidePostFlair; private final boolean mHideUpvoteRatio; private final boolean mHideSubredditAndUserPrefix; private final boolean mHideTheNumberOfVotes; private final boolean mHideTheNumberOfComments; private final boolean mSeparatePostAndComments; private final boolean mLegacyAutoplayVideoControllerUI; private final boolean mEasierToWatchInFullScreen; private final boolean mDisableProfileAvatarAnimation; private final int mDataSavingModeDefaultResolution; private final int mNonDataSavingModeDefaultResolution; private final int mMaxResolution; private final PostDetailRecyclerViewAdapterCallback mPostDetailRecyclerViewAdapterCallback; private Supplier> mCommentsSupplier; private final int mColorAccent; private final int mCardViewColor; private final int mSecondaryTextColor; private final int mPostTitleColor; private final int mPrimaryTextColor; private final int mPostTypeBackgroundColor; private final int mPostTypeTextColor; private final int mSubredditColor; private final int mUsernameColor; private final int mModeratorColor; private final int mAuthorFlairTextColor; private final int mSpoilerBackgroundColor; private final int mSpoilerTextColor; private final int mFlairBackgroundColor; private final int mFlairTextColor; private final int mNSFWBackgroundColor; private final int mNSFWTextColor; private final int mArchivedTintColor; private final int mLockedTintColor; private final int mCrosspostTintColor; private final int mMediaIndicatorIconTint; private final int mMediaIndicatorBackgroundColor; private final int mUpvoteRatioTintColor; private final int mNoPreviewPostTypeBackgroundColor; private final int mNoPreviewPostTypeIconTint; private final int mUpvotedColor; private final int mDownvotedColor; private final int mVoteAndReplyUnavailableVoteButtonColor; private final int mPostIconAndInfoColor; private final int mCommentColor; private final float mScale; private final ExoCreator mExoCreator; private boolean canStartActivity = true; private boolean canPlayVideo = true; public PostDetailRecyclerViewAdapter(@NonNull BaseActivity activity, ViewPostDetailFragment fragment, Executor executor, CustomThemeWrapper customThemeWrapper, Retrofit oauthRetrofit, Retrofit retrofit, Retrofit redgifsRetrofit, Provider streamableApiProvider, RedditDataRoomDatabase redditDataRoomDatabase, RequestManager glide, boolean separatePostAndComments, @Nullable String accessToken, @NonNull String accountName, Post post, Locale locale, SharedPreferences sharedPreferences, SharedPreferences currentAccountSharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferences, SharedPreferences postDetailsSharedPreferences, ExoCreator exoCreator, PostDetailRecyclerViewAdapterCallback postDetailRecyclerViewAdapterCallback) { mActivity = activity; mFragment = fragment; mExecutor = executor; mRetrofit = retrofit; mOauthRetrofit = oauthRetrofit; mRedgifsRetrofit = redgifsRetrofit; mStreamableApiProvider = streamableApiProvider; mRedditDataRoomDatabase = redditDataRoomDatabase; mGlide = glide; mMaxResolution = Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000")); mSaveMemoryCenterInsideDownsampleStrategy = new SaveMemoryCenterInisdeDownsampleStrategy(mMaxResolution); mCurrentAccountSharedPreferences = currentAccountSharedPreferences; mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); int markdownColor = customThemeWrapper.getPostContentColor(); int postSpoilerBackgroundColor = markdownColor | 0xFF000000; int linkColor = customThemeWrapper.getLinkColor(); mSeparatePostAndComments = separatePostAndComments; mLegacyAutoplayVideoControllerUI = sharedPreferences.getBoolean(SharedPreferencesUtils.LEGACY_AUTOPLAY_VIDEO_CONTROLLER_UI, false); mEasierToWatchInFullScreen = sharedPreferences.getBoolean(SharedPreferencesUtils.EASIER_TO_WATCH_IN_FULL_SCREEN, false); mDataSavingModeDefaultResolution = Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION, "360")); mNonDataSavingModeDefaultResolution = Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION_NO_DATA_SAVING, "0")); mAccessToken = accessToken; mAccountName = accountName; mPost = post; mSubredditNamePrefixed = post.getSubredditNamePrefixed(); mLocale = locale; mNeedBlurNsfw = nsfwAndSpoilerSharedPreferences.getBoolean((mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mAccountName) + SharedPreferencesUtils.BLUR_NSFW_BASE, true); mDoNotBlurNsfwInNsfwSubreddits = nsfwAndSpoilerSharedPreferences.getBoolean((mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mAccountName) + SharedPreferencesUtils.DO_NOT_BLUR_NSFW_IN_NSFW_SUBREDDITS, false); mNeedBlurSpoiler = nsfwAndSpoilerSharedPreferences.getBoolean((mAccountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mAccountName) + SharedPreferencesUtils.BLUR_SPOILER_BASE, false); mVoteButtonsOnTheRight = sharedPreferences.getBoolean(SharedPreferencesUtils.VOTE_BUTTONS_ON_THE_RIGHT_KEY, false); mShowElapsedTime = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ELAPSED_TIME_KEY, false); mTimeFormatPattern = sharedPreferences.getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE); mShowAbsoluteNumberOfVotes = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ABSOLUTE_NUMBER_OF_VOTES, true); String autoplayString = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_AUTOPLAY, SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_NEVER); int networkType = Utils.getConnectedNetwork(activity); if (autoplayString.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ALWAYS_ON)) { mAutoplay = true; } else if (autoplayString.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ON_WIFI)) { mAutoplay = networkType == Utils.NETWORK_TYPE_WIFI; } mAutoplayNsfwVideos = sharedPreferences.getBoolean(SharedPreferencesUtils.AUTOPLAY_NSFW_VIDEOS, true); mMuteAutoplayingVideos = sharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_AUTOPLAYING_VIDEOS, true); Resources resources = activity.getResources(); mStartAutoplayVisibleAreaOffset = resources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? sharedPreferences.getInt(SharedPreferencesUtils.START_AUTOPLAY_VISIBLE_AREA_OFFSET_PORTRAIT, 75) / 100.0 : sharedPreferences.getInt(SharedPreferencesUtils.START_AUTOPLAY_VISIBLE_AREA_OFFSET_LANDSCAPE, 50) / 100.0; mMuteNSFWVideo = sharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_NSFW_VIDEO, false); String dataSavingModeString = sharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ALWAYS)) { mDataSavingMode = true; } else if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { mDataSavingMode = networkType == Utils.NETWORK_TYPE_CELLULAR; } mDisableImagePreview = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_IMAGE_PREVIEW, false); mOnlyDisablePreviewInVideoAndGifPosts = sharedPreferences.getBoolean(SharedPreferencesUtils.ONLY_DISABLE_PREVIEW_IN_VIDEO_AND_GIF_POSTS, false); mDisableProfileAvatarAnimation = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_PROFILE_AVATAR_ANIMATION, false); mHidePostType = postDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_POST_TYPE, false); mHidePostFlair = postDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_POST_FLAIR, false); mHideUpvoteRatio = postDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_UPVOTE_RATIO, false); mHideSubredditAndUserPrefix = postDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_SUBREDDIT_AND_USER_PREFIX, false); mHideTheNumberOfVotes = postDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_VOTES, false); mHideTheNumberOfComments = postDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_COMMENTS, false); mPostDetailRecyclerViewAdapterCallback = postDetailRecyclerViewAdapterCallback; mScale = resources.getDisplayMetrics().density; mColorAccent = customThemeWrapper.getColorAccent(); mCardViewColor = customThemeWrapper.getCardViewBackgroundColor(); mPostTitleColor = customThemeWrapper.getPostTitleColor(); mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); mPostTypeBackgroundColor = customThemeWrapper.getPostTypeBackgroundColor(); mPostTypeTextColor = customThemeWrapper.getPostTypeTextColor(); mAuthorFlairTextColor = customThemeWrapper.getAuthorFlairTextColor(); mSpoilerBackgroundColor = customThemeWrapper.getSpoilerBackgroundColor(); mSpoilerTextColor = customThemeWrapper.getSpoilerTextColor(); mNSFWBackgroundColor = customThemeWrapper.getNsfwBackgroundColor(); mNSFWTextColor = customThemeWrapper.getNsfwTextColor(); mArchivedTintColor = customThemeWrapper.getArchivedIconTint(); mLockedTintColor = customThemeWrapper.getLockedIconTint(); mCrosspostTintColor = customThemeWrapper.getCrosspostIconTint(); mMediaIndicatorIconTint = customThemeWrapper.getMediaIndicatorIconColor(); mMediaIndicatorBackgroundColor = customThemeWrapper.getMediaIndicatorBackgroundColor(); mUpvoteRatioTintColor = customThemeWrapper.getUpvoteRatioIconTint(); mNoPreviewPostTypeBackgroundColor = customThemeWrapper.getNoPreviewPostTypeBackgroundColor(); mNoPreviewPostTypeIconTint = customThemeWrapper.getNoPreviewPostTypeIconTint(); mFlairBackgroundColor = customThemeWrapper.getFlairBackgroundColor(); mFlairTextColor = customThemeWrapper.getFlairTextColor(); mSubredditColor = customThemeWrapper.getSubreddit(); mUsernameColor = customThemeWrapper.getUsername(); mModeratorColor = customThemeWrapper.getModerator(); mUpvotedColor = customThemeWrapper.getUpvoted(); mDownvotedColor = customThemeWrapper.getDownvoted(); mVoteAndReplyUnavailableVoteButtonColor = customThemeWrapper.getVoteAndReplyUnavailableButtonColor(); mPostIconAndInfoColor = customThemeWrapper.getPostIconAndInfoColor(); mCommentColor = customThemeWrapper.getCommentColor(); mExoCreator = exoCreator; MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (mActivity.contentTypeface != null) { textView.setTypeface(mActivity.contentTypeface); } textView.setTextColor(markdownColor); textView.setHighlightColor(Color.TRANSPARENT); textView.setOnLongClickListener(view -> { if (textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { CopyTextBottomSheetFragment.show( mFragment.getChildFragmentManager(), mPost.getSelfTextPlain(), mPost.getSelfText() ); return true; } return false; }); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_IS_NSFW, mPost.isNSFW()); mActivity.startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(linkColor); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { if (!activity.isDestroyed() && !activity.isFinishing()) { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(UrlMenuBottomSheetFragment.EXTRA_URL, url); urlMenuBottomSheetFragment.setArguments(bundle); urlMenuBottomSheetFragment.show(fragment.getChildFragmentManager(), urlMenuBottomSheetFragment.getTag()); } return true; }; mEmoteCloseBracketInlineProcessor = new EmoteCloseBracketInlineProcessor(); mEmotePlugin = EmotePlugin.create(activity, Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.EMBEDDED_MEDIA_TYPE, "15")), mDataSavingMode, mDisableImagePreview, mediaMetadata -> { Intent intent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); if (canStartActivity) { canStartActivity = false; activity.startActivity(intent); } }); mImageAndGifPlugin = new ImageAndGifPlugin(); mPostDetailMarkwon = MarkdownUtils.createFullRedditMarkwon(mActivity, miscPlugin, mEmoteCloseBracketInlineProcessor, mEmotePlugin, mImageAndGifPlugin, markdownColor, postSpoilerBackgroundColor, onLinkLongClickListener); mImageAndGifEntry = new ImageAndGifEntry(activity, mGlide, Integer.parseInt(postDetailsSharedPreferences.getString(SharedPreferencesUtils.EMBEDDED_MEDIA_TYPE, "15")), mDataSavingMode, mDisableImagePreview, (post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (mPost.isSpoiler() && mNeedBlurSpoiler), (mediaMetadata, commentId, postId) -> { Intent intent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); if (canStartActivity) { canStartActivity = false; activity.startActivity(intent); } }); mMarkwonAdapter = MarkdownUtils.createCustomTablesAndImagesAdapter(mActivity, mImageAndGifEntry); } public void setCanStartActivity(boolean canStartActivity) { this.canStartActivity = canStartActivity; } @Override public int getItemViewType(int position) { switch (mPost.getPostType()) { case Post.VIDEO_TYPE: if (mAutoplay && !mSeparatePostAndComments) { if ((!mAutoplayNsfwVideos && mPost.isNSFW()) || mPost.isSpoiler()) { return VIEW_TYPE_POST_DETAIL_VIDEO_AND_GIF_PREVIEW; } return VIEW_TYPE_POST_DETAIL_VIDEO_AUTOPLAY; } else { return VIEW_TYPE_POST_DETAIL_VIDEO_AND_GIF_PREVIEW; } case Post.GIF_TYPE: if (mAutoplay) { if ((!mAutoplayNsfwVideos && mPost.isNSFW()) || mPost.isSpoiler()) { return VIEW_TYPE_POST_DETAIL_NO_PREVIEW_LINK; } return VIEW_TYPE_POST_DETAIL_GIF_AUTOPLAY; } else { if ((mPost.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (mPost.isSpoiler() && mNeedBlurSpoiler)) { return VIEW_TYPE_POST_DETAIL_NO_PREVIEW_LINK; } return VIEW_TYPE_POST_DETAIL_VIDEO_AND_GIF_PREVIEW; } case Post.IMAGE_TYPE: return VIEW_TYPE_POST_DETAIL_IMAGE; case Post.LINK_TYPE: return VIEW_TYPE_POST_DETAIL_LINK; case Post.NO_PREVIEW_LINK_TYPE: // Check if we have thumbnail fallback available Post.Preview preview = getSuitablePreview(mPost.getPreviews()); if (preview != null) { return VIEW_TYPE_POST_DETAIL_LINK; } else { return VIEW_TYPE_POST_DETAIL_NO_PREVIEW_LINK; } case Post.GALLERY_TYPE: return VIEW_TYPE_POST_DETAIL_GALLERY; default: return VIEW_TYPE_POST_DETAIL_TEXT_TYPE; } } @OptIn(markerClass = UnstableApi.class) @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_POST_DETAIL_VIDEO_AUTOPLAY: if (mDataSavingMode) { if (mDisableImagePreview || mOnlyDisablePreviewInVideoAndGifPosts) { return new PostDetailNoPreviewViewHolder(ItemPostDetailNoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new PostDetailVideoAndGifPreviewHolder(ItemPostDetailVideoAndGifPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } if (mLegacyAutoplayVideoControllerUI) { return new PostDetailVideoAutoplayLegacyControllerViewHolder(ItemPostDetailVideoAutoplayLegacyControllerBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new PostDetailVideoAutoplayViewHolder(ItemPostDetailVideoAutoplayBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } case VIEW_TYPE_POST_DETAIL_VIDEO_AND_GIF_PREVIEW: if (mDataSavingMode && (mDisableImagePreview || mOnlyDisablePreviewInVideoAndGifPosts)) { return new PostDetailNoPreviewViewHolder(ItemPostDetailNoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new PostDetailVideoAndGifPreviewHolder(ItemPostDetailVideoAndGifPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_POST_DETAIL_IMAGE: if (mDataSavingMode && mDisableImagePreview) { return new PostDetailNoPreviewViewHolder(ItemPostDetailNoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new PostDetailImageAndGifAutoplayViewHolder(ItemPostDetailImageAndGifAutoplayBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_POST_DETAIL_GIF_AUTOPLAY: if (mDataSavingMode && (mDisableImagePreview || mOnlyDisablePreviewInVideoAndGifPosts)) { return new PostDetailNoPreviewViewHolder(ItemPostDetailNoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new PostDetailImageAndGifAutoplayViewHolder(ItemPostDetailImageAndGifAutoplayBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_POST_DETAIL_LINK: if (mDataSavingMode && mDisableImagePreview) { return new PostDetailNoPreviewViewHolder(ItemPostDetailNoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new PostDetailLinkViewHolder(ItemPostDetailLinkBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_POST_DETAIL_NO_PREVIEW_LINK: return new PostDetailNoPreviewViewHolder(ItemPostDetailNoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); case VIEW_TYPE_POST_DETAIL_GALLERY: if (mDataSavingMode && mDisableImagePreview) { return new PostDetailNoPreviewViewHolder(ItemPostDetailNoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new PostDetailGalleryViewHolder(ItemPostDetailGalleryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); default: return new PostDetailTextViewHolder(ItemPostDetailTextBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @OptIn(markerClass = UnstableApi.class) @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof PostDetailBaseViewHolder) { ((PostDetailBaseViewHolder) holder).titleTextView.setText(mPost.getTitle()); if (mPost.getSubredditNamePrefixed().startsWith("u/")) { if (mPost.getAuthorIconUrl() == null) { String authorName = mPost.isAuthorDeleted() ? mPost.getSubredditNamePrefixed().substring(2) : mPost.getAuthor(); LoadUserData.loadUserData(mExecutor, new Handler(), mRedditDataRoomDatabase, mAccessToken, authorName, mOauthRetrofit, mRetrofit, iconImageUrl -> { if (mActivity != null && getItemCount() > 0) { if (iconImageUrl == null || iconImageUrl.isEmpty()) { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } else { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(iconImageUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } else { mGlide.load(iconImageUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } } if (holder.getBindingAdapterPosition() >= 0) { mPost.setAuthorIconUrl(iconImageUrl); } } }); } else if (!mPost.getAuthorIconUrl().equals("")) { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(mPost.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } else { mGlide.load(mPost.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } } else { if (mPost.getSubredditIconUrl() == null) { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, mPost.getSubredditNamePrefixed().substring(2), mAccessToken, mAccountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { if (iconImageUrl == null || iconImageUrl.equals("")) { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } else { RequestBuilder requestBuilder = mGlide.load(iconImageUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))); if (mDisableProfileAvatarAnimation) { requestBuilder = requestBuilder.dontAnimate(); } requestBuilder.into(((PostDetailBaseViewHolder) holder).iconGifImageView); } mPost.setSubredditIconUrl(iconImageUrl); }); } else if (!mPost.getSubredditIconUrl().equals("")) { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(mPost.getSubredditIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } else { mGlide.load(mPost.getSubredditIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostDetailBaseViewHolder) holder).iconGifImageView); } } if (mPost.getAuthorFlairHTML() != null && !mPost.getAuthorFlairHTML().equals("")) { ((PostDetailBaseViewHolder) holder).authorFlairTextView.setVisibility(View.VISIBLE); Utils.setHTMLWithImageToTextView(((PostDetailBaseViewHolder) holder).authorFlairTextView, mPost.getAuthorFlairHTML(), true); } else if (mPost.getAuthorFlair() != null && !mPost.getAuthorFlair().equals("")) { ((PostDetailBaseViewHolder) holder).authorFlairTextView.setVisibility(View.VISIBLE); ((PostDetailBaseViewHolder) holder).authorFlairTextView.setText(mPost.getAuthorFlair()); } switch (mPost.getVoteType()) { case 1: //Upvoted ((PostDetailBaseViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); ((PostDetailBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); ((PostDetailBaseViewHolder) holder).scoreTextView.setTextColor(mUpvotedColor); break; case -1: //Downvoted ((PostDetailBaseViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); ((PostDetailBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); ((PostDetailBaseViewHolder) holder).scoreTextView.setTextColor(mDownvotedColor); break; case 0: ((PostDetailBaseViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); ((PostDetailBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); ((PostDetailBaseViewHolder) holder).scoreTextView.setTextColor(mPostIconAndInfoColor); ((PostDetailBaseViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); ((PostDetailBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); } if (mPost.isArchived()) { ((PostDetailBaseViewHolder) holder).archivedImageView.setVisibility(View.VISIBLE); ((PostDetailBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); ((PostDetailBaseViewHolder) holder).scoreTextView.setTextColor(mVoteAndReplyUnavailableVoteButtonColor); ((PostDetailBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); } if (mPost.isCrosspost()) { ((PostDetailBaseViewHolder) holder).crosspostImageView.setVisibility(View.VISIBLE); } if (!mHideSubredditAndUserPrefix) { ((PostDetailBaseViewHolder) holder).subredditTextView.setText("r/" + mPost.getSubredditName()); ((PostDetailBaseViewHolder) holder).userTextView.setText(mPost.getAuthorNamePrefixed()); } else { ((PostDetailBaseViewHolder) holder).subredditTextView.setText(mPost.getSubredditName()); ((PostDetailBaseViewHolder) holder).userTextView.setText(mPost.getAuthor()); } if (mPost.isModerator()) { ((PostDetailBaseViewHolder) holder).userTextView.setTextColor(mModeratorColor); Drawable moderatorDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_verified_user_14dp, mModeratorColor); ((PostDetailBaseViewHolder) holder).userTextView.setCompoundDrawablesWithIntrinsicBounds( moderatorDrawable, null, null, null); } if (mShowElapsedTime) { ((PostDetailBaseViewHolder) holder).postTimeTextView.setText( Utils.getElapsedTime(mActivity, mPost.getPostTimeMillis())); } else { ((PostDetailBaseViewHolder) holder).postTimeTextView.setText(Utils.getFormattedTime(mLocale, mPost.getPostTimeMillis(), mTimeFormatPattern)); } if (mPost.isLocked()) { ((PostDetailBaseViewHolder) holder).lockedImageView.setVisibility(View.VISIBLE); } if (mPost.isSpoiler()) { ((PostDetailBaseViewHolder) holder).spoilerTextView.setVisibility(View.VISIBLE); } if (!mHidePostFlair && mPost.getFlair() != null && !mPost.getFlair().equals("")) { ((PostDetailBaseViewHolder) holder).flairTextView.setVisibility(View.VISIBLE); Utils.setHTMLWithImageToTextView(((PostDetailBaseViewHolder) holder).flairTextView, mPost.getFlair(), false); } if (mHideUpvoteRatio) { ((PostDetailBaseViewHolder) holder).upvoteRatioTextView.setVisibility(View.GONE); } else { ((PostDetailBaseViewHolder) holder).upvoteRatioTextView.setText(mPost.getUpvoteRatio() + "%"); } if (mPost.isNSFW()) { ((PostDetailBaseViewHolder) holder).nsfwTextView.setVisibility(View.VISIBLE); } else { ((PostDetailBaseViewHolder) holder).nsfwTextView.setVisibility(View.GONE); } if (!mHideTheNumberOfVotes) { ((PostDetailBaseViewHolder) holder).scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, mPost.getScore() + mPost.getVoteType())); } else { ((PostDetailBaseViewHolder) holder).scoreTextView.setText(mActivity.getString(R.string.vote)); } ((PostDetailBaseViewHolder) holder).commentsCountButton.setText(Integer.toString(mPost.getNComments())); if (mPost.isSaved()) { ((PostDetailBaseViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } else { ((PostDetailBaseViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } if (mPost.getSelfText() != null && !mPost.getSelfText().equals("")) { ((PostDetailBaseViewHolder) holder).contentMarkdownView.setVisibility(View.VISIBLE); ((PostDetailBaseViewHolder) holder).contentMarkdownView.setAdapter(mMarkwonAdapter); mEmoteCloseBracketInlineProcessor.setMediaMetadataMap(mPost.getMediaMetadataMap()); mImageAndGifPlugin.setMediaMetadataMap(mPost.getMediaMetadataMap()); mMarkwonAdapter.setMarkdown(mPostDetailMarkwon, mPost.getSelfText()); // noinspection NotifyDataSetChanged mMarkwonAdapter.notifyDataSetChanged(); } if (holder instanceof PostDetailBaseVideoAutoplayViewHolder) { ((PostDetailBaseVideoAutoplayViewHolder) holder).previewImageView.setVisibility(View.VISIBLE); Post.Preview preview = getSuitablePreview(mPost.getPreviews()); if (preview != null) { ((PostDetailBaseVideoAutoplayViewHolder) holder).aspectRatioFrameLayout.setAspectRatio((float) preview.getPreviewWidth() / preview.getPreviewHeight()); mGlide.load(preview.getPreviewUrl()).centerInside().downsample(mSaveMemoryCenterInsideDownsampleStrategy).into(((PostDetailBaseVideoAutoplayViewHolder) holder).previewImageView); } else { ((PostDetailBaseVideoAutoplayViewHolder) holder).aspectRatioFrameLayout.setAspectRatio(1); } if (!((PostDetailBaseVideoAutoplayViewHolder) holder).isManuallyPaused) { ((PostDetailBaseVideoAutoplayViewHolder) holder).setVolume((mMuteAutoplayingVideos || (mPost.isNSFW() && mMuteNSFWVideo)) ? 0f : 1f); } /*if (mPost.isRedgifs() && !mPost.isLoadedStreamableVideoAlready()) { ((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall = mRedgifsRetrofit.create(RedgifsAPI.class) .getRedgifsData(APIUtils.getRedgifsOAuthHeader( mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")), mPost.getRedgifsId(), APIUtils.USER_AGENT); FetchRedgifsVideoLinks.fetchRedgifsVideoLinksInRecyclerViewAdapter(mExecutor, new Handler(), ((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall, new FetchVideoLinkListener() { @Override public void onFetchRedgifsVideoLinkSuccess(String webm, String mp4) { mPost.setVideoDownloadUrl(mp4); mPost.setVideoUrl(mp4); mPost.setLoadedStreamableVideoAlready(true); ((PostDetailBaseVideoAutoplayViewHolder) holder).bindVideoUri(Uri.parse(mPost.getVideoUrl())); } @Override public void failed(@Nullable Integer messageRes) { ((PostDetailBaseVideoAutoplayViewHolder) holder).loadFallbackDirectVideo(); } }); } else */if(mPost.isStreamable() && !mPost.isLoadedStreamableVideoAlready()) { ((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall = mStreamableApiProvider.get().getStreamableData(mPost.getStreamableShortCode()); FetchStreamableVideo.fetchStreamableVideoInRecyclerViewAdapter(mExecutor, new Handler(), ((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall, new FetchVideoLinkListener() { @Override public void onFetchStreamableVideoLinkSuccess(StreamableVideo streamableVideo) { StreamableVideo.Media media = streamableVideo.mp4 == null ? streamableVideo.mp4Mobile : streamableVideo.mp4; mPost.setVideoDownloadUrl(media.url); mPost.setVideoUrl(media.url); mPost.setLoadedStreamableVideoAlready(true); ((PostDetailBaseVideoAutoplayViewHolder) holder).bindVideoUri(Uri.parse(mPost.getVideoUrl())); } @Override public void failed(@Nullable Integer messageRes) { ((PostDetailBaseVideoAutoplayViewHolder) holder).loadFallbackDirectVideo(); } }); } else { ((PostDetailBaseVideoAutoplayViewHolder) holder).bindVideoUri(Uri.parse(mPost.getVideoUrl())); } } else if (holder instanceof PostDetailVideoAndGifPreviewHolder) { if (!mHidePostType) { if (mPost.getPostType() == Post.GIF_TYPE) { ((PostDetailVideoAndGifPreviewHolder) holder).binding.typeTextViewItemPostDetailVideoAndGifPreview.setText(mActivity.getString(R.string.gif)); } else { ((PostDetailVideoAndGifPreviewHolder) holder).binding.typeTextViewItemPostDetailVideoAndGifPreview.setText(mActivity.getString(R.string.video)); } } Post.Preview preview = getSuitablePreview(mPost.getPreviews()); if (preview != null) { ((PostDetailVideoAndGifPreviewHolder) holder).binding.imageViewItemPostDetailVideoAndGifPreview.setRatio((float) preview.getPreviewHeight() / (float) preview.getPreviewWidth()); loadImage((PostDetailVideoAndGifPreviewHolder) holder, preview); } } else if (holder instanceof PostDetailImageAndGifAutoplayViewHolder) { if (!mHidePostType) { if (mPost.getPostType() == Post.IMAGE_TYPE) { ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.typeTextViewItemPostDetailImageAndGifAutoplay.setText(R.string.image); } else { ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.typeTextViewItemPostDetailImageAndGifAutoplay.setText(R.string.gif); } } Post.Preview preview = getSuitablePreview(mPost.getPreviews()); if (preview != null) { if (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0) { int height = (int) (400 * mScale); ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.imageViewItemPostDetailImageAndGifAutoplay.setScaleType(ImageView.ScaleType.CENTER_CROP); ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.imageViewItemPostDetailImageAndGifAutoplay.getLayoutParams().height = height; } else { ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.imageViewItemPostDetailImageAndGifAutoplay.setRatio((float) preview.getPreviewHeight() / (float) preview.getPreviewWidth()); } loadImage((PostDetailImageAndGifAutoplayViewHolder) holder, preview); } } else if (holder instanceof PostDetailLinkViewHolder) { String domain = Uri.parse(mPost.getUrl()).getHost(); ((PostDetailLinkViewHolder) holder).binding.linkTextViewItemPostDetailLink.setText(domain); Post.Preview preview = getSuitablePreview(mPost.getPreviews()); if (preview != null) { ((PostDetailLinkViewHolder) holder).binding.imageViewItemPostDetailLink.setRatio((float) preview.getPreviewHeight() / (float) preview.getPreviewWidth()); loadImage((PostDetailLinkViewHolder) holder, preview); } } else if (holder instanceof PostDetailNoPreviewViewHolder) { if (mPost.getPostType() == Post.LINK_TYPE || mPost.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { if (!mHidePostType) { ((PostDetailNoPreviewViewHolder) holder).binding.typeTextViewItemPostDetailNoPreview.setText(R.string.link); } String noPreviewLinkDomain = Uri.parse(mPost.getUrl()).getHost(); ((PostDetailNoPreviewViewHolder) holder).binding.linkTextViewItemPostDetailNoPreview.setVisibility(View.VISIBLE); ((PostDetailNoPreviewViewHolder) holder).binding.linkTextViewItemPostDetailNoPreview.setText(noPreviewLinkDomain); ((PostDetailNoPreviewViewHolder) holder).binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setImageResource(R.drawable.ic_link_day_night_24dp); } else { ((PostDetailNoPreviewViewHolder) holder).binding.linkTextViewItemPostDetailNoPreview.setVisibility(View.GONE); switch (mPost.getPostType()) { case Post.VIDEO_TYPE: if (!mHidePostType) { ((PostDetailNoPreviewViewHolder) holder).binding.typeTextViewItemPostDetailNoPreview.setText(R.string.video); } ((PostDetailNoPreviewViewHolder) holder).binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setImageResource(R.drawable.ic_video_day_night_24dp); break; case Post.IMAGE_TYPE: if (!mHidePostType) { ((PostDetailNoPreviewViewHolder) holder).binding.typeTextViewItemPostDetailNoPreview.setText(R.string.image); } ((PostDetailNoPreviewViewHolder) holder).binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setImageResource(R.drawable.ic_image_day_night_24dp); break; case Post.GIF_TYPE: if (!mHidePostType) { ((PostDetailNoPreviewViewHolder) holder).binding.typeTextViewItemPostDetailNoPreview.setText(R.string.gif); } ((PostDetailNoPreviewViewHolder) holder).binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setImageResource(R.drawable.ic_image_day_night_24dp); break; case Post.GALLERY_TYPE: if (!mHidePostType) { ((PostDetailNoPreviewViewHolder) holder).binding.typeTextViewItemPostDetailNoPreview.setText(R.string.gallery); } ((PostDetailNoPreviewViewHolder) holder).binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setImageResource(R.drawable.ic_gallery_day_night_24dp); break; } } } else if (holder instanceof PostDetailGalleryViewHolder) { if (mDataSavingMode && mDisableImagePreview) { ((PostDetailGalleryViewHolder) holder).binding.noPreviewPostTypeImageViewItemPostDetailGallery.setVisibility(View.VISIBLE); ((PostDetailGalleryViewHolder) holder).binding.noPreviewPostTypeImageViewItemPostDetailGallery.setImageResource(R.drawable.ic_gallery_day_night_24dp); } else { ((PostDetailGalleryViewHolder) holder).binding.galleryFrameLayoutItemPostDetailGallery.setVisibility(View.VISIBLE); ((PostDetailGalleryViewHolder) holder).binding.imageIndexTextViewItemPostDetailGallery.setText(mActivity.getString(R.string.image_index_in_gallery, 1, mPost.getGallery().size())); Post.Preview preview = getSuitablePreview(mPost.getPreviews()); if (preview != null) { if (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0) { ((PostDetailGalleryViewHolder) holder).adapter.setRatio(-1); } else { ((PostDetailGalleryViewHolder) holder).adapter.setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } } else { ((PostDetailGalleryViewHolder) holder).adapter.setRatio(-1); } ((PostDetailGalleryViewHolder) holder).adapter.setBlurImage( (mPost.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (mPost.isSpoiler() && mNeedBlurSpoiler)); // Delay setGalleryImages until the gallery RecyclerView has been // laid out with its final width. In the split post/comments view, // the RecyclerView may have width=0 during initial bind because the // weighted LinearLayout hasn't distributed widths yet. Without this, // gallery items get bound at a tiny size (e.g. 122px) and Glide // decodes images at that resolution, causing severe pixelation. ArrayList gallery = mPost.getGallery(); int rvWidth = ((PostDetailGalleryViewHolder) holder).binding.galleryRecyclerViewItemPostDetailGallery.getWidth(); if (rvWidth > 0) { ((PostDetailGalleryViewHolder) holder).adapter.setGalleryImages(gallery); } else { ((PostDetailGalleryViewHolder) holder).binding.galleryRecyclerViewItemPostDetailGallery.post(() -> ((PostDetailGalleryViewHolder) holder).adapter.setGalleryImages(gallery) ); } } } } } @Nullable private Post.Preview getSuitablePreview(List previews) { Post.Preview preview; if (!previews.isEmpty()) { int previewIndex; if (mDataSavingMode && previews.size() > 2) { previewIndex = previews.size() / 2; } else { previewIndex = 0; } preview = previews.get(previewIndex); if (preview.getPreviewWidth() * preview.getPreviewHeight() > mMaxResolution) { for (int i = previews.size() - 1; i >= 1; i--) { preview = previews.get(i); if (preview.getPreviewWidth() * preview.getPreviewHeight() <= mMaxResolution) { return preview; } } } return preview; } // Thumbnail fallback for post detail view String thumbnailUrl = mPost.getThumbnailUrl(); if (thumbnailUrl != null && !thumbnailUrl.isEmpty() && !thumbnailUrl.equals("self") && !thumbnailUrl.equals("default") && !thumbnailUrl.equals("nsfw") && !thumbnailUrl.equals("spoiler") && !thumbnailUrl.equals("image") && thumbnailUrl.startsWith("http")) { return new Post.Preview(thumbnailUrl, 0, 0, "", ""); } return null; } private void loadImage(PostDetailBaseViewHolder holder, @NonNull Post.Preview preview) { if (holder instanceof PostDetailImageAndGifAutoplayViewHolder) { boolean blurImage = (mPost.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit()) && !(mPost.getPostType() == Post.GIF_TYPE && mAutoplayNsfwVideos)) || (mPost.isSpoiler() && mNeedBlurSpoiler); String url = mPost.getPostType() == Post.IMAGE_TYPE || blurImage ? preview.getPreviewUrl() : mPost.getUrl(); RequestBuilder imageRequestBuilder = mGlide.load(url) .listener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.progressBarItemPostDetailImageAndGifAutoplay.setVisibility(View.GONE); ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.loadImageErrorTextViewItemPostDetailImageAndGifAutoplay.setVisibility(View.VISIBLE); ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.loadImageErrorTextViewItemPostDetailImageAndGifAutoplay.setOnClickListener(view -> { ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.progressBarItemPostDetailImageAndGifAutoplay.setVisibility(View.VISIBLE); ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.loadImageErrorTextViewItemPostDetailImageAndGifAutoplay.setVisibility(View.GONE); loadImage(holder, preview); }); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { ((PostDetailImageAndGifAutoplayViewHolder) holder).binding.loadWrapperItemPostDetailImageAndGifAutoplay.setVisibility(View.GONE); return false; } }); if (blurImage) { imageRequestBuilder.apply(RequestOptions.bitmapTransform(new BlurTransformation(50, 10))).into(((PostDetailImageAndGifAutoplayViewHolder) holder).binding.imageViewItemPostDetailImageAndGifAutoplay); } else { imageRequestBuilder.centerInside().downsample(mSaveMemoryCenterInsideDownsampleStrategy).into(((PostDetailImageAndGifAutoplayViewHolder) holder).binding.imageViewItemPostDetailImageAndGifAutoplay); } } else if (holder instanceof PostDetailVideoAndGifPreviewHolder) { RequestBuilder imageRequestBuilder = mGlide.load(preview.getPreviewUrl()) .listener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { ((PostDetailVideoAndGifPreviewHolder) holder).binding.progressBarItemPostDetailVideoAndGifPreview.setVisibility(View.GONE); ((PostDetailVideoAndGifPreviewHolder) holder).binding.loadImageErrorTextViewItemPostDetailVideoAndGifPreview.setVisibility(View.VISIBLE); ((PostDetailVideoAndGifPreviewHolder) holder).binding.loadImageErrorTextViewItemPostDetailVideoAndGifPreview.setOnClickListener(view -> { ((PostDetailVideoAndGifPreviewHolder) holder).binding.progressBarItemPostDetailVideoAndGifPreview.setVisibility(View.VISIBLE); ((PostDetailVideoAndGifPreviewHolder) holder).binding.loadImageErrorTextViewItemPostDetailVideoAndGifPreview.setVisibility(View.GONE); loadImage(holder, preview); }); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { ((PostDetailVideoAndGifPreviewHolder) holder).binding.loadWrapperItemPostDetailVideoAndGifPreview.setVisibility(View.GONE); return false; } }); if ((mPost.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (mPost.isSpoiler() && mNeedBlurSpoiler)) { imageRequestBuilder.apply(RequestOptions.bitmapTransform(new BlurTransformation(50, 10))) .into(((PostDetailVideoAndGifPreviewHolder) holder).binding.imageViewItemPostDetailVideoAndGifPreview); } else { imageRequestBuilder.centerInside().downsample(mSaveMemoryCenterInsideDownsampleStrategy).into(((PostDetailVideoAndGifPreviewHolder) holder).binding.imageViewItemPostDetailVideoAndGifPreview); } } else if (holder instanceof PostDetailLinkViewHolder) { RequestBuilder imageRequestBuilder = mGlide.load(preview.getPreviewUrl()) .listener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { ((PostDetailLinkViewHolder) holder).binding.progressBarItemPostDetailLink.setVisibility(View.GONE); ((PostDetailLinkViewHolder) holder).binding.loadImageErrorTextViewItemPostDetailLink.setVisibility(View.VISIBLE); ((PostDetailLinkViewHolder) holder).binding.loadImageErrorTextViewItemPostDetailLink.setOnClickListener(view -> { ((PostDetailLinkViewHolder) holder).binding.progressBarItemPostDetailLink.setVisibility(View.VISIBLE); ((PostDetailLinkViewHolder) holder).binding.loadImageErrorTextViewItemPostDetailLink.setVisibility(View.GONE); loadImage(holder, preview); }); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { ((PostDetailLinkViewHolder) holder).binding.loadWrapperItemPostDetailLink.setVisibility(View.GONE); return false; } }); if ((mPost.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (mPost.isSpoiler() && mNeedBlurSpoiler)) { imageRequestBuilder.apply(RequestOptions.bitmapTransform(new BlurTransformation(50, 10))) .into(((PostDetailLinkViewHolder) holder).binding.imageViewItemPostDetailLink); } else { imageRequestBuilder.centerInside().downsample(mSaveMemoryCenterInsideDownsampleStrategy).into(((PostDetailLinkViewHolder) holder).binding.imageViewItemPostDetailLink); } } } public void updatePost(Post post) { mPost = post; notifyItemChanged(0); } public void setBlurNsfwAndDoNotBlurNsfwInNsfwSubreddits(boolean needBlurNsfw, boolean doNotBlurNsfwInNsfwSubreddits) { mNeedBlurNsfw = needBlurNsfw; mDoNotBlurNsfwInNsfwSubreddits = doNotBlurNsfwInNsfwSubreddits; } public void setBlurSpoiler(boolean needBlurSpoiler) { mNeedBlurSpoiler = needBlurSpoiler; } public void setAutoplay(boolean autoplay) { mAutoplay = autoplay; } public void setDataSavingMode(boolean dataSavingMode) { mDataSavingMode = dataSavingMode; mEmotePlugin.setDataSavingMode(dataSavingMode); mImageAndGifEntry.setDataSavingMode(dataSavingMode); } public void onItemSwipe(RecyclerView.ViewHolder viewHolder, int direction, int swipeLeftAction, int swipeRightAction) { if (viewHolder instanceof PostDetailBaseViewHolder) { if (direction == ItemTouchHelper.LEFT || direction == ItemTouchHelper.START) { if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((PostDetailBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((PostDetailBaseViewHolder) viewHolder).downvoteButton.performClick(); } } else { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((PostDetailBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((PostDetailBaseViewHolder) viewHolder).downvoteButton.performClick(); } } } } public void addOneComment() { if (mPost != null) { mPost.setNComments(mPost.getNComments() + 1); notifyItemChanged(0); } } private void openMedia(Post post) { openMedia(post, 0); } private void openMedia(Post post, int galleryItemIndex) { openMedia(post, galleryItemIndex, -1); } private void openMedia(Post post, long videoProgress) { openMedia(post, 0, videoProgress); } private void openMedia(Post post, int galleryItemIndex, long videoProgress) { if (canStartActivity) { canStartActivity = false; if (post.getPostType() == Post.VIDEO_TYPE) { Intent intent = new Intent(mActivity, ViewVideoActivity.class); if (post.isImgur()) { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_IMGUR); } else if (post.isRedgifs()) { intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_REDGIFS); intent.putExtra(ViewVideoActivity.EXTRA_REDGIFS_ID, post.getRedgifsId()); intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); /*if (post.isLoadRedgifsOrStreamableVideoSuccess()) { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); }*/ } else if (post.isStreamable()) { intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_STREAMABLE); intent.putExtra(ViewVideoActivity.EXTRA_STREAMABLE_SHORT_CODE, post.getStreamableShortCode()); if (post.isLoadedStreamableVideoAlready()) { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); } } else { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_SUBREDDIT, post.getSubredditName()); intent.putExtra(ViewVideoActivity.EXTRA_ID, post.getId()); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); } intent.putExtra(ViewVideoActivity.EXTRA_POST, post); if (videoProgress > 0) { intent.putExtra(ViewVideoActivity.EXTRA_PROGRESS_SECONDS, videoProgress); } intent.putExtra(ViewVideoActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } else if (post.getPostType() == Post.IMAGE_TYPE) { Intent intent = new Intent(mActivity, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, post.getUrl()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, post.getSubredditName() + "-" + post.getId() + ".jpg"); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } else if (post.getPostType() == Post.GIF_TYPE) { if (post.getMp4Variant() != null) { Intent intent = new Intent(mActivity, ViewVideoActivity.class); intent.setData(Uri.parse(post.getMp4Variant())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_DIRECT); intent.putExtra(ViewVideoActivity.EXTRA_SUBREDDIT, post.getSubredditName()); intent.putExtra(ViewVideoActivity.EXTRA_ID, post.getId()); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getMp4Variant()); intent.putExtra(ViewVideoActivity.EXTRA_POST, post); intent.putExtra(ViewVideoActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } else { Intent intent = new Intent(mActivity, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, post.getSubredditName() + "-" + post.getId() + ".gif"); intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, post.getVideoUrl()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } } else if (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(post.getUrl()); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } else if (post.getPostType() == Post.GALLERY_TYPE) { Intent intent = new Intent(mActivity, ViewRedditGalleryActivity.class); intent.putExtra(ViewRedditGalleryActivity.EXTRA_POST, post); intent.putExtra(ViewRedditGalleryActivity.EXTRA_GALLERY_ITEM_INDEX, galleryItemIndex); mActivity.startActivity(intent); } } } @OptIn(markerClass = UnstableApi.class) @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof PostDetailBaseViewHolder) { ((PostDetailBaseViewHolder) holder).userTextView.setTextColor(mUsernameColor); ((PostDetailBaseViewHolder) holder).userTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); ((PostDetailBaseViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); ((PostDetailBaseViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); ((PostDetailBaseViewHolder) holder).scoreTextView.setTextColor(mPostIconAndInfoColor); ((PostDetailBaseViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); ((PostDetailBaseViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); ((PostDetailBaseViewHolder) holder).flairTextView.setVisibility(View.GONE); ((PostDetailBaseViewHolder) holder).lockedImageView.setVisibility(View.GONE); ((PostDetailBaseViewHolder) holder).spoilerTextView.setVisibility(View.GONE); ((PostDetailBaseViewHolder) holder).nsfwTextView.setVisibility(View.GONE); ((PostDetailBaseViewHolder) holder).contentMarkdownView.setVisibility(View.GONE); if (holder instanceof PostDetailBaseVideoAutoplayViewHolder) { if (((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall != null && !((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall.isCanceled()) { ((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall.cancel(); ((PostDetailBaseVideoAutoplayViewHolder) holder).fetchRedgifsOrStreamableVideoCall = null; } ((PostDetailBaseVideoAutoplayViewHolder) holder).mErrorLoadingRedgifsImageView.setVisibility(View.GONE); ((PostDetailBaseVideoAutoplayViewHolder) holder).videoQualityButton.setVisibility(View.GONE); ((PostDetailBaseVideoAutoplayViewHolder) holder).muteButton.setVisibility(View.GONE); if (!((PostDetailBaseVideoAutoplayViewHolder) holder).isManuallyPaused) { ((PostDetailBaseVideoAutoplayViewHolder) holder).resetVolume(); } mGlide.clear(((PostDetailBaseVideoAutoplayViewHolder) holder).previewImageView); ((PostDetailBaseVideoAutoplayViewHolder) holder).previewImageView.setVisibility(View.GONE); ((PostDetailBaseVideoAutoplayViewHolder) holder).setDefaultResolutionAlready = false; } else if (holder instanceof PostDetailVideoAndGifPreviewHolder) { mGlide.clear(((PostDetailVideoAndGifPreviewHolder) holder).binding.imageViewItemPostDetailVideoAndGifPreview); } else if (holder instanceof PostDetailImageAndGifAutoplayViewHolder) { mGlide.clear(((PostDetailImageAndGifAutoplayViewHolder) holder).binding.imageViewItemPostDetailImageAndGifAutoplay); } else if (holder instanceof PostDetailLinkViewHolder) { mGlide.clear(((PostDetailLinkViewHolder) holder).binding.imageViewItemPostDetailLink); } else if (holder instanceof PostDetailGalleryViewHolder) { ((PostDetailGalleryViewHolder) holder).binding.galleryFrameLayoutItemPostDetailGallery.setVisibility(View.GONE); ((PostDetailGalleryViewHolder) holder).binding.noPreviewPostTypeImageViewItemPostDetailGallery.setVisibility(View.GONE); } } } @Override public int getItemCount() { return 1; } public void setCommentsSupplier(Supplier> supplier) { mCommentsSupplier = supplier; } @Nullable @Override public Object getKeyForOrder(int order) { return mPost; } @Nullable @Override public Integer getOrderForKey(@NonNull Object key) { return 0; } public void setCanPlayVideo(boolean canPlayVideo) { this.canPlayVideo = canPlayVideo; } public interface PostDetailRecyclerViewAdapterCallback { void updatePost(Post post); } public class PostDetailBaseViewHolder extends RecyclerView.ViewHolder { AspectRatioGifImageView iconGifImageView; TextView subredditTextView; TextView userTextView; TextView authorFlairTextView; TextView postTimeTextView; TextView titleTextView; CustomTextView typeTextView; ImageView crosspostImageView; ImageView archivedImageView; ImageView lockedImageView; CustomTextView nsfwTextView; CustomTextView spoilerTextView; CustomTextView flairTextView; TextView upvoteRatioTextView; RecyclerView contentMarkdownView; ConstraintLayout bottomConstraintLayout; MaterialButton upvoteButton; TextView scoreTextView; MaterialButton downvoteButton; MaterialButton commentsCountButton; MaterialButton saveButton; MaterialButton shareButton; PostDetailBaseViewHolder(@NonNull View itemView) { super(itemView); } void setBaseView(AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, TextView authorFlairTextView, TextView postTimeTextView, TextView titleTextView, CustomTextView typeTextView, ImageView crosspostImageView, ImageView archivedImageView, ImageView lockedImageView, CustomTextView nSFWTextView, CustomTextView spoilerTextView, CustomTextView flairTextView, TextView upvoteRatioTextView, RecyclerView contentMarkdownView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, MaterialButton commentsCountButton, MaterialButton saveButton, MaterialButton shareButton) { this.iconGifImageView = iconGifImageView; this.subredditTextView = subredditTextView; this.userTextView = userTextView; this.authorFlairTextView = authorFlairTextView; this.postTimeTextView = postTimeTextView; this.titleTextView = titleTextView; this.typeTextView = typeTextView; this.crosspostImageView = crosspostImageView; this.archivedImageView = archivedImageView; this.lockedImageView = lockedImageView; this.nsfwTextView = nSFWTextView; this.spoilerTextView = spoilerTextView; this.flairTextView = flairTextView; this.upvoteRatioTextView = upvoteRatioTextView; this.contentMarkdownView = contentMarkdownView; this.bottomConstraintLayout = bottomConstraintLayout; this.upvoteButton = upvoteButton; this.scoreTextView = scoreTextView; this.downvoteButton = downvoteButton; this.commentsCountButton = commentsCountButton; this.saveButton = saveButton; this.shareButton = shareButton; itemView.setOnLongClickListener(v -> { PostOptionsBottomSheetFragment postOptionsBottomSheetFragment; if (mPost.getPostType() == Post.GALLERY_TYPE && this instanceof PostDetailGalleryViewHolder) { postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(mPost, mFragment.getPostListPosition(), ((LinearLayoutManagerBugFixed) ((PostDetailGalleryViewHolder) this).binding.galleryRecyclerViewItemPostDetailGallery.getLayoutManager()).findFirstVisibleItemPosition()); } else { postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(mPost, mFragment.getPostListPosition()); } postOptionsBottomSheetFragment.show(mFragment.getChildFragmentManager(), postOptionsBottomSheetFragment.getTag()); return true; }); iconGifImageView.setOnClickListener(view -> subredditTextView.performClick()); subredditTextView.setOnClickListener(view -> { Intent intent; intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, mPost.getSubredditName()); mActivity.startActivity(intent); }); userTextView.setOnClickListener(view -> { if (mPost.isAuthorDeleted()) { return; } Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, mPost.getAuthor()); mActivity.startActivity(intent); }); authorFlairTextView.setOnClickListener(view -> userTextView.performClick()); crosspostImageView.setOnClickListener(view -> { Intent crosspostIntent = new Intent(mActivity, ViewPostDetailActivity.class); crosspostIntent.putExtra(ViewPostDetailActivity.EXTRA_POST_ID, mPost.getCrosspostParentId()); mActivity.startActivity(crosspostIntent); }); if (!mHidePostType) { typeTextView.setOnClickListener(view -> { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, mSubredditNamePrefixed.substring(2)); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, PostPagingSource.TYPE_SUBREDDIT); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, mPost.getPostType()); mActivity.startActivity(intent); }); } else { typeTextView.setVisibility(View.GONE); } if (!mHidePostFlair) { flairTextView.setOnClickListener(view -> { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, mSubredditNamePrefixed.substring(2)); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, PostPagingSource.TYPE_SUBREDDIT); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, mPost.getFlair()); mActivity.startActivity(intent); }); } else { flairTextView.setVisibility(View.GONE); } nSFWTextView.setOnClickListener(view -> { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, mSubredditNamePrefixed.substring(2)); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, PostPagingSource.TYPE_SUBREDDIT); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); mActivity.startActivity(intent); }); contentMarkdownView.setLayoutManager(new SwipeLockLinearLayoutManager(mActivity, new SwipeLockInterface() { @Override public void lockSwipe() { mActivity.lockSwipeRightToGoBack(); } @Override public void unlockSwipe() { mActivity.unlockSwipeRightToGoBack(); } })); mMarkwonAdapter.setOnLongClickListener(v -> { CopyTextBottomSheetFragment.show( mFragment.getChildFragmentManager(), mPost.getSelfTextPlain(), mPost.getSelfText() ); return true; }); upvoteButton.setOnClickListener(view -> { if (mPost.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_vote_unavailable, Toast.LENGTH_SHORT).show(); return; } if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } ColorStateList previousUpvoteButtonIconTint = upvoteButton.getIconTint(); ColorStateList previousDownvoteButtonIconTint = downvoteButton.getIconTint(); int previousScoreTextViewColor = scoreTextView.getCurrentTextColor(); Drawable previousUpvoteButtonDrawable = upvoteButton.getIcon(); Drawable previousDownvoteButtonDrawable = downvoteButton.getIcon(); int previousVoteType = mPost.getVoteType(); String newVoteType; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (previousVoteType != 1) { //Not upvoted before mPost.setVoteType(1); newVoteType = APIUtils.DIR_UPVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); } else { //Upvoted before mPost.setVoteType(0); newVoteType = APIUtils.DIR_UNVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, mPost.getScore() + mPost.getVoteType())); } VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingWithoutPositionListener() { @Override public void onVoteThingSuccess() { if (newVoteType.equals(APIUtils.DIR_UPVOTE)) { mPost.setVoteType(1); upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); } else { mPost.setVoteType(0); upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, mPost.getScore() + mPost.getVoteType())); } mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } @Override public void onVoteThingFail() { Toast.makeText(mActivity, R.string.vote_failed, Toast.LENGTH_SHORT).show(); mPost.setVoteType(previousVoteType); if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, mPost.getScore() + previousVoteType)); } upvoteButton.setIcon(previousUpvoteButtonDrawable); upvoteButton.setIconTint(previousUpvoteButtonIconTint); scoreTextView.setTextColor(previousScoreTextViewColor); downvoteButton.setIcon(previousDownvoteButtonDrawable); downvoteButton.setIconTint(previousDownvoteButtonIconTint); mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } }, mPost.getFullName(), newVoteType); }); scoreTextView.setOnClickListener(view -> { upvoteButton.performClick(); }); downvoteButton.setOnClickListener(view -> { if (mPost.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_vote_unavailable, Toast.LENGTH_SHORT).show(); return; } if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } ColorStateList previousUpvoteButtonIconTint = upvoteButton.getIconTint(); ColorStateList previousDownvoteButtonIconTint = downvoteButton.getIconTint(); int previousScoreTextViewColor = scoreTextView.getCurrentTextColor(); Drawable previousUpvoteButtonDrawable = upvoteButton.getIcon(); Drawable previousDownvoteButtonDrawable = downvoteButton.getIcon(); int previousVoteType = mPost.getVoteType(); String newVoteType; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (previousVoteType != -1) { //Not downvoted before mPost.setVoteType(-1); newVoteType = APIUtils.DIR_DOWNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); } else { //Downvoted before mPost.setVoteType(0); newVoteType = APIUtils.DIR_UNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, mPost.getScore() + mPost.getVoteType())); } VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingWithoutPositionListener() { @Override public void onVoteThingSuccess() { if (newVoteType.equals(APIUtils.DIR_DOWNVOTE)) { mPost.setVoteType(-1); downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); } else { mPost.setVoteType(0); downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, mPost.getScore() + mPost.getVoteType())); } mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } @Override public void onVoteThingFail() { Toast.makeText(mActivity, R.string.vote_failed, Toast.LENGTH_SHORT).show(); mPost.setVoteType(previousVoteType); if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, mPost.getScore() + previousVoteType)); } upvoteButton.setIcon(previousUpvoteButtonDrawable); upvoteButton.setIconTint(previousUpvoteButtonIconTint); scoreTextView.setTextColor(previousScoreTextViewColor); downvoteButton.setIcon(previousDownvoteButtonDrawable); downvoteButton.setIconTint(previousDownvoteButtonIconTint); mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } }, mPost.getFullName(), newVoteType); }); if (!mHideTheNumberOfComments) { this.commentsCountButton.setOnClickListener(view -> { if (mPost.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_comment_unavailable, Toast.LENGTH_SHORT).show(); return; } if (mPost.isLocked()) { Toast.makeText(mActivity, R.string.locked_post_comment_unavailable, Toast.LENGTH_SHORT).show(); return; } if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } Intent intent = new Intent(mActivity, CommentActivity.class); intent.putExtra(CommentActivity.EXTRA_PARENT_FULLNAME_KEY, mPost.getFullName()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_TITLE_KEY, mPost.getTitle()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_MARKDOWN_KEY, mPost.getSelfText()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_KEY, mPost.getSelfTextPlain()); intent.putExtra(CommentActivity.EXTRA_SUBREDDIT_NAME_KEY, mPost.getSubredditName()); intent.putExtra(CommentActivity.EXTRA_IS_REPLYING_KEY, false); intent.putExtra(CommentActivity.EXTRA_PARENT_DEPTH_KEY, 0); mActivity.startActivityForResult(intent, WRITE_COMMENT_REQUEST_CODE); }); } else { this.commentsCountButton.setVisibility(View.GONE); } this.saveButton.setOnClickListener(view -> { if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } if (mPost.isSaved()) { this.saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); SaveThing.unsaveThing(mOauthRetrofit, mAccessToken, mPost.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { mPost.setSaved(false); PostDetailBaseViewHolder.this.saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); Toast.makeText(mActivity, R.string.post_unsaved_success, Toast.LENGTH_SHORT).show(); mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } @Override public void failed() { mPost.setSaved(true); PostDetailBaseViewHolder.this.saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); Toast.makeText(mActivity, R.string.post_unsaved_failed, Toast.LENGTH_SHORT).show(); mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } }); } else { this.saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); SaveThing.saveThing(mOauthRetrofit, mAccessToken, mPost.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { mPost.setSaved(true); PostDetailBaseViewHolder.this.saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); Toast.makeText(mActivity, R.string.post_saved_success, Toast.LENGTH_SHORT).show(); mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } @Override public void failed() { mPost.setSaved(false); PostDetailBaseViewHolder.this.saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); Toast.makeText(mActivity, R.string.post_saved_failed, Toast.LENGTH_SHORT).show(); mPostDetailRecyclerViewAdapterCallback.updatePost(mPost); } }); } }); this.shareButton.setOnClickListener(view -> { Bundle bundle = new Bundle(); bundle.putString(ShareBottomSheetFragment.EXTRA_POST_LINK, mPost.getPermalink()); if (mPost.getPostType() != Post.TEXT_TYPE) { bundle.putInt(ShareBottomSheetFragment.EXTRA_MEDIA_TYPE, mPost.getPostType()); switch (mPost.getPostType()) { case Post.IMAGE_TYPE: case Post.GIF_TYPE: case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: bundle.putString(ShareBottomSheetFragment.EXTRA_MEDIA_LINK, mPost.getUrl()); break; case Post.VIDEO_TYPE: bundle.putString(ShareBottomSheetFragment.EXTRA_MEDIA_LINK, mPost.getVideoDownloadUrl()); break; } } bundle.putParcelable(ShareBottomSheetFragment.EXTRA_POST, mPost); if (mCommentsSupplier != null) { ArrayList comments = mCommentsSupplier.get(); if (comments != null && !comments.isEmpty()) { ArrayList topComments = new ArrayList<>(comments.subList(0, Math.min(10, comments.size()))); bundle.putParcelableArrayList(ShareBottomSheetFragment.EXTRA_COMMENTS, topComments); } } ShareBottomSheetFragment shareBottomSheetFragment = new ShareBottomSheetFragment(); shareBottomSheetFragment.setArguments(bundle); shareBottomSheetFragment.show(mFragment.getChildFragmentManager(), shareBottomSheetFragment.getTag()); }); this.shareButton.setOnLongClickListener(view -> { mActivity.copyLink(mPost.getPermalink()); return true; }); if (mVoteButtonsOnTheRight) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(bottomConstraintLayout); constraintSet.clear(upvoteButton.getId(), ConstraintSet.START); constraintSet.clear(scoreTextView.getId(), ConstraintSet.START); constraintSet.clear(downvoteButton.getId(), ConstraintSet.START); constraintSet.clear(saveButton.getId(), ConstraintSet.END); constraintSet.clear(shareButton.getId(), ConstraintSet.END); constraintSet.connect(upvoteButton.getId(), ConstraintSet.END, scoreTextView.getId(), ConstraintSet.START); constraintSet.connect(scoreTextView.getId(), ConstraintSet.END, downvoteButton.getId(), ConstraintSet.START); constraintSet.connect(downvoteButton.getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect(commentsCountButton.getId(), ConstraintSet.START, saveButton.getId(), ConstraintSet.END); constraintSet.connect(commentsCountButton.getId(), ConstraintSet.END, upvoteButton.getId(), ConstraintSet.START); constraintSet.connect(saveButton.getId(), ConstraintSet.START, shareButton.getId(), ConstraintSet.END); constraintSet.connect(shareButton.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.setHorizontalBias(commentsCountButton.getId(), 0); constraintSet.applyTo(bottomConstraintLayout); } if (mActivity.typeface != null) { subredditTextView.setTypeface(mActivity.typeface); userTextView.setTypeface(mActivity.typeface); authorFlairTextView.setTypeface(mActivity.typeface); postTimeTextView.setTypeface(mActivity.typeface); typeTextView.setTypeface(mActivity.typeface); spoilerTextView.setTypeface(mActivity.typeface); nSFWTextView.setTypeface(mActivity.typeface); flairTextView.setTypeface(mActivity.typeface); upvoteRatioTextView.setTypeface(mActivity.typeface); upvoteButton.setTypeface(mActivity.typeface); commentsCountButton.setTypeface(mActivity.typeface); } if (mActivity.titleTypeface != null) { titleTextView.setTypeface(mActivity.typeface); } itemView.setBackgroundColor(mCardViewColor); subredditTextView.setTextColor(mSubredditColor); userTextView.setTextColor(mUsernameColor); authorFlairTextView.setTextColor(mAuthorFlairTextColor); postTimeTextView.setTextColor(mSecondaryTextColor); titleTextView.setTextColor(mPostTitleColor); typeTextView.setBackgroundColor(mPostTypeBackgroundColor); typeTextView.setBorderColor(mPostTypeBackgroundColor); typeTextView.setTextColor(mPostTypeTextColor); spoilerTextView.setBackgroundColor(mSpoilerBackgroundColor); spoilerTextView.setBorderColor(mSpoilerBackgroundColor); spoilerTextView.setTextColor(mSpoilerTextColor); nSFWTextView.setBackgroundColor(mNSFWBackgroundColor); nSFWTextView.setBorderColor(mNSFWBackgroundColor); nSFWTextView.setTextColor(mNSFWTextColor); flairTextView.setBackgroundColor(mFlairBackgroundColor); flairTextView.setBorderColor(mFlairBackgroundColor); flairTextView.setTextColor(mFlairTextColor); archivedImageView.setColorFilter(mArchivedTintColor, PorterDuff.Mode.SRC_IN); lockedImageView.setColorFilter(mLockedTintColor, PorterDuff.Mode.SRC_IN); crosspostImageView.setColorFilter(mCrosspostTintColor, PorterDuff.Mode.SRC_IN); Drawable upvoteRatioDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_upvote_ratio_18dp, mUpvoteRatioTintColor); upvoteRatioTextView.setCompoundDrawablesWithIntrinsicBounds( upvoteRatioDrawable, null, null, null); upvoteRatioTextView.setTextColor(mSecondaryTextColor); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); commentsCountButton.setTextColor(mPostIconAndInfoColor); commentsCountButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); saveButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); shareButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); } } @UnstableApi class PostDetailBaseVideoAutoplayViewHolder extends PostDetailBaseViewHolder implements ToroPlayer { public Call fetchRedgifsOrStreamableVideoCall; AspectRatioFrameLayout aspectRatioFrameLayout; PlayerView playerView; GifImageView previewImageView; ImageView mErrorLoadingRedgifsImageView; ImageView videoQualityButton; ImageView muteButton; ImageView fullscreenButton; ImageView playPauseButton; DefaultTimeBar progressBar; @Nullable Container container; @Nullable ExoPlayerViewHelper helper; private Uri mediaUri; private float volume; private boolean isManuallyPaused; private Drawable playDrawable; private Drawable pauseDrawable; private boolean setDefaultResolutionAlready; public PostDetailBaseVideoAutoplayViewHolder(@NonNull View itemView, AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, TextView authorFlairTextView, TextView postTimeTextView, TextView titleTextView, CustomTextView typeTextView, ImageView crosspostImageView, ImageView archivedImageView, ImageView lockedImageView, CustomTextView nsfwTextView, CustomTextView spoilerTextView, CustomTextView flairTextView, TextView upvoteRatioTextView, AspectRatioFrameLayout aspectRatioFrameLayout, PlayerView playerView, GifImageView previewImageView, ImageView errorLoadingRedgifsImageView, ImageView videoQualityButton, ImageView muteButton, ImageView fullscreenButton, ImageView playPauseButton, DefaultTimeBar progressBar, RecyclerView contentMarkdownView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, MaterialButton commentsCountButton, MaterialButton saveButton, MaterialButton shareButton) { super(itemView); setBaseView(iconGifImageView, subredditTextView, userTextView, authorFlairTextView, postTimeTextView, titleTextView, typeTextView, crosspostImageView, archivedImageView, lockedImageView, nsfwTextView, spoilerTextView, flairTextView, upvoteRatioTextView, contentMarkdownView, bottomConstraintLayout, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); this.aspectRatioFrameLayout = aspectRatioFrameLayout; this.previewImageView = previewImageView; this.mErrorLoadingRedgifsImageView = errorLoadingRedgifsImageView; this.playerView = playerView; this.videoQualityButton = videoQualityButton; this.muteButton = muteButton; this.fullscreenButton = fullscreenButton; this.playPauseButton = playPauseButton; this.progressBar = progressBar; playDrawable = AppCompatResources.getDrawable(mActivity, R.drawable.ic_play_arrow_24dp); pauseDrawable = AppCompatResources.getDrawable(mActivity, R.drawable.ic_pause_24dp); aspectRatioFrameLayout.setOnClickListener(null); muteButton.setOnClickListener(view -> { if (helper != null) { if (helper.getVolume() != 0) { muteButton.setImageDrawable(AppCompatResources.getDrawable(mActivity, R.drawable.ic_mute_24dp)); helper.setVolume(0f); volume = 0f; } else { muteButton.setImageDrawable(AppCompatResources.getDrawable(mActivity, R.drawable.ic_unmute_24dp)); helper.setVolume(1f); volume = 1f; } } }); fullscreenButton.setOnClickListener(view -> { if (helper != null) { openMedia(mPost, helper.getLatestPlaybackInfo().getResumePosition()); } else { openMedia(mPost); } }); playPauseButton.setOnClickListener(view -> { if (isPlaying()) { pause(); isManuallyPaused = true; savePlaybackInfo(getPlayerOrder(), getCurrentPlaybackInfo()); } else { isManuallyPaused = false; play(); } }); progressBar.addListener(new TimeBar.OnScrubListener() { @Override public void onScrubStart(TimeBar timeBar, long position) { } @Override public void onScrubMove(TimeBar timeBar, long position) { } @Override public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { if (!canceled) { savePlaybackInfo(getPlayerOrder(), getCurrentPlaybackInfo()); } } }); previewImageView.setOnClickListener(view -> fullscreenButton.performClick()); playerView.setOnClickListener(view -> { if (mEasierToWatchInFullScreen && playerView.isControllerFullyVisible()) { fullscreenButton.performClick(); } }); } void bindVideoUri(Uri videoUri) { mediaUri = videoUri; } void setVolume(float volume) { this.volume = volume; } void resetVolume() { volume = 0f; } private void savePlaybackInfo(int order, @Nullable PlaybackInfo playbackInfo) { if (container != null) container.savePlaybackInfo(order, playbackInfo); } void loadFallbackDirectVideo() { if (mPost.getVideoFallBackDirectUrl() != null) { mediaUri = Uri.parse(mPost.getVideoFallBackDirectUrl()); mPost.setVideoDownloadUrl(mPost.getVideoFallBackDirectUrl()); mPost.setVideoUrl(mPost.getVideoFallBackDirectUrl()); mPost.setLoadedStreamableVideoAlready(true); if (container != null) { container.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); } } } @NonNull @Override public View getPlayerView() { return playerView; } @NonNull @Override public PlaybackInfo getCurrentPlaybackInfo() { return helper != null && mediaUri != null ? helper.getLatestPlaybackInfo() : new PlaybackInfo(); } @Override public void initialize(@NonNull Container container, @NonNull PlaybackInfo playbackInfo) { if (mediaUri == null) { return; } if (this.container == null) { this.container = container; } if (helper == null) { helper = new ExoPlayerViewHelper(this, mediaUri, null, mExoCreator); helper.addEventListener(new Playable.DefaultEventListener() { @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { if (events.containsAny( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { playPauseButton.setImageDrawable(Util.shouldShowPlayButton(player) ? playDrawable : pauseDrawable); } } @Override public void onTracksChanged(@NonNull Tracks tracks) { ImmutableList trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { if (mPost.isNormalVideo()) { videoQualityButton.setVisibility(View.VISIBLE); videoQualityButton.setOnClickListener(view -> { TrackSelectionDialogBuilder builder = new TrackSelectionDialogBuilder(mActivity, mActivity.getString(R.string.select_video_quality), helper.getPlayer(), C.TRACK_TYPE_VIDEO); builder.setShowDisableOption(true); builder.setAllowAdaptiveSelections(false); Dialog dialog = builder.setTheme(R.style.MaterialAlertDialogTheme).build(); dialog.show(); if (dialog instanceof AlertDialog) { ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mPrimaryTextColor); ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mPrimaryTextColor); } }); if (!setDefaultResolutionAlready) { int desiredResolution = 0; if (mDataSavingMode) { if (mDataSavingModeDefaultResolution > 0) { desiredResolution = mDataSavingModeDefaultResolution; } } else if (mNonDataSavingModeDefaultResolution > 0) { desiredResolution = mNonDataSavingModeDefaultResolution; } if (desiredResolution > 0) { TrackSelectionOverride trackSelectionOverride = null; int bestTrackIndex = -1; int bestResolution = -1; int worstResolution = Integer.MAX_VALUE; int worstTrackIndex = -1; Tracks.Group bestTrackGroup = null; Tracks.Group worstTrackGroup = null; for (Tracks.Group trackGroup : tracks.getGroups()) { if (trackGroup.getType() == C.TRACK_TYPE_VIDEO) { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { int trackResolution = Math.min(trackGroup.getTrackFormat(trackIndex).height, trackGroup.getTrackFormat(trackIndex).width); if (trackResolution <= desiredResolution && trackResolution > bestResolution) { bestTrackIndex = trackIndex; bestResolution = trackResolution; bestTrackGroup = trackGroup; } if (trackResolution < worstResolution) { worstTrackIndex = trackIndex; worstResolution = trackResolution; worstTrackGroup = trackGroup; } } } } if (bestTrackIndex != -1 && bestTrackGroup != null) { trackSelectionOverride = new TrackSelectionOverride( bestTrackGroup.getMediaTrackGroup(), ImmutableList.of(bestTrackIndex) ); } else if (worstTrackIndex != -1 && worstTrackGroup != null) { trackSelectionOverride = new TrackSelectionOverride( worstTrackGroup.getMediaTrackGroup(), ImmutableList.of(worstTrackIndex) ); } if (trackSelectionOverride != null) { helper.getPlayer().setTrackSelectionParameters( helper.getPlayer().getTrackSelectionParameters() .buildUpon() .addOverride(trackSelectionOverride) .build() ); } } setDefaultResolutionAlready = true; } } for (int i = 0; i < trackGroups.size(); i++) { String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { helper.setVolume(volume); muteButton.setVisibility(View.VISIBLE); if (volume != 0f) { muteButton.setImageDrawable(mActivity.getDrawable(R.drawable.ic_unmute_24dp)); } else { muteButton.setImageDrawable(mActivity.getDrawable(R.drawable.ic_mute_24dp)); } break; } } } else { muteButton.setVisibility(View.GONE); } } @Override public void onRenderedFirstFrame() { mGlide.clear(previewImageView); previewImageView.setVisibility(View.GONE); } @Override public void onPlayerError(@NonNull PlaybackException error) { if (mPost.getVideoFallBackDirectUrl() == null || mPost.getVideoFallBackDirectUrl().equals(mediaUri.toString())) { mErrorLoadingRedgifsImageView.setVisibility(View.VISIBLE); } else { loadFallbackDirectVideo(); } } }); } helper.initialize(container, playbackInfo); } @Override public void play() { if (helper != null && mediaUri != null) { if (!isPlaying() && isManuallyPaused) { helper.play(); pause(); helper.setVolume(volume); } else { helper.play(); } mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } @Override public void pause() { if (helper != null) { helper.pause(); mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } @Override public boolean isPlaying() { return helper != null && helper.isPlaying(); } @Override public void release() { if (helper != null) { helper.release(); helper = null; } container = null; } @Override public boolean wantsToPlay() { return canPlayVideo && mediaUri != null && ToroUtil.visibleAreaOffset(this, itemView.getParent()) >= mStartAutoplayVisibleAreaOffset; } @Override public int getPlayerOrder() { return 0; } } @UnstableApi class PostDetailVideoAutoplayViewHolder extends PostDetailBaseVideoAutoplayViewHolder { PostDetailVideoAutoplayViewHolder(@NonNull ItemPostDetailVideoAutoplayBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostDetailVideoAutoplay, binding.subredditTextViewItemPostDetailVideoAutoplay, binding.userTextViewItemPostDetailVideoAutoplay, binding.authorFlairTextViewItemPostDetailVideoAutoplay, binding.postTimeTextViewItemPostDetailVideoAutoplay, binding.titleTextViewItemPostDetailVideoAutoplay, binding.typeTextViewItemPostDetailVideoAutoplay, binding.crosspostImageViewItemPostDetailVideoAutoplay, binding.archivedImageViewItemPostDetailVideoAutoplay, binding.lockedImageViewItemPostDetailVideoAutoplay, binding.nsfwTextViewItemPostDetailVideoAutoplay, binding.spoilerCustomTextViewItemPostDetailVideoAutoplay, binding.flairCustomTextViewItemPostDetailVideoAutoplay, binding.upvoteRatioTextViewItemPostDetailVideoAutoplay, binding.aspectRatioFrameLayoutItemPostDetailVideoAutoplay, binding.playerViewItemPostDetailVideoAutoplay, binding.previewImageViewItemPostDetailVideoAutoplay, binding.errorLoadingVideoImageViewItemPostDetailVideoAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.contentMarkdownViewItemPostDetailVideoAutoplay, binding.bottomConstraintLayoutItemPostDetailVideoAutoplay, binding.upvoteButtonItemPostDetailVideoAutoplay, binding.scoreTextViewItemPostDetailVideoAutoplay, binding.downvoteButtonItemPostDetailVideoAutoplay, binding.commentsCountButtonItemPostDetailVideoAutoplay, binding.saveButtonItemPostDetailVideoAutoplay, binding.shareButtonItemPostDetailVideoAutoplay); } } @UnstableApi class PostDetailVideoAutoplayLegacyControllerViewHolder extends PostDetailBaseVideoAutoplayViewHolder { PostDetailVideoAutoplayLegacyControllerViewHolder(ItemPostDetailVideoAutoplayLegacyControllerBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostDetailVideoAutoplay, binding.subredditTextViewItemPostDetailVideoAutoplay, binding.userTextViewItemPostDetailVideoAutoplay, binding.authorFlairTextViewItemPostDetailVideoAutoplay, binding.postTimeTextViewItemPostDetailVideoAutoplay, binding.titleTextViewItemPostDetailVideoAutoplay, binding.typeTextViewItemPostDetailVideoAutoplay, binding.crosspostImageViewItemPostDetailVideoAutoplay, binding.archivedImageViewItemPostDetailVideoAutoplay, binding.lockedImageViewItemPostDetailVideoAutoplay, binding.nsfwTextViewItemPostDetailVideoAutoplay, binding.spoilerCustomTextViewItemPostDetailVideoAutoplay, binding.flairCustomTextViewItemPostDetailVideoAutoplay, binding.upvoteRatioTextViewItemPostDetailVideoAutoplay, binding.aspectRatioFrameLayoutItemPostDetailVideoAutoplay, binding.playerViewItemPostDetailVideoAutoplay, binding.previewImageViewItemPostDetailVideoAutoplay, binding.errorLoadingVideoImageViewItemPostDetailVideoAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.contentMarkdownViewItemPostDetailVideoAutoplay, binding.bottomConstraintLayoutItemPostDetailVideoAutoplay, binding.upvoteButtonItemPostDetailVideoAutoplay, binding.scoreTextViewItemPostDetailVideoAutoplay, binding.downvoteButtonItemPostDetailVideoAutoplay, binding.commentsCountButtonItemPostDetailVideoAutoplay, binding.saveButtonItemPostDetailVideoAutoplay, binding.shareButtonItemPostDetailVideoAutoplay); } } class PostDetailVideoAndGifPreviewHolder extends PostDetailBaseViewHolder { ItemPostDetailVideoAndGifPreviewBinding binding; PostDetailVideoAndGifPreviewHolder(@NonNull ItemPostDetailVideoAndGifPreviewBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.iconGifImageViewItemPostDetailVideoAndGifPreview, binding.subredditTextViewItemPostDetailVideoAndGifPreview, binding.userTextViewItemPostDetailVideoAndGifPreview, binding.authorFlairTextViewItemPostDetailVideoAndGifPreview, binding.postTimeTextViewItemPostDetailVideoAndGifPreview, binding.titleTextViewItemPostDetailVideoAndGifPreview, binding.typeTextViewItemPostDetailVideoAndGifPreview, binding.crosspostImageViewItemPostDetailVideoAndGifPreview, binding.archivedImageViewItemPostDetailVideoAndGifPreview, binding.lockedImageViewItemPostDetailVideoAndGifPreview, binding.nsfwTextViewItemPostDetailVideoAndGifPreview, binding.spoilerCustomTextViewItemPostDetailVideoAndGifPreview, binding.flairCustomTextViewItemPostDetailVideoAndGifPreview, binding.upvoteRatioTextViewItemPostDetailVideoAndGifPreview, binding.contentMarkdownViewItemPostDetailVideoAndGifPreview, binding.bottomConstraintLayoutItemPostDetailVideoAndGifPreview, binding.upvoteButtonItemPostDetailVideoAndGifPreview, binding.scoreTextViewItemPostDetailVideoAndGifPreview, binding.downvoteButtonItemPostDetailVideoAndGifPreview, binding.commentsCountButtonItemPostDetailVideoAndGifPreview, binding.saveButtonItemPostDetailVideoAndGifPreview, binding.shareButtonItemPostDetailVideoAndGifPreview); binding.videoOrGifIndicatorImageViewItemPostDetail.setColorFilter(mMediaIndicatorIconTint, PorterDuff.Mode.SRC_IN); binding.videoOrGifIndicatorImageViewItemPostDetail.setBackgroundTintList(ColorStateList.valueOf(mMediaIndicatorBackgroundColor)); binding.progressBarItemPostDetailVideoAndGifPreview.setIndicatorColor(mColorAccent); binding.loadImageErrorTextViewItemPostDetailVideoAndGifPreview.setTextColor(mPrimaryTextColor); binding.imageViewItemPostDetailVideoAndGifPreview.setOnClickListener(view -> { openMedia(mPost); }); binding.imageViewItemPostDetailVideoAndGifPreview.setOnLongClickListener(v -> { itemView.performLongClick(); return true; }); } } class PostDetailImageAndGifAutoplayViewHolder extends PostDetailBaseViewHolder { ItemPostDetailImageAndGifAutoplayBinding binding; PostDetailImageAndGifAutoplayViewHolder(@NonNull ItemPostDetailImageAndGifAutoplayBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.iconGifImageViewItemPostDetailImageAndGifAutoplay, binding.subredditTextViewItemPostDetailImageAndGifAutoplay, binding.userTextViewItemPostDetailImageAndGifAutoplay, binding.authorFlairTextViewItemPostDetailImageAndGifAutoplay, binding.postTimeTextViewItemPostDetailImageAndGifAutoplay, binding.titleTextViewItemPostDetailImageAndGifAutoplay, binding.typeTextViewItemPostDetailImageAndGifAutoplay, binding.crosspostImageViewItemPostDetailImageAndGifAutoplay, binding.archivedImageViewItemPostDetailImageAndGifAutoplay, binding.lockedImageViewItemPostDetailImageAndGifAutoplay, binding.nsfwTextViewItemPostDetailImageAndGifAutoplay, binding.spoilerCustomTextViewItemPostDetailImageAndGifAutoplay, binding.flairCustomTextViewItemPostDetailImageAndGifAutoplay, binding.upvoteRatioTextViewItemPostDetailImageAndGifAutoplay, binding.contentMarkdownViewItemPostDetailImageAndGifAutoplay, binding.bottomConstraintLayoutItemPostDetailImageAndGifAutoplay, binding.upvoteButtonItemPostDetailImageAndGifAutoplay, binding.scoreTextViewItemPostDetailImageAndGifAutoplay, binding.downvoteButtonItemPostDetailImageAndGifAutoplay, binding.commentsCountButtonItemPostDetailImageAndGifAutoplay, binding.saveButtonItemPostDetailImageAndGifAutoplay, binding.shareButtonItemPostDetailImageAndGifAutoplay); binding.progressBarItemPostDetailImageAndGifAutoplay.setIndicatorColor(mColorAccent); binding.loadImageErrorTextViewItemPostDetailImageAndGifAutoplay.setTextColor(mPrimaryTextColor); binding.imageViewItemPostDetailImageAndGifAutoplay.setOnClickListener(view -> { openMedia(mPost); }); binding.imageViewItemPostDetailImageAndGifAutoplay.setOnLongClickListener(view -> { itemView.performLongClick(); return true; }); } } class PostDetailLinkViewHolder extends PostDetailBaseViewHolder { ItemPostDetailLinkBinding binding; PostDetailLinkViewHolder(@NonNull ItemPostDetailLinkBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.iconGifImageViewItemPostDetailLink, binding.subredditTextViewItemPostDetailLink, binding.userTextViewItemPostDetailLink, binding.authorFlairTextViewItemPostDetailLink, binding.postTimeTextViewItemPostDetailLink, binding.titleTextViewItemPostDetailLink, binding.typeTextViewItemPostDetailLink, binding.crosspostImageViewItemPostDetailLink, binding.archivedImageViewItemPostDetailLink, binding.lockedImageViewItemPostDetailLink, binding.nsfwTextViewItemPostDetailLink, binding.spoilerCustomTextViewItemPostDetailLink, binding.flairCustomTextViewItemPostDetailLink, binding.upvoteRatioTextViewItemPostDetailLink, binding.contentMarkdownViewItemPostDetailLink, binding.bottomConstraintLayoutItemPostDetailLink, binding.upvoteButtonItemPostDetailLink, binding.scoreTextViewItemPostDetailLink, binding.downvoteButtonItemPostDetailLink, binding.commentsCountButtonItemPostDetailLink, binding.saveButtonItemPostDetailLink, binding.shareButtonItemPostDetailLink); if (mActivity.typeface != null) { binding.linkTextViewItemPostDetailLink.setTypeface(mActivity.typeface); } binding.linkTextViewItemPostDetailLink.setTextColor(mSecondaryTextColor); binding.progressBarItemPostDetailLink.setIndicatorColor(mColorAccent); binding.loadImageErrorTextViewItemPostDetailLink.setTextColor(mPrimaryTextColor); binding.imageViewItemPostDetailLink.setOnClickListener(view -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(mPost.getUrl()); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_IS_NSFW, mPost.isNSFW()); mActivity.startActivity(intent); }); binding.imageViewItemPostDetailLink.setOnLongClickListener(view -> { itemView.performLongClick(); return true; }); } } class PostDetailNoPreviewViewHolder extends PostDetailBaseViewHolder { ItemPostDetailNoPreviewBinding binding; PostDetailNoPreviewViewHolder(@NonNull ItemPostDetailNoPreviewBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.iconGifImageViewItemPostDetailNoPreview, binding.subredditTextViewItemPostDetailNoPreview, binding.userTextViewItemPostDetailNoPreview, binding.authorFlairTextViewItemPostDetailNoPreview, binding.postTimeTextViewItemPostDetailNoPreview, binding.titleTextViewItemPostDetailNoPreview, binding.typeTextViewItemPostDetailNoPreview, binding.crosspostImageViewItemPostDetailNoPreview, binding.archivedImageViewItemPostDetailNoPreview, binding.lockedImageViewItemPostDetailNoPreview, binding.nsfwTextViewItemPostDetailNoPreview, binding.spoilerCustomTextViewItemPostDetailNoPreview, binding.flairCustomTextViewItemPostDetailNoPreview, binding.upvoteRatioTextViewItemPostDetailNoPreview, binding.contentMarkdownViewItemPostDetailNoPreview, binding.bottomConstraintLayoutItemPostDetailNoPreview, binding.upvoteButtonItemPostDetailNoPreview, binding.scoreTextViewItemPostDetailNoPreview, binding.downvoteButtonItemPostDetailNoPreview, binding.commentsCountButtonItemPostDetailNoPreview, binding.saveButtonItemPostDetailNoPreview, binding.shareButtonItemPostDetailNoPreview); if (mActivity.typeface != null) { binding.linkTextViewItemPostDetailNoPreview.setTypeface(mActivity.typeface); } binding.linkTextViewItemPostDetailNoPreview.setTextColor(mSecondaryTextColor); binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setBackgroundColor(mNoPreviewPostTypeBackgroundColor); binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setColorFilter(mNoPreviewPostTypeIconTint, PorterDuff.Mode.SRC_IN); binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setOnClickListener(view -> { openMedia(mPost); }); binding.imageViewNoPreviewPostTypeItemPostDetailNoPreview.setOnLongClickListener(view -> { itemView.performLongClick(); return true; }); } } class PostDetailGalleryViewHolder extends PostDetailBaseViewHolder { ItemPostDetailGalleryBinding binding; PostGalleryTypeImageRecyclerViewAdapter adapter; PostDetailGalleryViewHolder(@NonNull ItemPostDetailGalleryBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.iconGifImageViewItemPostDetailGallery, binding.subredditTextViewItemPostDetailGallery, binding.userTextViewItemPostDetailGallery, binding.authorFlairTextViewItemPostDetailGallery, binding.postTimeTextViewItemPostDetailGallery, binding.titleTextViewItemPostDetailGallery, binding.typeTextViewItemPostDetailGallery, binding.crosspostImageViewItemPostDetailGallery, binding.archivedImageViewItemPostDetailGallery, binding.lockedImageViewItemPostDetailGallery, binding.nsfwTextViewItemPostDetailGallery, binding.spoilerCustomTextViewItemPostDetailGallery, binding.flairCustomTextViewItemPostDetailGallery, binding.upvoteRatioTextViewItemPostDetailGallery, binding.contentMarkdownViewItemPostDetailGallery, binding.bottomConstraintLayoutItemPostDetailGallery, binding.upvoteButtonItemPostDetailGallery, binding.scoreTextViewItemPostDetailGallery, binding.downvoteButtonItemPostDetailGallery, binding.commentsCountButtonItemPostDetailGallery, binding.saveButtonItemPostDetailGallery, binding.shareButtonItemPostDetailGallery); if (mActivity.typeface != null) { binding.imageIndexTextViewItemPostDetailGallery.setTypeface(mActivity.typeface); } binding.imageIndexTextViewItemPostDetailGallery.setTextColor(mMediaIndicatorIconTint); binding.imageIndexTextViewItemPostDetailGallery.setBackgroundColor(mMediaIndicatorBackgroundColor); binding.imageIndexTextViewItemPostDetailGallery.setBorderColor(mMediaIndicatorBackgroundColor); binding.noPreviewPostTypeImageViewItemPostDetailGallery.setBackgroundColor(mNoPreviewPostTypeBackgroundColor); binding.noPreviewPostTypeImageViewItemPostDetailGallery.setColorFilter(mNoPreviewPostTypeIconTint, PorterDuff.Mode.SRC_IN); adapter = new PostGalleryTypeImageRecyclerViewAdapter(mGlide, mActivity.typeface, mPostDetailMarkwon, mSaveMemoryCenterInsideDownsampleStrategy, mColorAccent, mPrimaryTextColor, mCardViewColor, mCommentColor, mScale); binding.galleryRecyclerViewItemPostDetailGallery.setAdapter(adapter); new PagerSnapHelper().attachToRecyclerView(binding.galleryRecyclerViewItemPostDetailGallery); LinearLayoutManagerBugFixed layoutManager = new LinearLayoutManagerBugFixed(mActivity, RecyclerView.HORIZONTAL, false); binding.galleryRecyclerViewItemPostDetailGallery.setLayoutManager(layoutManager); binding.galleryRecyclerViewItemPostDetailGallery.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); binding.imageIndexTextViewItemPostDetailGallery.setText(mActivity.getString(R.string.image_index_in_gallery, layoutManager.findFirstVisibleItemPosition() + 1, mPost.getGallery().size())); } }); binding.galleryRecyclerViewItemPostDetailGallery.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { private float downX; private float downY; private boolean dragged; private long downTime; private final int minTouchSlop = ViewConfiguration.get(mActivity).getScaledTouchSlop(); private final int longClickThreshold = ViewConfiguration.getLongPressTimeout(); private boolean longPressed; @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { int action = e.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downX = e.getRawX(); downY = e.getRawY(); downTime = System.currentTimeMillis(); if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(true); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(false); } mActivity.lockSwipeRightToGoBack(); break; case MotionEvent.ACTION_MOVE: if (Math.abs(e.getRawX() - downX) > minTouchSlop || Math.abs(e.getRawY() - downY) > minTouchSlop) { dragged = true; } if (!dragged && !longPressed) { if (System.currentTimeMillis() - downTime >= longClickThreshold) { itemView.performLongClick(); longPressed = true; } } if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(true); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(false); } mActivity.lockSwipeRightToGoBack(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (e.getActionMasked() == MotionEvent.ACTION_UP && !dragged) { if (System.currentTimeMillis() - downTime < longClickThreshold) { int position = getBindingAdapterPosition(); if (position >= 0) { if (mPost != null) { openMedia(mPost, layoutManager.findFirstVisibleItemPosition()); } } } } downX = 0; downY = 0; dragged = false; longPressed = false; if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(false); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(true); } mActivity.unlockSwipeRightToGoBack(); break; } return false; } @Override public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } }); binding.noPreviewPostTypeImageViewItemPostDetailGallery.setOnClickListener(view -> { openMedia(mPost, layoutManager.findFirstVisibleItemPosition()); }); } } class PostDetailTextViewHolder extends PostDetailBaseViewHolder { ItemPostDetailTextBinding binding; PostDetailTextViewHolder(@NonNull ItemPostDetailTextBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.iconGifImageViewItemPostDetailText, binding.subredditTextViewItemPostDetailText, binding.userTextViewItemPostDetailText, binding.authorFlairTextViewItemPostDetailText, binding.postTimeTextViewItemPostDetailText, binding.titleTextViewItemPostDetailText, binding.typeTextViewItemPostDetailText, binding.crosspostImageViewItemPostDetailText, binding.archivedImageViewItemPostDetailText, binding.lockedImageViewItemPostDetailText, binding.nsfwTextViewItemPostDetailText, binding.spoilerCustomTextViewItemPostDetailText, binding.flairCustomTextViewItemPostDetailText, binding.upvoteRatioTextViewItemPostDetailText, binding.contentMarkdownViewItemPostDetailText, binding.bottomConstraintLayoutItemPostDetailText, binding.upvoteButtonItemPostDetailText, binding.scoreTextViewItemPostDetailText, binding.downvoteButtonItemPostDetailText, binding.commentsCountButtonItemPostDetailText, binding.saveButtonItemPostDetailText, binding.shareButtonItemPostDetailText); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostFilterUsageRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; public class PostFilterUsageRecyclerViewAdapter extends RecyclerView.Adapter { private List postFilterUsages; private final BaseActivity activity; private final CustomThemeWrapper customThemeWrapper; private final OnItemClickListener onItemClickListener; public interface OnItemClickListener { void onClick(PostFilterUsage postFilterUsage); } public PostFilterUsageRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, OnItemClickListener onItemClickListener) { this.activity = activity; this.customThemeWrapper = customThemeWrapper; this.onItemClickListener = onItemClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new PostFilterUsageViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_post_filter_usage, parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { PostFilterUsage postFilterUsage = postFilterUsages.get(position); switch (postFilterUsage.usage) { case PostFilterUsage.HOME_TYPE: ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_home); break; case PostFilterUsage.SUBREDDIT_TYPE: if (postFilterUsage.nameOfUsage.equals(PostFilterUsage.NO_USAGE)) { ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_subreddit_all); } else { ((PostFilterUsageViewHolder) holder).usageTextView.setText(activity.getString(R.string.post_filter_usage_subreddit, postFilterUsage.nameOfUsage)); } break; case PostFilterUsage.USER_TYPE: if (postFilterUsage.nameOfUsage.equals(PostFilterUsage.NO_USAGE)) { ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_user_all); } else { ((PostFilterUsageViewHolder) holder).usageTextView.setText(activity.getString(R.string.post_filter_usage_user, postFilterUsage.nameOfUsage)); } break; case PostFilterUsage.MULTIREDDIT_TYPE: if (postFilterUsage.nameOfUsage.equals(PostFilterUsage.NO_USAGE)) { ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_multireddit_all); } else { ((PostFilterUsageViewHolder) holder).usageTextView.setText(activity.getString(R.string.post_filter_usage_multireddit, postFilterUsage.nameOfUsage)); } break; case PostFilterUsage.SEARCH_TYPE: ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_search); break; case PostFilterUsage.HISTORY_TYPE: ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_history); break; case PostFilterUsage.UPVOTED_TYPE: ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_upvoted); break; case PostFilterUsage.DOWNVOTED_TYPE: ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_downvoted); break; case PostFilterUsage.HIDDEN_TYPE: ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_hidden); break; case PostFilterUsage.SAVED_TYPE: ((PostFilterUsageViewHolder) holder).usageTextView.setText(R.string.post_filter_usage_saved); break; } } @Override public int getItemCount() { return postFilterUsages == null ? 0 : postFilterUsages.size(); } public void setPostFilterUsages(List postFilterUsages) { this.postFilterUsages = postFilterUsages; notifyDataSetChanged(); } private class PostFilterUsageViewHolder extends RecyclerView.ViewHolder { TextView usageTextView; public PostFilterUsageViewHolder(@NonNull View itemView) { super(itemView); usageTextView = (TextView) itemView; usageTextView.setTextColor(customThemeWrapper.getPrimaryTextColor()); if (activity.typeface != null) { usageTextView.setTypeface(activity.typeface); } usageTextView.setOnClickListener(view -> { onItemClickListener.onClick(postFilterUsages.get(getBindingAdapterPosition())); }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostFilterWithUsageRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.navigationdrawer.PostFilterUsageEmbeddedRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ItemPostFilterWithUsageBinding; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterWithUsage; import ml.docilealligator.infinityforreddit.utils.Utils; public class PostFilterWithUsageRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_HEADER = 1; private static final int VIEW_TYPE_POST_FILTER = 2; private final BaseActivity activity; private final CustomThemeWrapper customThemeWrapper; private final OnItemClickListener onItemClickListener; private List postFilterWithUsageList; private final RecyclerView.RecycledViewPool recycledViewPool; public interface OnItemClickListener { void onItemClick(PostFilter postFilter); } public PostFilterWithUsageRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, OnItemClickListener onItemClickListener) { this.activity = activity; this.customThemeWrapper = customThemeWrapper; this.recycledViewPool = new RecyclerView.RecycledViewPool(); this.onItemClickListener = onItemClickListener; } @Override public int getItemViewType(int position) { if (position == 0) { return VIEW_TYPE_HEADER; } return VIEW_TYPE_POST_FILTER; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_HEADER) { return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_filter_fragment_header, parent, false)); } else { return new PostFilterViewHolder(ItemPostFilterWithUsageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof PostFilterViewHolder) { ((PostFilterViewHolder) holder).binding.postFilterNameTextViewItemPostFilter.setText(postFilterWithUsageList.get(position - 1).postFilter.name); ((PostFilterViewHolder) holder).adapter.setPostFilterUsageList(postFilterWithUsageList.get(position - 1).postFilterUsages); } } @Override public int getItemCount() { return postFilterWithUsageList == null ? 1 : 1 + postFilterWithUsageList.size(); } public void setPostFilterWithUsageList(List postFilterWithUsageList) { this.postFilterWithUsageList = postFilterWithUsageList; notifyDataSetChanged(); } private class PostFilterViewHolder extends RecyclerView.ViewHolder { ItemPostFilterWithUsageBinding binding; PostFilterUsageEmbeddedRecyclerViewAdapter adapter; public PostFilterViewHolder(@NonNull ItemPostFilterWithUsageBinding binding) { super(binding.getRoot()); this.binding = binding; binding.postFilterNameTextViewItemPostFilter.setTextColor(customThemeWrapper.getPrimaryTextColor()); if (activity.typeface != null) { binding.postFilterNameTextViewItemPostFilter.setTypeface(activity.typeface); } binding.getRoot().setOnClickListener(view -> { onItemClickListener.onItemClick(postFilterWithUsageList.get(getBindingAdapterPosition() - 1).postFilter); }); binding.postFilterUsageRecyclerViewItemPostFilter.setRecycledViewPool(recycledViewPool); binding.postFilterUsageRecyclerViewItemPostFilter.setLayoutManager(new LinearLayoutManagerBugFixed(activity)); adapter = new PostFilterUsageEmbeddedRecyclerViewAdapter(activity); binding.postFilterUsageRecyclerViewItemPostFilter.setAdapter(adapter); } } private class HeaderViewHolder extends RecyclerView.ViewHolder { public HeaderViewHolder(@NonNull View itemView) { super(itemView); TextView infoTextView = itemView.findViewById(R.id.info_text_view_item_filter_fragment_header); infoTextView.setTextColor(customThemeWrapper.getSecondaryTextColor()); infoTextView.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(activity, R.drawable.ic_info_preference_day_night_24dp, activity.customThemeWrapper.getPrimaryIconColor()), null, null, null); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostGalleryTypeImageRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import java.util.ArrayList; import io.noties.markwon.Markwon; import jp.wasabeef.glide.transformations.BlurTransformation; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.databinding.ItemGalleryImageInPostFeedBinding; import ml.docilealligator.infinityforreddit.post.Post; public class PostGalleryTypeImageRecyclerViewAdapter extends RecyclerView.Adapter { private final RequestManager glide; private final Typeface typeface; private Markwon mPostDetailMarkwon; private final SaveMemoryCenterInisdeDownsampleStrategy saveMemoryCenterInisdeDownsampleStrategy; private final int mColorAccent; private final int mPrimaryTextColor; private int mCardViewColor; private int mCommentColor; private final float mScale; private ArrayList galleryImages; private boolean blurImage; private float ratio; private final boolean showCaption; public PostGalleryTypeImageRecyclerViewAdapter(RequestManager glide, Typeface typeface, SaveMemoryCenterInisdeDownsampleStrategy saveMemoryCenterInisdeDownsampleStrategy, int mColorAccent, int mPrimaryTextColor, float scale) { this.glide = glide; this.typeface = typeface; this.saveMemoryCenterInisdeDownsampleStrategy = saveMemoryCenterInisdeDownsampleStrategy; this.mColorAccent = mColorAccent; this.mPrimaryTextColor = mPrimaryTextColor; this.mScale = scale; showCaption = false; } public PostGalleryTypeImageRecyclerViewAdapter(RequestManager glide, Typeface typeface, Markwon postDetailMarkwon, SaveMemoryCenterInisdeDownsampleStrategy saveMemoryCenterInisdeDownsampleStrategy, int mColorAccent, int mPrimaryTextColor, int mCardViewColor, int mCommentColor, float scale) { this.glide = glide; this.typeface = typeface; this.mPostDetailMarkwon = postDetailMarkwon; this.saveMemoryCenterInisdeDownsampleStrategy = saveMemoryCenterInisdeDownsampleStrategy; this.mColorAccent = mColorAccent; this.mPrimaryTextColor = mPrimaryTextColor; this.mCardViewColor = mCardViewColor; this.mCommentColor = mCommentColor; this.mScale = scale; showCaption = true; } @NonNull @Override public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ImageViewHolder(ItemGalleryImageInPostFeedBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull ImageViewHolder holder, int position) { if (ratio < 0) { int height = (int) (400 * mScale); holder.binding.imageViewItemGalleryImageInPostFeed.setScaleType(ImageView.ScaleType.CENTER_CROP); holder.binding.imageViewItemGalleryImageInPostFeed.getLayoutParams().height = height; } else { holder.binding.imageViewItemGalleryImageInPostFeed.setRatio(ratio); } holder.binding.errorTextViewItemGalleryImageInPostFeed.setVisibility(View.GONE); holder.binding.progressBarItemGalleryImageInPostFeed.setVisibility(View.VISIBLE); holder.binding.imageViewItemGalleryImageInPostFeed.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int viewWidth = right - left; // In split/weighted layouts, views get intermediate layout passes with // incorrect (tiny) dimensions. Skip those and wait for the real size. ViewGroup parent = (ViewGroup) v.getParent(); while (parent != null && !(parent instanceof RecyclerView)) { parent = (ViewGroup) parent.getParent(); } if (parent != null && parent.getWidth() > 0 && viewWidth < parent.getWidth() / 2) { return; } holder.binding.imageViewItemGalleryImageInPostFeed.removeOnLayoutChangeListener(this); loadImage(holder); } }); if (showCaption) { loadCaptionPreview(holder); } } @Override public int getItemCount() { return galleryImages == null ? 0 : galleryImages.size(); } @Override public void onViewRecycled(@NonNull ImageViewHolder holder) { super.onViewRecycled(holder); holder.binding.captionConstraintLayoutItemGalleryImageInPostFeed.setVisibility(View.GONE); holder.binding.captionTextViewItemGalleryImageInPostFeed.setText(""); holder.binding.captionUrlTextViewItemGalleryImageInPostFeed.setText(""); holder.binding.progressBarItemGalleryImageInPostFeed.setVisibility(View.GONE); glide.clear(holder.binding.imageViewItemGalleryImageInPostFeed); } private void loadImage(ImageViewHolder holder) { if (galleryImages == null || galleryImages.isEmpty()) { return; } int index = holder.getBindingAdapterPosition(); if (index < 0 || index >= galleryImages.size()) { return; } RequestBuilder imageRequestBuilder = glide.load(galleryImages.get(index).url).listener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { holder.binding.progressBarItemGalleryImageInPostFeed.setVisibility(View.GONE); holder.binding.errorTextViewItemGalleryImageInPostFeed.setVisibility(View.VISIBLE); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { holder.binding.errorTextViewItemGalleryImageInPostFeed.setVisibility(View.GONE); holder.binding.progressBarItemGalleryImageInPostFeed.setVisibility(View.GONE); return false; } }); if (blurImage) { imageRequestBuilder.apply(RequestOptions.bitmapTransform(new BlurTransformation(50, 10))) .into(holder.binding.imageViewItemGalleryImageInPostFeed); } else { imageRequestBuilder.centerInside().downsample(saveMemoryCenterInisdeDownsampleStrategy).into(holder.binding.imageViewItemGalleryImageInPostFeed); } } private void loadCaptionPreview(ImageViewHolder holder) { if (galleryImages == null || galleryImages.isEmpty()) { return; } int index = holder.getBindingAdapterPosition(); if (index < 0 || index >= galleryImages.size()) { return; } String previewCaption = galleryImages.get(index).caption; String previewCaptionUrl = galleryImages.get(index).captionUrl; boolean previewCaptionIsEmpty = TextUtils.isEmpty(previewCaption); boolean previewCaptionUrlIsEmpty = TextUtils.isEmpty(previewCaptionUrl); if (!previewCaptionIsEmpty || !previewCaptionUrlIsEmpty) { holder.binding.captionConstraintLayoutItemGalleryImageInPostFeed.setBackgroundColor(mCardViewColor & 0x0D000000); // Make 10% darker than CardViewColor holder.binding.captionConstraintLayoutItemGalleryImageInPostFeed.setVisibility(View.VISIBLE); } if (!previewCaptionIsEmpty) { holder.binding.captionTextViewItemGalleryImageInPostFeed.setTextColor(mCommentColor); holder.binding.captionTextViewItemGalleryImageInPostFeed.setText(previewCaption); holder.binding.captionTextViewItemGalleryImageInPostFeed.setSelected(true); } if (!previewCaptionUrlIsEmpty) { String domain = Uri.parse(previewCaptionUrl).getHost(); domain = domain.startsWith("www.") ? domain.substring(4) : domain; mPostDetailMarkwon.setMarkdown(holder.binding.captionUrlTextViewItemGalleryImageInPostFeed, String.format("[%s](%s)", domain, previewCaptionUrl)); } } public void setGalleryImages(ArrayList galleryImages) { this.galleryImages = galleryImages; notifyDataSetChanged(); } public void setBlurImage(boolean blurImage) { this.blurImage = blurImage; } public void setRatio(float ratio) { this.ratio = ratio; } class ImageViewHolder extends RecyclerView.ViewHolder { ItemGalleryImageInPostFeedBinding binding; public ImageViewHolder(ItemGalleryImageInPostFeedBinding binding) { super(binding.getRoot()); this.binding = binding; if (typeface != null) { binding.errorTextViewItemGalleryImageInPostFeed.setTypeface(typeface); } binding.progressBarItemGalleryImageInPostFeed.setIndicatorColor(mColorAccent); binding.errorTextViewItemGalleryImageInPostFeed.setTextColor(mPrimaryTextColor); binding.errorTextViewItemGalleryImageInPostFeed.setOnClickListener(view -> { binding.progressBarItemGalleryImageInPostFeed.setVisibility(View.VISIBLE); binding.errorTextViewItemGalleryImageInPostFeed.setVisibility(View.GONE); loadImage(this); }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.content.res.AppCompatResources; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.content.ContextCompat; import androidx.media3.common.C; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.Tracks; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.ui.AspectRatioFrameLayout; import androidx.media3.ui.DefaultTimeBar; import androidx.media3.ui.PlayerView; import androidx.media3.ui.TimeBar; import androidx.media3.ui.TrackSelectionDialogBuilder; import androidx.paging.PagingDataAdapter; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.PagerSnapHelper; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.google.android.material.button.MaterialButton; import com.google.android.material.loadingindicator.LoadingIndicator; import com.google.common.collect.ImmutableList; import com.libRG.CustomTextView; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.Executor; import javax.inject.Provider; import jp.wasabeef.glide.transformations.BlurTransformation; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.FetchVideoLinkListener; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.FilteredPostsActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.ShareBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.AspectRatioGifImageView; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard2GalleryTypeBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard2TextBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard2VideoAutoplayBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard2VideoAutoplayLegacyControllerBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard2WithPreviewBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard3GalleryTypeBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard3TextBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard3VideoTypeAutoplayBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard3VideoTypeAutoplayLegacyControllerBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCard3WithPreviewBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCompact2Binding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCompact2RightThumbnailBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCompactBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostCompactRightThumbnailBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostGalleryBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostGalleryGalleryTypeBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostGalleryTypeBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostTextBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostVideoTypeAutoplayBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostVideoTypeAutoplayLegacyControllerBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPostWithPreviewBinding; import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostDetailFragment; import ml.docilealligator.infinityforreddit.fragments.PostFragmentBase; import ml.docilealligator.infinityforreddit.post.FetchStreamableVideo; import ml.docilealligator.infinityforreddit.post.MarkPostAsReadInterface; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.thing.SaveThing; import ml.docilealligator.infinityforreddit.thing.StreamableVideo; import ml.docilealligator.infinityforreddit.thing.VoteThing; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager; import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator; import ml.docilealligator.infinityforreddit.videoautoplay.ExoPlayerViewHelper; import ml.docilealligator.infinityforreddit.videoautoplay.MultiPlayPlayerSelector; import ml.docilealligator.infinityforreddit.videoautoplay.Playable; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; import pl.droidsonroids.gif.GifImageView; import retrofit2.Call; import retrofit2.Retrofit; /** * Created by alex on 2/25/18. */ public class PostRecyclerViewAdapter extends PagingDataAdapter implements CacheManager { private static final int VIEW_TYPE_POST_CARD_VIDEO_AUTOPLAY_TYPE = 1; private static final int VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE = 2; private static final int VIEW_TYPE_POST_CARD_GALLERY_TYPE = 3; private static final int VIEW_TYPE_POST_CARD_TEXT_TYPE = 4; private static final int VIEW_TYPE_POST_COMPACT = 5; private static final int VIEW_TYPE_POST_COMPACT_2 = 6; private static final int VIEW_TYPE_POST_GALLERY = 7; private static final int VIEW_TYPE_POST_GALLERY_GALLERY_TYPE = 8; private static final int VIEW_TYPE_POST_CARD_2_VIDEO_AUTOPLAY_TYPE = 9; private static final int VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE = 10; private static final int VIEW_TYPE_POST_CARD_2_GALLERY_TYPE = 11; private static final int VIEW_TYPE_POST_CARD_2_TEXT_TYPE = 12; private static final int VIEW_TYPE_POST_CARD_3_VIDEO_AUTOPLAY_TYPE = 13; private static final int VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE = 14; private static final int VIEW_TYPE_POST_CARD_3_GALLERY_TYPE = 15; private static final int VIEW_TYPE_POST_CARD_3_TEXT_TYPE = 16; private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback<>() { @Override public boolean areItemsTheSame(@NonNull Post post, @NonNull Post t1) { return post.getId().equals(t1.getId()); } @Override public boolean areContentsTheSame(@NonNull Post post, @NonNull Post t1) { return false; } }; private BaseActivity mActivity; private PostFragmentBase mFragment; private SharedPreferences mSharedPreferences; private SharedPreferences mCurrentAccountSharedPreferences; private Executor mExecutor; private Retrofit mOauthRetrofit; private Retrofit mRedgifsRetrofit; private Provider mStreamableApiProvider; private String mAccessToken; private String mAccountName; private RequestManager mGlide; private int mMaxResolution; private SaveMemoryCenterInisdeDownsampleStrategy mSaveMemoryCenterInsideDownsampleStrategy; private CustomThemeWrapper mCustomThemeWrapper; private Locale mLocale; private boolean canStartActivity = true; private boolean mDisableSwipingBetweenTabs; private int mPostType; private int mPostLayout; private int mDefaultLinkPostLayout; private int mColorAccent; private int mCardViewBackgroundColor; private int mReadPostCardViewBackgroundColor; private int mFilledCardViewBackgroundColor; private int mReadPostFilledCardViewBackgroundColor; private int mPrimaryTextColor; private int mSecondaryTextColor; private int mPostTitleColor; private int mPostContentColor; private int mReadPostTitleColor; private int mReadPostContentColor; private int mStickiedPostIconTint; private int mPostTypeBackgroundColor; private int mPostTypeTextColor; private int mSubredditColor; private int mUsernameColor; private int mModeratorColor; private int mSpoilerBackgroundColor; private int mSpoilerTextColor; private int mFlairBackgroundColor; private int mFlairTextColor; private int mNSFWBackgroundColor; private int mNSFWTextColor; private int mArchivedIconTint; private int mLockedIconTint; private int mCrosspostIconTint; private int mMediaIndicatorIconTint; private int mMediaIndicatorBackgroundColor; private int mNoPreviewPostTypeBackgroundColor; private int mNoPreviewPostTypeIconTint; private int mUpvotedColor; private int mDownvotedColor; private int mVoteAndReplyUnavailableVoteButtonColor; private int mPostIconAndInfoColor; private int mDividerColor; private float mScale; private boolean mDisplaySubredditName; private boolean mVoteButtonsOnTheRight; private boolean mNeedBlurNsfw; private boolean mDoNotBlurNsfwInNsfwSubreddits; private boolean mNeedBlurSpoiler; private boolean mShowElapsedTime; private String mTimeFormatPattern; private boolean mShowDividerInCompactLayout; private boolean mShowAbsoluteNumberOfVotes; private boolean mAutoplay = false; private boolean mAutoplayNsfwVideos; private boolean mMuteAutoplayingVideos; private boolean mShowThumbnailOnTheLeftInCompactLayout; private double mStartAutoplayVisibleAreaOffset; private boolean mMuteNSFWVideo; private boolean mLongPressToHideToolbarInCompactLayout; private boolean mCompactLayoutToolbarHiddenByDefault; private boolean mDataSavingMode = false; private boolean mDisableImagePreview; private boolean mOnlyDisablePreviewInVideoAndGifPosts; private boolean mMarkPostsAsRead; private boolean mMarkPostsAsReadAfterVoting; private boolean mMarkPostsAsReadOnScroll; private boolean mHidePostType; private boolean mHidePostFlair; private boolean mHideSubredditAndUserPrefix; private boolean mHideTheNumberOfVotes; private boolean mHideTheNumberOfComments; private boolean mLegacyAutoplayVideoControllerUI; private boolean mFixedHeightPreviewInCard; private boolean mHideTextPostContent; private boolean mEasierToWatchInFullScreen; private boolean mDisableProfileAvatarAnimation; private int mDataSavingModeDefaultResolution; private int mNonDataSavingModeDefaultResolution; private int mSimultaneousAutoplayLimit; private String mLongPressPostNonMediaAreaAction = SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS; private String mLongPressPostMediaAction = SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS; private boolean mHandleReadPost; private ExoCreator mExoCreator; private Callback mCallback; private boolean canPlayVideo = true; private RecyclerView.RecycledViewPool mGalleryRecycledViewPool; private MultiPlayPlayerSelector multiPlayPlayerSelector; // postHistorySharedPreferences will be null when being used in HistoryPostFragment. public PostRecyclerViewAdapter(BaseActivity activity, PostFragmentBase fragment, Executor executor, Retrofit oauthRetrofit, Retrofit redgifsRetrofit, Provider streamableApiProvider, CustomThemeWrapper customThemeWrapper, Locale locale, @Nullable String accessToken, @NonNull String accountName, int postType, int postLayout, boolean displaySubredditName, SharedPreferences sharedPreferences, SharedPreferences currentAccountSharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferences, @Nullable SharedPreferences postHistorySharedPreferences, ExoCreator exoCreator, Callback callback) { super(DIFF_CALLBACK); if (activity != null) { mActivity = activity; mFragment = fragment; mSharedPreferences = sharedPreferences; mCurrentAccountSharedPreferences = currentAccountSharedPreferences; mExecutor = executor; mOauthRetrofit = oauthRetrofit; mRedgifsRetrofit = redgifsRetrofit; mStreamableApiProvider = streamableApiProvider; mAccessToken = accessToken; mAccountName = accountName; mPostType = postType; mDisplaySubredditName = displaySubredditName; mNeedBlurNsfw = nsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.BLUR_NSFW_BASE, true); mDoNotBlurNsfwInNsfwSubreddits = nsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.DO_NOT_BLUR_NSFW_IN_NSFW_SUBREDDITS, false); mNeedBlurSpoiler = nsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.BLUR_SPOILER_BASE, false); mVoteButtonsOnTheRight = sharedPreferences.getBoolean(SharedPreferencesUtils.VOTE_BUTTONS_ON_THE_RIGHT_KEY, false); mShowElapsedTime = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ELAPSED_TIME_KEY, false); mTimeFormatPattern = sharedPreferences.getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE); mShowDividerInCompactLayout = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_DIVIDER_IN_COMPACT_LAYOUT, true); mShowAbsoluteNumberOfVotes = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ABSOLUTE_NUMBER_OF_VOTES, true); String autoplayString = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_AUTOPLAY, SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_NEVER); int networkType = Utils.getConnectedNetwork(activity); if (autoplayString.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ALWAYS_ON)) { mAutoplay = true; } else if (autoplayString.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ON_WIFI)) { mAutoplay = networkType == Utils.NETWORK_TYPE_WIFI; } mAutoplayNsfwVideos = sharedPreferences.getBoolean(SharedPreferencesUtils.AUTOPLAY_NSFW_VIDEOS, true); mMuteAutoplayingVideos = sharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_AUTOPLAYING_VIDEOS, true); mShowThumbnailOnTheLeftInCompactLayout = sharedPreferences.getBoolean( SharedPreferencesUtils.SHOW_THUMBNAIL_ON_THE_LEFT_IN_COMPACT_LAYOUT, false); Resources resources = activity.getResources(); mStartAutoplayVisibleAreaOffset = resources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? sharedPreferences.getInt(SharedPreferencesUtils.START_AUTOPLAY_VISIBLE_AREA_OFFSET_PORTRAIT, 75) / 100.0 : sharedPreferences.getInt(SharedPreferencesUtils.START_AUTOPLAY_VISIBLE_AREA_OFFSET_LANDSCAPE, 50) / 100.0; mMuteNSFWVideo = sharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_NSFW_VIDEO, false); mLongPressToHideToolbarInCompactLayout = sharedPreferences.getBoolean(SharedPreferencesUtils.LONG_PRESS_TO_HIDE_TOOLBAR_IN_COMPACT_LAYOUT, false); mCompactLayoutToolbarHiddenByDefault = sharedPreferences.getBoolean(SharedPreferencesUtils.POST_COMPACT_LAYOUT_TOOLBAR_HIDDEN_BY_DEFAULT, false); String dataSavingModeString = sharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ALWAYS)) { mDataSavingMode = true; } else if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { mDataSavingMode = networkType == Utils.NETWORK_TYPE_CELLULAR; } mDisableImagePreview = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_IMAGE_PREVIEW, false); mOnlyDisablePreviewInVideoAndGifPosts = sharedPreferences.getBoolean(SharedPreferencesUtils.ONLY_DISABLE_PREVIEW_IN_VIDEO_AND_GIF_POSTS, false); mDisableProfileAvatarAnimation = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_PROFILE_AVATAR_ANIMATION, false); mDisableSwipingBetweenTabs = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS, false); if (postHistorySharedPreferences != null) { mMarkPostsAsRead = postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MARK_POSTS_AS_READ_BASE, false); mMarkPostsAsReadAfterVoting = postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MARK_POSTS_AS_READ_AFTER_VOTING_BASE, false); mMarkPostsAsReadOnScroll = postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.MARK_POSTS_AS_READ_ON_SCROLL_BASE, false); mHandleReadPost = true; } mHidePostType = sharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_POST_TYPE, false); mHidePostFlair = sharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_POST_FLAIR, false); mHideSubredditAndUserPrefix = sharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_SUBREDDIT_AND_USER_PREFIX, false); mHideTheNumberOfVotes = sharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_VOTES, false); mHideTheNumberOfComments = sharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_COMMENTS, false); mLegacyAutoplayVideoControllerUI = sharedPreferences.getBoolean(SharedPreferencesUtils.LEGACY_AUTOPLAY_VIDEO_CONTROLLER_UI, false); mFixedHeightPreviewInCard = sharedPreferences.getBoolean(SharedPreferencesUtils.FIXED_HEIGHT_PREVIEW_IN_CARD, false); mHideTextPostContent = sharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_TEXT_POST_CONTENT, false); mEasierToWatchInFullScreen = sharedPreferences.getBoolean(SharedPreferencesUtils.EASIER_TO_WATCH_IN_FULL_SCREEN, false); mDataSavingModeDefaultResolution = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION, "360")); mNonDataSavingModeDefaultResolution = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION_NO_DATA_SAVING, "0")); mSimultaneousAutoplayLimit = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SIMULTANEOUS_AUTOPLAY_LIMIT, "1")); mPostLayout = postLayout; mDefaultLinkPostLayout = Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.DEFAULT_LINK_POST_LAYOUT_KEY, "-1")); mColorAccent = customThemeWrapper.getColorAccent(); mCardViewBackgroundColor = customThemeWrapper.getCardViewBackgroundColor(); mReadPostCardViewBackgroundColor = customThemeWrapper.getReadPostCardViewBackgroundColor(); mFilledCardViewBackgroundColor = customThemeWrapper.getFilledCardViewBackgroundColor(); mReadPostFilledCardViewBackgroundColor = customThemeWrapper.getReadPostFilledCardViewBackgroundColor(); mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); mPostTitleColor = customThemeWrapper.getPostTitleColor(); mPostContentColor = customThemeWrapper.getPostContentColor(); mReadPostTitleColor = customThemeWrapper.getReadPostTitleColor(); mReadPostContentColor = customThemeWrapper.getReadPostContentColor(); mStickiedPostIconTint = customThemeWrapper.getStickiedPostIconTint(); mPostTypeBackgroundColor = customThemeWrapper.getPostTypeBackgroundColor(); mPostTypeTextColor = customThemeWrapper.getPostTypeTextColor(); mSubredditColor = customThemeWrapper.getSubreddit(); mUsernameColor = customThemeWrapper.getUsername(); mModeratorColor = customThemeWrapper.getModerator(); mSpoilerBackgroundColor = customThemeWrapper.getSpoilerBackgroundColor(); mSpoilerTextColor = customThemeWrapper.getSpoilerTextColor(); mFlairBackgroundColor = customThemeWrapper.getFlairBackgroundColor(); mFlairTextColor = customThemeWrapper.getFlairTextColor(); mNSFWBackgroundColor = customThemeWrapper.getNsfwBackgroundColor(); mNSFWTextColor = customThemeWrapper.getNsfwTextColor(); mArchivedIconTint = customThemeWrapper.getArchivedIconTint(); mLockedIconTint = customThemeWrapper.getLockedIconTint(); mCrosspostIconTint = customThemeWrapper.getCrosspostIconTint(); mMediaIndicatorIconTint = customThemeWrapper.getMediaIndicatorIconColor(); mMediaIndicatorBackgroundColor = customThemeWrapper.getMediaIndicatorBackgroundColor(); mNoPreviewPostTypeBackgroundColor = customThemeWrapper.getNoPreviewPostTypeBackgroundColor(); mNoPreviewPostTypeIconTint = customThemeWrapper.getNoPreviewPostTypeIconTint(); mUpvotedColor = customThemeWrapper.getUpvoted(); mDownvotedColor = customThemeWrapper.getDownvoted(); mVoteAndReplyUnavailableVoteButtonColor = customThemeWrapper.getVoteAndReplyUnavailableButtonColor(); mPostIconAndInfoColor = customThemeWrapper.getPostIconAndInfoColor(); mDividerColor = customThemeWrapper.getDividerColor(); mScale = resources.getDisplayMetrics().density; mGlide = Glide.with(mActivity); mMaxResolution = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000")); mSaveMemoryCenterInsideDownsampleStrategy = new SaveMemoryCenterInisdeDownsampleStrategy(mMaxResolution); mCustomThemeWrapper = customThemeWrapper; mLocale = locale; mExoCreator = exoCreator; mCallback = callback; mGalleryRecycledViewPool = new RecyclerView.RecycledViewPool(); multiPlayPlayerSelector = new MultiPlayPlayerSelector(mSimultaneousAutoplayLimit); } } public void setCanStartActivity(boolean canStartActivity) { this.canStartActivity = canStartActivity; } @Override public int getItemViewType(int position) { if (mPostLayout == SharedPreferencesUtils.POST_LAYOUT_CARD) { Post post = getItem(position); if (post != null) { switch (post.getPostType()) { case Post.VIDEO_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } if (mAutoplay) { if ((!mAutoplayNsfwVideos && post.isNSFW()) || post.isSpoiler()) { return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; } return VIEW_TYPE_POST_CARD_VIDEO_AUTOPLAY_TYPE; } return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; case Post.GIF_TYPE: case Post.IMAGE_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; case Post.GALLERY_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } return VIEW_TYPE_POST_CARD_GALLERY_TYPE; case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } switch (mDefaultLinkPostLayout) { case SharedPreferencesUtils.POST_LAYOUT_CARD_2: return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_CARD_3: return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_GALLERY: return VIEW_TYPE_POST_GALLERY; case SharedPreferencesUtils.POST_LAYOUT_COMPACT: return VIEW_TYPE_POST_COMPACT; case SharedPreferencesUtils.POST_LAYOUT_COMPACT_2: return VIEW_TYPE_POST_COMPACT_2; } return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; default: return VIEW_TYPE_POST_CARD_TEXT_TYPE; } } return VIEW_TYPE_POST_CARD_TEXT_TYPE; } else if (mPostLayout == SharedPreferencesUtils.POST_LAYOUT_COMPACT) { Post post = getItem(position); if (post != null) { if (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { switch (mDefaultLinkPostLayout) { case SharedPreferencesUtils.POST_LAYOUT_CARD: return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_CARD_2: return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_CARD_3: return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_GALLERY: return VIEW_TYPE_POST_GALLERY; } } } return VIEW_TYPE_POST_COMPACT; } else if (mPostLayout == SharedPreferencesUtils.POST_LAYOUT_COMPACT_2) { Post post = getItem(position); if (post != null) { if (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { switch (mDefaultLinkPostLayout) { case SharedPreferencesUtils.POST_LAYOUT_CARD: return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_CARD_2: return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_CARD_3: return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_GALLERY: return VIEW_TYPE_POST_GALLERY; case SharedPreferencesUtils.POST_LAYOUT_COMPACT: return VIEW_TYPE_POST_COMPACT; } } } return VIEW_TYPE_POST_COMPACT_2; } else if (mPostLayout == SharedPreferencesUtils.POST_LAYOUT_GALLERY) { Post post = getItem(position); if (post != null) { if (post.getPostType() == Post.GALLERY_TYPE) { return VIEW_TYPE_POST_GALLERY_GALLERY_TYPE; } else { return VIEW_TYPE_POST_GALLERY; } } else { return VIEW_TYPE_POST_GALLERY; } } else if (mPostLayout == SharedPreferencesUtils.POST_LAYOUT_CARD_2) { Post post = getItem(position); if (post != null) { switch (post.getPostType()) { case Post.VIDEO_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } if (mAutoplay) { if ((!mAutoplayNsfwVideos && post.isNSFW()) || post.isSpoiler()) { return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; } return VIEW_TYPE_POST_CARD_2_VIDEO_AUTOPLAY_TYPE; } return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; case Post.GIF_TYPE: case Post.IMAGE_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; case Post.GALLERY_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } return VIEW_TYPE_POST_CARD_2_GALLERY_TYPE; case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } switch (mDefaultLinkPostLayout) { case SharedPreferencesUtils.POST_LAYOUT_CARD: return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_CARD_3: return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_GALLERY: return VIEW_TYPE_POST_GALLERY; case SharedPreferencesUtils.POST_LAYOUT_COMPACT: return VIEW_TYPE_POST_COMPACT; case SharedPreferencesUtils.POST_LAYOUT_COMPACT_2: return VIEW_TYPE_POST_COMPACT_2; } return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; default: return VIEW_TYPE_POST_CARD_2_TEXT_TYPE; } } return VIEW_TYPE_POST_CARD_2_TEXT_TYPE; } else { Post post = getItem(position); if (post != null) { switch (post.getPostType()) { case Post.VIDEO_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } if (mAutoplay) { if ((!mAutoplayNsfwVideos && post.isNSFW()) || post.isSpoiler()) { return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; } return VIEW_TYPE_POST_CARD_3_VIDEO_AUTOPLAY_TYPE; } return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; case Post.GIF_TYPE: case Post.IMAGE_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; case Post.GALLERY_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } return VIEW_TYPE_POST_CARD_3_GALLERY_TYPE; case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: if (shouldUseCompactLayout(post)) { return VIEW_TYPE_POST_COMPACT; } switch (mDefaultLinkPostLayout) { case SharedPreferencesUtils.POST_LAYOUT_CARD: return VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_CARD_2: return VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE; case SharedPreferencesUtils.POST_LAYOUT_GALLERY: return VIEW_TYPE_POST_GALLERY; case SharedPreferencesUtils.POST_LAYOUT_COMPACT: return VIEW_TYPE_POST_COMPACT; case SharedPreferencesUtils.POST_LAYOUT_COMPACT_2: return VIEW_TYPE_POST_COMPACT_2; } return VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE; default: return VIEW_TYPE_POST_CARD_3_TEXT_TYPE; } } return VIEW_TYPE_POST_CARD_3_TEXT_TYPE; } } @OptIn(markerClass = UnstableApi.class) @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_POST_CARD_VIDEO_AUTOPLAY_TYPE) { if (mDataSavingMode) { return new PostWithPreviewTypeViewHolder(ItemPostWithPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } if (mLegacyAutoplayVideoControllerUI) { return new PostVideoAutoplayLegacyControllerViewHolder(ItemPostVideoTypeAutoplayLegacyControllerBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new PostVideoAutoplayViewHolder(ItemPostVideoTypeAutoplayBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } else if (viewType == VIEW_TYPE_POST_CARD_WITH_PREVIEW_TYPE) { return new PostWithPreviewTypeViewHolder(ItemPostWithPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_CARD_GALLERY_TYPE) { return new PostGalleryTypeViewHolder(ItemPostGalleryTypeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_CARD_TEXT_TYPE) { return new PostTextTypeViewHolder(ItemPostTextBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_COMPACT) { if (mShowThumbnailOnTheLeftInCompactLayout) { return new PostCompactLeftThumbnailViewHolder(ItemPostCompactBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new PostCompactRightThumbnailViewHolder(ItemPostCompactRightThumbnailBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } else if (viewType == VIEW_TYPE_POST_COMPACT_2) { if (mShowThumbnailOnTheLeftInCompactLayout) { return new PostCompact2LeftThumbnailViewHolder(ItemPostCompact2Binding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new PostCompact2RightThumbnailViewHolder(ItemPostCompact2RightThumbnailBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } else if (viewType == VIEW_TYPE_POST_GALLERY) { return new PostGalleryViewHolder(ItemPostGalleryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_GALLERY_GALLERY_TYPE) { return new PostGalleryGalleryTypeViewHolder(ItemPostGalleryGalleryTypeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_CARD_2_VIDEO_AUTOPLAY_TYPE) { if (mDataSavingMode) { return new PostCard2WithPreviewViewHolder(ItemPostCard2WithPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } if (mLegacyAutoplayVideoControllerUI) { return new PostCard2VideoAutoplayLegacyControllerViewHolder(ItemPostCard2VideoAutoplayLegacyControllerBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new PostCard2VideoAutoplayViewHolder(ItemPostCard2VideoAutoplayBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } else if (viewType == VIEW_TYPE_POST_CARD_2_WITH_PREVIEW_TYPE) { return new PostCard2WithPreviewViewHolder(ItemPostCard2WithPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_CARD_2_GALLERY_TYPE) { return new PostCard2GalleryTypeViewHolder(ItemPostCard2GalleryTypeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_CARD_2_TEXT_TYPE) { return new PostCard2TextTypeViewHolder(ItemPostCard2TextBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_CARD_3_VIDEO_AUTOPLAY_TYPE) { if (mDataSavingMode) { return new PostMaterial3CardWithPreviewViewHolder(ItemPostCard3WithPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } if (mLegacyAutoplayVideoControllerUI) { return new PostMaterial3CardVideoAutoplayLegacyControllerViewHolder(ItemPostCard3VideoTypeAutoplayLegacyControllerBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new PostMaterial3CardVideoAutoplayViewHolder(ItemPostCard3VideoTypeAutoplayBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } else if (viewType == VIEW_TYPE_POST_CARD_3_WITH_PREVIEW_TYPE) { return new PostMaterial3CardWithPreviewViewHolder(ItemPostCard3WithPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_POST_CARD_3_GALLERY_TYPE) { return new PostMaterial3CardGalleryTypeViewHolder(ItemPostCard3GalleryTypeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { //VIEW_TYPE_POST_CARD_3_TEXT_TYPE return new PostMaterial3CardTextTypeViewHolder(ItemPostCard3TextBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @OptIn(markerClass = UnstableApi.class) @Override public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { if (holder instanceof PostViewHolder) { Post post = getItem(position); if (post == null) { return; } ((PostViewHolder) holder).post = post; ((PostViewHolder) holder).currentPosition = position; if (mHandleReadPost && post.isRead()) { ((PostViewHolder) holder).setItemViewBackgroundColor(true); ((PostViewHolder) holder).titleTextView.setTextColor(mReadPostTitleColor); } if (mDisplaySubredditName) { if (post.getAuthorNamePrefixed().equals(post.getSubredditNamePrefixed())) { if (post.getAuthorIconUrl() == null) { mFragment.loadIcon(post.getAuthor(), false, (subredditOrUserName, iconUrl) -> { if (mActivity != null && getItemCount() > 0 && post.getAuthor().equals(subredditOrUserName)) { if (iconUrl == null || iconUrl.isEmpty()) { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostViewHolder) holder).iconGifImageView); } else { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } else { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } } if (holder.getBindingAdapterPosition() >= 0) { post.setAuthorIconUrl(iconUrl); } } }); } else if (!post.getAuthorIconUrl().isEmpty()) { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(post.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } else { mGlide.load(post.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostViewHolder) holder).iconGifImageView); } } else { if (post.getSubredditIconUrl() == null) { mFragment.loadIcon(post.getSubredditName(), true, (subredditOrUserName, iconUrl) -> { if (mActivity != null && getItemCount() > 0 && post.getSubredditName().equals(subredditOrUserName)) { if (iconUrl == null || iconUrl.isEmpty()) { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostViewHolder) holder).iconGifImageView); } else { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } else { mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } } if (holder.getBindingAdapterPosition() >= 0) { post.setSubredditIconUrl(iconUrl); } } }); } else if (!post.getSubredditIconUrl().isEmpty()) { if (mDisableProfileAvatarAnimation) { mGlide.asBitmap().load(post.getSubredditIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } else { mGlide.load(post.getSubredditIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((PostViewHolder) holder).iconGifImageView); } } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostViewHolder) holder).iconGifImageView); } } } else { if (post.getAuthorIconUrl() == null) { String authorName = post.isAuthorDeleted() ? post.getSubredditName() : post.getAuthor(); mFragment.loadIcon(authorName, post.isAuthorDeleted(), (subredditOrUserName, iconUrl) -> { if (mActivity != null && getItemCount() > 0) { if (iconUrl == null || iconUrl.isEmpty() && authorName.equals(subredditOrUserName)) { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostViewHolder) holder).iconGifImageView); } else { RequestBuilder requestBuilder = mGlide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))); if (mDisableProfileAvatarAnimation) { requestBuilder = requestBuilder.dontAnimate(); } requestBuilder.into(((PostViewHolder) holder).iconGifImageView); } if (holder.getBindingAdapterPosition() >= 0) { post.setAuthorIconUrl(iconUrl); } } }); } else if (!post.getAuthorIconUrl().isEmpty()) { RequestBuilder requestBuilder = mGlide.load(post.getAuthorIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))); if (mDisableProfileAvatarAnimation) { requestBuilder = requestBuilder.dontAnimate(); } requestBuilder.into(((PostViewHolder) holder).iconGifImageView); } else { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((PostViewHolder) holder).iconGifImageView); } } if (mShowElapsedTime) { ((PostViewHolder) holder).postTimeTextView.setText( Utils.getElapsedTime(mActivity, post.getPostTimeMillis())); } else { ((PostViewHolder) holder).postTimeTextView.setText(Utils.getFormattedTime(mLocale, post.getPostTimeMillis(), mTimeFormatPattern)); } ((PostViewHolder) holder).titleTextView.setText(post.getTitle()); if (!mHideTheNumberOfVotes) { ((PostViewHolder) holder).scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, post.getScore() + post.getVoteType())); } else { ((PostViewHolder) holder).scoreTextView.setText(mActivity.getString(R.string.vote)); } if (((PostViewHolder) holder).typeTextView != null) { if (mHidePostType) { ((PostViewHolder) holder).typeTextView.setVisibility(View.GONE); } else { ((PostViewHolder) holder).typeTextView.setVisibility(View.VISIBLE); } } if (((PostViewHolder) holder).lockedImageView != null && post.isLocked()) { ((PostViewHolder) holder).lockedImageView.setVisibility(View.VISIBLE); } if (((PostViewHolder) holder).nsfwTextView != null && post.isNSFW()) { ((PostViewHolder) holder).nsfwTextView.setVisibility(View.VISIBLE); } if (((PostViewHolder) holder).spoilerTextView != null && post.isSpoiler()) { ((PostViewHolder) holder).spoilerTextView.setVisibility(View.VISIBLE); } if (((PostViewHolder) holder).flairTextView != null && post.getFlair() != null && !post.getFlair().isEmpty()) { if (mHidePostFlair) { ((PostViewHolder) holder).flairTextView.setVisibility(View.GONE); } else { ((PostViewHolder) holder).flairTextView.setVisibility(View.VISIBLE); Utils.setHTMLWithImageToTextView(((PostViewHolder) holder).flairTextView, post.getFlair(), false); } } if (post.isArchived()) { if (((PostViewHolder) holder).archivedImageView != null) { ((PostViewHolder) holder).archivedImageView.setVisibility(View.VISIBLE); } ((PostViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); ((PostViewHolder) holder).scoreTextView.setTextColor(mVoteAndReplyUnavailableVoteButtonColor); ((PostViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mVoteAndReplyUnavailableVoteButtonColor)); } if (((PostViewHolder) holder).crosspostImageView != null && post.isCrosspost()) { ((PostViewHolder) holder).crosspostImageView.setVisibility(View.VISIBLE); } switch (post.getVoteType()) { case 1: //Upvoted ((PostViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); ((PostViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); ((PostViewHolder) holder).scoreTextView.setTextColor(mUpvotedColor); break; case -1: //Downvoted ((PostViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); ((PostViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); ((PostViewHolder) holder).scoreTextView.setTextColor(mDownvotedColor); break; } if (mPostType == PostPagingSource.TYPE_SUBREDDIT && !mDisplaySubredditName && post.isStickied()) { ((PostViewHolder) holder).stickiedPostImageView.setVisibility(View.VISIBLE); mGlide.load(R.drawable.ic_thumbtack_24dp).into(((PostViewHolder) holder).stickiedPostImageView); } if (((PostViewHolder) holder).commentsCountButton != null ) { if (!mHideTheNumberOfComments) { ((PostViewHolder) holder).commentsCountButton.setVisibility(View.VISIBLE); ((PostViewHolder) holder).commentsCountButton.setText(Integer.toString(post.getNComments())); } else { ((PostViewHolder) holder).commentsCountButton.setVisibility(View.GONE); } } if (((PostViewHolder) holder).saveButton != null) { if (post.isSaved()) { ((PostViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } else { ((PostViewHolder) holder).saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } } if (holder instanceof PostBaseViewHolder) { if (mHideSubredditAndUserPrefix) { ((PostBaseViewHolder) holder).subredditTextView.setText(post.getSubredditName()); ((PostBaseViewHolder) holder).userTextView.setText(post.getAuthor()); } else { ((PostBaseViewHolder) holder).subredditTextView.setText(post.getSubredditNamePrefixed()); ((PostBaseViewHolder) holder).userTextView.setText(post.getAuthorNamePrefixed()); } ((PostBaseViewHolder) holder).userTextView.setTextColor( post.isModerator() ? mModeratorColor : mUsernameColor); if (holder instanceof PostBaseVideoAutoplayViewHolder) { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.previewImageView.setVisibility(View.VISIBLE); Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); if (!mFixedHeightPreviewInCard && preview != null) { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.aspectRatioFrameLayout.setAspectRatio((float) preview.getPreviewWidth() / preview.getPreviewHeight()); mGlide.load(preview.getPreviewUrl()).centerInside().downsample(mSaveMemoryCenterInsideDownsampleStrategy).into(((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.previewImageView); } else { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.aspectRatioFrameLayout.setAspectRatio(1); } if (!((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.isManuallyPaused) { if (mFragment.getMasterMutingOption() == null) { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.setVolume(mMuteAutoplayingVideos || (post.isNSFW() && mMuteNSFWVideo) ? 0f : 1f); } else { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.setVolume(mFragment.getMasterMutingOption() ? 0f : 1f); } } ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.loadVideo(position); } else if (holder instanceof PostWithPreviewTypeViewHolder) { if (post.getPostType() == Post.VIDEO_TYPE) { ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.VISIBLE); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_play_circle_36dp)); if (((PostWithPreviewTypeViewHolder) holder).typeTextView != null) { ((PostWithPreviewTypeViewHolder) holder).typeTextView.setText(mActivity.getString(R.string.video)); } } else if (post.getPostType() == Post.GIF_TYPE) { if (!mAutoplay) { ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.VISIBLE); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_play_circle_36dp)); } if (((PostWithPreviewTypeViewHolder) holder).typeTextView != null) { ((PostWithPreviewTypeViewHolder) holder).typeTextView.setText(mActivity.getString(R.string.gif)); } } else if (post.getPostType() == Post.IMAGE_TYPE) { if (((PostWithPreviewTypeViewHolder) holder).typeTextView != null) { ((PostWithPreviewTypeViewHolder) holder).typeTextView.setText(mActivity.getString(R.string.image)); } } else if (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { if (((PostWithPreviewTypeViewHolder) holder).typeTextView != null) { ((PostWithPreviewTypeViewHolder) holder).typeTextView.setText(mActivity.getString(R.string.link)); } ((PostWithPreviewTypeViewHolder) holder).linkTextView.setVisibility(View.VISIBLE); String domain = Uri.parse(post.getUrl()).getHost(); ((PostWithPreviewTypeViewHolder) holder).linkTextView.setText(domain); if (post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setVisibility(View.VISIBLE); ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_link_day_night_24dp); } } if (mDataSavingMode && mDisableImagePreview) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setVisibility(View.VISIBLE); if (post.getPostType() == Post.VIDEO_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_video_day_night_24dp); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.GONE); } else if (post.getPostType() == Post.IMAGE_TYPE || post.getPostType() == Post.GIF_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_image_day_night_24dp); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.GONE); } else if (post.getPostType() == Post.LINK_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_link_day_night_24dp); } } else if (mDataSavingMode && mOnlyDisablePreviewInVideoAndGifPosts && (post.getPostType() == Post.VIDEO_TYPE || post.getPostType() == Post.GIF_TYPE)) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setVisibility(View.VISIBLE); ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_video_day_night_24dp); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.GONE); } else { if (post.getPostType() == Post.GIF_TYPE && ((post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit()) && !(mAutoplay && mAutoplayNsfwVideos)) || (post.isSpoiler() && mNeedBlurSpoiler))) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setVisibility(View.VISIBLE); ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_image_day_night_24dp); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.GONE); } else { Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); ((PostWithPreviewTypeViewHolder) holder).preview = preview; if (preview != null) { if (((PostWithPreviewTypeViewHolder) holder).imageWrapperFrameLayout != null) { ((PostWithPreviewTypeViewHolder) holder).imageWrapperFrameLayout.setVisibility(View.VISIBLE); } ((PostWithPreviewTypeViewHolder) holder).imageView.setVisibility(View.VISIBLE); if (mFixedHeightPreviewInCard || (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0)) { int height = (int) (400 * mScale); ((PostWithPreviewTypeViewHolder) holder).imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); ((PostWithPreviewTypeViewHolder) holder).imageView.getLayoutParams().height = height; } else { ((PostWithPreviewTypeViewHolder) holder).imageView .setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } ((PostWithPreviewTypeViewHolder) holder).imageView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { ((PostWithPreviewTypeViewHolder) holder).imageView.removeOnLayoutChangeListener(this); loadImage(holder); } }); // Hide placeholder since we have a preview (including thumbnail fallback) ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setVisibility(View.GONE); } else { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setVisibility(View.VISIBLE); if (post.getPostType() == Post.VIDEO_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_video_day_night_24dp); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.GONE); } else if (post.getPostType() == Post.IMAGE_TYPE || post.getPostType() == Post.GIF_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_image_day_night_24dp); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.GONE); } else if (post.getPostType() == Post.LINK_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_link_day_night_24dp); } else if (post.getPostType() == Post.GALLERY_TYPE) { ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setImageResource(R.drawable.ic_gallery_day_night_24dp); } } } } } else if (holder instanceof PostBaseGalleryTypeViewHolder) { if (mDataSavingMode && mDisableImagePreview) { ((PostBaseGalleryTypeViewHolder) holder).noPreviewImageView.setVisibility(View.VISIBLE); ((PostBaseGalleryTypeViewHolder) holder).noPreviewImageView.setImageResource(R.drawable.ic_gallery_day_night_24dp); } else { ((PostBaseGalleryTypeViewHolder) holder).frameLayout.setVisibility(View.VISIBLE); ((PostBaseGalleryTypeViewHolder) holder).imageIndexTextView.setText(mActivity.getString(R.string.image_index_in_gallery, 1, post.getGallery().size())); Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); if (preview != null) { if (mFixedHeightPreviewInCard || (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0)) { ((PostBaseGalleryTypeViewHolder) holder).adapter.setRatio(-1); } else { ((PostBaseGalleryTypeViewHolder) holder).adapter.setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } } else { ((PostBaseGalleryTypeViewHolder) holder).adapter.setRatio(-1); } ((PostBaseGalleryTypeViewHolder) holder).adapter.setGalleryImages(post.getGallery()); ((PostBaseGalleryTypeViewHolder) holder).adapter.setBlurImage( (post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (post.isSpoiler() && mNeedBlurSpoiler)); } } else if (holder instanceof PostTextTypeViewHolder) { if (!mHideTextPostContent && !post.isSpoiler() && post.getSelfTextPlainTrimmed() != null && !post.getSelfTextPlainTrimmed().isEmpty()) { ((PostTextTypeViewHolder) holder).contentTextView.setVisibility(View.VISIBLE); if (mHandleReadPost && post.isRead()) { ((PostTextTypeViewHolder) holder).contentTextView.setTextColor(mReadPostContentColor); } ((PostTextTypeViewHolder) holder).contentTextView.setText(post.getSelfTextPlainTrimmed()); } } mCallback.currentlyBindItem(holder.getBindingAdapterPosition()); } else if (holder instanceof PostCompactBaseViewHolder) { if (mDisplaySubredditName) { ((PostCompactBaseViewHolder) holder).nameTextView.setTextColor(mSubredditColor); if (mHideSubredditAndUserPrefix) { ((PostCompactBaseViewHolder) holder).nameTextView.setText(post.getSubredditName()); } else { ((PostCompactBaseViewHolder) holder).nameTextView.setText(post.getSubredditNamePrefixed()); } } else { ((PostCompactBaseViewHolder) holder).nameTextView.setTextColor( post.isModerator() ? mModeratorColor : mUsernameColor); if (mHideSubredditAndUserPrefix) { ((PostCompactBaseViewHolder) holder).nameTextView.setText(post.getAuthor()); } else { ((PostCompactBaseViewHolder) holder).nameTextView.setText(post.getAuthorNamePrefixed()); } } if (((PostCompactBaseViewHolder) holder).bottomConstraintLayout != null) { if (mCompactLayoutToolbarHiddenByDefault) { ViewGroup.LayoutParams params = ((PostCompactBaseViewHolder) holder).bottomConstraintLayout.getLayoutParams(); params.height = 0; ((PostCompactBaseViewHolder) holder).bottomConstraintLayout.setLayoutParams(params); } else { ViewGroup.LayoutParams params = ((PostCompactBaseViewHolder) holder).bottomConstraintLayout.getLayoutParams(); params.height = LinearLayout.LayoutParams.WRAP_CONTENT; ((PostCompactBaseViewHolder) holder).bottomConstraintLayout.setLayoutParams(params); } } if (mShowDividerInCompactLayout) { ((PostCompactBaseViewHolder) holder).divider.setVisibility(View.VISIBLE); } else { ((PostCompactBaseViewHolder) holder).divider.setVisibility(View.GONE); } if (post.getPostType() != Post.TEXT_TYPE && post.getPostType() != Post.NO_PREVIEW_LINK_TYPE && !(mDataSavingMode && mDisableImagePreview)) { ((PostCompactBaseViewHolder) holder).relativeLayout.setVisibility(View.VISIBLE); if (post.getPostType() == Post.GALLERY_TYPE && post.getPreviews() != null && post.getPreviews().isEmpty()) { ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageView.setImageResource(R.drawable.ic_gallery_day_night_24dp); } if (post.getPreviews() != null && !post.getPreviews().isEmpty()) { ((PostCompactBaseViewHolder) holder).imageView.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).loadingIndicator.setVisibility(View.VISIBLE); loadImage(holder); } } switch (post.getPostType()) { case Post.IMAGE_TYPE: if (((PostCompactBaseViewHolder) holder).typeTextView != null) { ((PostCompactBaseViewHolder) holder).typeTextView.setText(R.string.image); } if (mDataSavingMode && mDisableImagePreview) { ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageView.setImageResource(R.drawable.ic_image_day_night_24dp); } break; case Post.LINK_TYPE: if (((PostCompactBaseViewHolder) holder).typeTextView != null) { ((PostCompactBaseViewHolder) holder).typeTextView.setText(R.string.link); } if (mDataSavingMode && mDisableImagePreview) { ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageView.setImageResource(R.drawable.ic_link_day_night_24dp); } if (((PostCompactBaseViewHolder) holder).linkTextView != null) { ((PostCompactBaseViewHolder) holder).linkTextView.setVisibility(View.VISIBLE); } String domain = Uri.parse(post.getUrl()).getHost(); if (((PostCompactBaseViewHolder) holder).linkTextView != null) { ((PostCompactBaseViewHolder) holder).linkTextView.setText(domain); } break; case Post.GIF_TYPE: if (((PostCompactBaseViewHolder) holder).typeTextView != null) { ((PostCompactBaseViewHolder) holder).typeTextView.setText(R.string.gif); } if (mDataSavingMode && (mDisableImagePreview || mOnlyDisablePreviewInVideoAndGifPosts)) { ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageView.setImageResource(R.drawable.ic_image_day_night_24dp); } else { ((PostCompactBaseViewHolder) holder).playButtonImageView.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).playButtonImageView.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_play_circle_24dp)); } break; case Post.VIDEO_TYPE: if (((PostCompactBaseViewHolder) holder).typeTextView != null) { ((PostCompactBaseViewHolder) holder).typeTextView.setText(R.string.video); } if (mDataSavingMode && (mDisableImagePreview || mOnlyDisablePreviewInVideoAndGifPosts)) { ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageView.setImageResource(R.drawable.ic_video_day_night_24dp); } else { ((PostCompactBaseViewHolder) holder).playButtonImageView.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).playButtonImageView.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_play_circle_24dp)); } break; case Post.NO_PREVIEW_LINK_TYPE: if (((PostCompactBaseViewHolder) holder).typeTextView != null) { ((PostCompactBaseViewHolder) holder).typeTextView.setText(R.string.link); } if (((PostCompactBaseViewHolder) holder).linkTextView != null) { ((PostCompactBaseViewHolder) holder).linkTextView.setVisibility(View.VISIBLE); String noPreviewLinkUrl = post.getUrl(); String noPreviewLinkDomain = Uri.parse(noPreviewLinkUrl).getHost(); ((PostCompactBaseViewHolder) holder).linkTextView.setText(noPreviewLinkDomain); } ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageView.setImageResource(R.drawable.ic_link_day_night_24dp); break; case Post.GALLERY_TYPE: if (((PostCompactBaseViewHolder) holder).typeTextView != null) { ((PostCompactBaseViewHolder) holder).typeTextView.setText(R.string.gallery); } if (mDataSavingMode && mDisableImagePreview) { ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageView.setImageResource(R.drawable.ic_gallery_day_night_24dp); } else { ((PostCompactBaseViewHolder) holder).playButtonImageView.setVisibility(View.VISIBLE); ((PostCompactBaseViewHolder) holder).playButtonImageView.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_gallery_day_night_24dp)); } break; case Post.TEXT_TYPE: if (((PostCompactBaseViewHolder) holder).typeTextView != null) { ((PostCompactBaseViewHolder) holder).typeTextView.setText(R.string.text); } break; } mCallback.currentlyBindItem(holder.getBindingAdapterPosition()); } } else if (holder instanceof PostGalleryViewHolder) { Post post = getItem(position); if (post != null) { ((PostGalleryViewHolder) holder).post = post; ((PostGalleryViewHolder) holder).currentPosition = position; if (mHandleReadPost && post.isRead()) { holder.itemView.setBackgroundTintList(ColorStateList.valueOf(mReadPostCardViewBackgroundColor)); ((PostGalleryViewHolder) holder).binding.titleTextViewItemPostGallery.setTextColor(mReadPostTitleColor); } if (mDataSavingMode && (mDisableImagePreview || ((post.getPostType() == Post.VIDEO_TYPE || post.getPostType() == Post.GIF_TYPE) && mOnlyDisablePreviewInVideoAndGifPosts))) { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.VISIBLE); if (post.getPostType() == Post.VIDEO_TYPE) { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_video_day_night_24dp); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setVisibility(View.GONE); } else if (post.getPostType() == Post.IMAGE_TYPE || post.getPostType() == Post.GIF_TYPE) { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_image_day_night_24dp); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setVisibility(View.GONE); } else if (post.getPostType() == Post.LINK_TYPE) { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_link_day_night_24dp); } else if (post.getPostType() == Post.GALLERY_TYPE) { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_gallery_day_night_24dp); } } else { switch (post.getPostType()) { case Post.IMAGE_TYPE: { Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); ((PostGalleryViewHolder) holder).preview = preview; if (preview != null) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setVisibility(View.VISIBLE); if (mFixedHeightPreviewInCard || (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0)) { int height = (int) (400 * mScale); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setScaleType(ImageView.ScaleType.CENTER_CROP); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.getLayoutParams().height = height; } else { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery .setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.removeOnLayoutChangeListener(this); loadImage(holder); } }); // Hide placeholder since we have a preview (including thumbnail fallback) ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.GONE); } else { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_image_day_night_24dp); } break; } case Post.GIF_TYPE: { if (post.getPostType() == Post.GIF_TYPE && ((post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit()) && !(mAutoplay && mAutoplayNsfwVideos)) || (post.isSpoiler() && mNeedBlurSpoiler))) { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_image_day_night_24dp); } else { Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); ((PostGalleryViewHolder) holder).preview = preview; if (preview != null) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_play_circle_36dp)); if (mFixedHeightPreviewInCard || (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0)) { int height = (int) (400 * mScale); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setScaleType(ImageView.ScaleType.CENTER_CROP); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.getLayoutParams().height = height; } else { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery .setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.removeOnLayoutChangeListener(this); loadImage(holder); } }); } else { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_image_day_night_24dp); } } break; } case Post.VIDEO_TYPE: { Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); ((PostGalleryViewHolder) holder).preview = preview; if (preview != null) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_play_circle_36dp)); if (mFixedHeightPreviewInCard || (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0)) { int height = (int) (400 * mScale); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setScaleType(ImageView.ScaleType.CENTER_CROP); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.getLayoutParams().height = height; } else { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery .setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.removeOnLayoutChangeListener(this); loadImage(holder); } }); // Hide placeholder since we have a preview (including thumbnail fallback) ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.GONE); } else { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_video_day_night_24dp); } break; } case Post.LINK_TYPE: { Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); ((PostGalleryViewHolder) holder).preview = preview; if (preview != null) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_link_post_type_indicator_day_night_24dp)); if (mFixedHeightPreviewInCard || (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0)) { int height = (int) (400 * mScale); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setScaleType(ImageView.ScaleType.CENTER_CROP); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.getLayoutParams().height = height; } else { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery .setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.removeOnLayoutChangeListener(this); loadImage(holder); } }); // Hide placeholder since we have a preview (including thumbnail fallback) ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.GONE); } else { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_link_day_night_24dp); } break; } case Post.NO_PREVIEW_LINK_TYPE: { ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setImageResource(R.drawable.ic_link_day_night_24dp); break; } case Post.TEXT_TYPE: { ((PostGalleryViewHolder) holder).binding.titleTextViewItemPostGallery.setVisibility(View.VISIBLE); ((PostGalleryViewHolder) holder).binding.titleTextViewItemPostGallery.setText(post.getTitle()); break; } } } } } else if (holder instanceof PostGalleryBaseGalleryTypeViewHolder) { Post post = getItem(position); if (post != null) { ((PostGalleryBaseGalleryTypeViewHolder) holder).post = post; ((PostGalleryBaseGalleryTypeViewHolder) holder).currentPosition = position; if (mHandleReadPost && post.isRead()) { holder.itemView.setBackgroundTintList(ColorStateList.valueOf(mReadPostCardViewBackgroundColor)); } if (mDataSavingMode && mDisableImagePreview) { ((PostGalleryBaseGalleryTypeViewHolder) holder).noPreviewImageView.setVisibility(View.VISIBLE); ((PostGalleryBaseGalleryTypeViewHolder) holder).noPreviewImageView.setImageResource(R.drawable.ic_gallery_day_night_24dp); } else { Post.Preview preview = getSuitablePreviewWithThumbnailFallback(post.getPreviews(), post.getThumbnailUrl()); ((PostGalleryBaseGalleryTypeViewHolder) holder).preview = preview; ((PostGalleryBaseGalleryTypeViewHolder) holder).frameLayout.setVisibility(View.VISIBLE); ((PostGalleryBaseGalleryTypeViewHolder) holder).imageIndexTextView.setText(mActivity.getString(R.string.image_index_in_gallery, 1, post.getGallery().size())); if (preview != null) { if (mFixedHeightPreviewInCard || (preview.getPreviewWidth() <= 0 || preview.getPreviewHeight() <= 0)) { ((PostGalleryBaseGalleryTypeViewHolder) holder).adapter.setRatio(-1); } else { ((PostGalleryBaseGalleryTypeViewHolder) holder).adapter.setRatio((float) preview.getPreviewHeight() / preview.getPreviewWidth()); } } else { ((PostGalleryBaseGalleryTypeViewHolder) holder).adapter.setRatio(-1); } ((PostGalleryBaseGalleryTypeViewHolder) holder).adapter.setGalleryImages(post.getGallery()); ((PostGalleryBaseGalleryTypeViewHolder) holder).adapter.setBlurImage( (post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (post.isSpoiler() && mNeedBlurSpoiler)); } } } } @Nullable private Post.Preview getSuitablePreview(ArrayList previews) { Post.Preview preview; if (!previews.isEmpty()) { int previewIndex; if (mDataSavingMode && previews.size() > 2) { previewIndex = previews.size() / 2; } else { previewIndex = 0; } preview = previews.get(previewIndex); if (preview.getPreviewWidth() * preview.getPreviewHeight() > mMaxResolution) { for (int i = previews.size() - 1; i >= 1; i--) { preview = previews.get(i); if (preview.getPreviewWidth() * preview.getPreviewHeight() <= mMaxResolution) { return preview; } } } return preview; } return null; } private Post.Preview getSuitablePreviewWithThumbnailFallback(ArrayList previews, String thumbnailUrl) { Post.Preview preview = getSuitablePreview(previews); if (preview == null && thumbnailUrl != null && !thumbnailUrl.isEmpty() && !thumbnailUrl.equals("self") && !thumbnailUrl.equals("default") && !thumbnailUrl.equals("nsfw") && !thumbnailUrl.equals("spoiler") && !thumbnailUrl.equals("image") && thumbnailUrl.startsWith("http")) { return new Post.Preview(thumbnailUrl, 0, 0, "", ""); } return preview; } private boolean hasValidThumbnailFallback(String thumbnailUrl) { return thumbnailUrl != null && !thumbnailUrl.isEmpty() && !thumbnailUrl.equals("self") && !thumbnailUrl.equals("default") && !thumbnailUrl.equals("nsfw") && !thumbnailUrl.equals("spoiler") && !thumbnailUrl.equals("image") && thumbnailUrl.startsWith("http"); } private boolean shouldUseCompactLayout(Post post) { return (post.getPreviews() == null || post.getPreviews().isEmpty()) && !hasValidThumbnailFallback(post.getThumbnailUrl()); } private void loadImage(final RecyclerView.ViewHolder holder) { if (holder instanceof PostWithPreviewTypeViewHolder) { ((PostWithPreviewTypeViewHolder) holder).loadingIndicator.setVisibility(View.VISIBLE); Post post = ((PostWithPreviewTypeViewHolder) holder).post; Post.Preview preview = ((PostWithPreviewTypeViewHolder) holder).preview; if (preview != null) { String url; boolean blurImage = (post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit()) && !(post.getPostType() == Post.GIF_TYPE && mAutoplay && mAutoplayNsfwVideos)) || (post.isSpoiler() && mNeedBlurSpoiler); if (post.getPostType() == Post.GIF_TYPE && mAutoplay && !blurImage) { url = post.getUrl(); } else { url = preview.getPreviewUrl(); } RequestBuilder imageRequestBuilder = mGlide.load(url).listener(((PostWithPreviewTypeViewHolder) holder).glideRequestListener); if (blurImage) { imageRequestBuilder.apply(RequestOptions.bitmapTransform(new BlurTransformation(50, 10))) .into(((PostWithPreviewTypeViewHolder) holder).imageView); } else { imageRequestBuilder.centerInside().downsample(mSaveMemoryCenterInsideDownsampleStrategy).into(((PostWithPreviewTypeViewHolder) holder).imageView); } } } else if (holder instanceof PostCompactBaseViewHolder) { Post post = ((PostCompactBaseViewHolder) holder).post; String postCompactThumbnailPreviewUrl = null; ArrayList previews = post.getPreviews(); if (previews != null && !previews.isEmpty()) { if (previews.size() >= 2) { postCompactThumbnailPreviewUrl = previews.get(1).getPreviewUrl(); } else { postCompactThumbnailPreviewUrl = previews.get(0).getPreviewUrl(); } } else { // Use thumbnail as fallback for compact view String thumbnailUrl = post.getThumbnailUrl(); if (thumbnailUrl != null && !thumbnailUrl.isEmpty() && !thumbnailUrl.equals("self") && !thumbnailUrl.equals("default") && !thumbnailUrl.equals("nsfw") && !thumbnailUrl.equals("spoiler") && !thumbnailUrl.equals("image") && thumbnailUrl.startsWith("http")) { postCompactThumbnailPreviewUrl = thumbnailUrl; } } if (postCompactThumbnailPreviewUrl != null) { RequestBuilder imageRequestBuilder = mGlide.load(postCompactThumbnailPreviewUrl) .error(R.drawable.ic_error_outline_black_day_night_24dp).listener(((PostCompactBaseViewHolder) holder).requestListener); if ((post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit())) || (post.isSpoiler() && mNeedBlurSpoiler)) { imageRequestBuilder .transform(new BlurTransformation(50, 2)).into(((PostCompactBaseViewHolder) holder).imageView); } else { imageRequestBuilder.into(((PostCompactBaseViewHolder) holder).imageView); } } } else if (holder instanceof PostGalleryViewHolder) { ((PostGalleryViewHolder) holder).binding.progressBarItemPostGallery.setVisibility(View.VISIBLE); Post post = ((PostGalleryViewHolder) holder).post; Post.Preview preview = ((PostGalleryViewHolder) holder).preview; if (preview != null) { String url; boolean blurImage = (post.isNSFW() && mNeedBlurNsfw && !(mDoNotBlurNsfwInNsfwSubreddits && mFragment != null && mFragment.getIsNsfwSubreddit()) && !(post.getPostType() == Post.GIF_TYPE && mAutoplay && mAutoplayNsfwVideos)) || post.isSpoiler() && mNeedBlurSpoiler; if (post.getPostType() == Post.GIF_TYPE && mAutoplay && !blurImage) { url = post.getUrl(); } else { url = preview.getPreviewUrl(); } RequestBuilder imageRequestBuilder = mGlide.load(url).listener(((PostGalleryViewHolder) holder).requestListener); if (blurImage) { imageRequestBuilder.apply(RequestOptions.bitmapTransform(new BlurTransformation(50, 10))) .into(((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery); } else { imageRequestBuilder.centerInside().downsample(mSaveMemoryCenterInsideDownsampleStrategy).into(((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery); } } } } private void shareLink(Post post) { Bundle bundle = new Bundle(); bundle.putString(ShareBottomSheetFragment.EXTRA_POST_LINK, post.getPermalink()); if (post.getPostType() != Post.TEXT_TYPE) { bundle.putInt(ShareBottomSheetFragment.EXTRA_MEDIA_TYPE, post.getPostType()); switch (post.getPostType()) { case Post.IMAGE_TYPE: case Post.GIF_TYPE: case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: bundle.putString(ShareBottomSheetFragment.EXTRA_MEDIA_LINK, post.getUrl()); break; case Post.VIDEO_TYPE: bundle.putString(ShareBottomSheetFragment.EXTRA_MEDIA_LINK, post.getVideoDownloadUrl()); break; } } bundle.putParcelable(ShareBottomSheetFragment.EXTRA_POST, post); ShareBottomSheetFragment shareBottomSheetFragment = new ShareBottomSheetFragment(); shareBottomSheetFragment.setArguments(bundle); shareBottomSheetFragment.show(mActivity.getSupportFragmentManager(), shareBottomSheetFragment.getTag()); } @Nullable public Post getItemByPosition(int position) { if (position >= 0 && super.getItemCount() > position) { return super.getItem(position); } return null; } public void setVoteButtonsPosition(boolean voteButtonsOnTheRight) { mVoteButtonsOnTheRight = voteButtonsOnTheRight; } public void setPostLayout(int postLayout) { mPostLayout = postLayout; } public void setBlurNsfwAndDoNotBlurNsfwInNsfwSubreddits(boolean needBlurNsfw, boolean doNotBlurNsfwInNsfwSubreddits) { mNeedBlurNsfw = needBlurNsfw; mDoNotBlurNsfwInNsfwSubreddits = doNotBlurNsfwInNsfwSubreddits; } public void setBlurSpoiler(boolean needBlurSpoiler) { mNeedBlurSpoiler = needBlurSpoiler; } public void setShowElapsedTime(boolean showElapsedTime) { mShowElapsedTime = showElapsedTime; } public void setTimeFormat(String timeFormat) { mTimeFormatPattern = timeFormat; } public void setShowDividerInCompactLayout(boolean showDividerInCompactLayout) { mShowDividerInCompactLayout = showDividerInCompactLayout; } public void setShowAbsoluteNumberOfVotes(boolean showAbsoluteNumberOfVotes) { mShowAbsoluteNumberOfVotes = showAbsoluteNumberOfVotes; } public void setAutoplay(boolean autoplay) { mAutoplay = autoplay; } public boolean isAutoplay() { return mAutoplay; } public void setAutoplayNsfwVideos(boolean autoplayNsfwVideos) { mAutoplayNsfwVideos = autoplayNsfwVideos; } public void setMuteAutoplayingVideos(boolean muteAutoplayingVideos) { mMuteAutoplayingVideos = muteAutoplayingVideos; } public void setShowThumbnailOnTheLeftInCompactLayout(boolean showThumbnailOnTheLeftInCompactLayout) { mShowThumbnailOnTheLeftInCompactLayout = showThumbnailOnTheLeftInCompactLayout; } public void setStartAutoplayVisibleAreaOffset(double startAutoplayVisibleAreaOffset) { this.mStartAutoplayVisibleAreaOffset = startAutoplayVisibleAreaOffset / 100.0; } public void setMuteNSFWVideo(boolean muteNSFWVideo) { this.mMuteNSFWVideo = muteNSFWVideo; } public void setLongPressToHideToolbarInCompactLayout(boolean longPressToHideToolbarInCompactLayout) { mLongPressToHideToolbarInCompactLayout = longPressToHideToolbarInCompactLayout; } public void setCompactLayoutToolbarHiddenByDefault(boolean compactLayoutToolbarHiddenByDefault) { mCompactLayoutToolbarHiddenByDefault = compactLayoutToolbarHiddenByDefault; } public void setDataSavingMode(boolean dataSavingMode) { mDataSavingMode = dataSavingMode; } public void setDisableImagePreview(boolean disableImagePreview) { mDisableImagePreview = disableImagePreview; } public void setOnlyDisablePreviewInVideoPosts(boolean onlyDisablePreviewInVideoAndGifPosts) { mOnlyDisablePreviewInVideoAndGifPosts = onlyDisablePreviewInVideoAndGifPosts; } public void setHidePostType(boolean hidePostType) { mHidePostType = hidePostType; } public void setHidePostFlair(boolean hidePostFlair) { mHidePostFlair = hidePostFlair; } public void setHideSubredditAndUserPrefix(boolean hideSubredditAndUserPrefix) { mHideSubredditAndUserPrefix = hideSubredditAndUserPrefix; } public void setHideTheNumberOfVotes(boolean hideTheNumberOfVotes) { mHideTheNumberOfVotes = hideTheNumberOfVotes; } public void setHideTheNumberOfComments(boolean hideTheNumberOfComments) { mHideTheNumberOfComments = hideTheNumberOfComments; } public void setDefaultLinkPostLayout(int defaultLinkPostLayout) { mDefaultLinkPostLayout = defaultLinkPostLayout; } public void setFixedHeightPreviewInCard(boolean fixedHeightPreviewInCard) { mFixedHeightPreviewInCard = fixedHeightPreviewInCard; } public void setHideTextPostContent(boolean hideTextPostContent) { mHideTextPostContent = hideTextPostContent; } public void setPostFeedMaxResolution(int postFeedMaxResolution) { mMaxResolution = postFeedMaxResolution; if (mSaveMemoryCenterInsideDownsampleStrategy != null) { mSaveMemoryCenterInsideDownsampleStrategy.setThreshold(postFeedMaxResolution); } } public void setEasierToWatchInFullScreen(boolean easierToWatchInFullScreen) { this.mEasierToWatchInFullScreen = easierToWatchInFullScreen; } public void setLongPressPostNonMediaAreaAction(String value) { mLongPressPostNonMediaAreaAction = value; } public void setLongPressPostMediaAction(String value) { mLongPressPostMediaAction = value; } public void setDataSavingModeDefaultResolution(int value) { mDataSavingModeDefaultResolution = value; } public void setNonDataSavingModeDefaultResolution(int value) { mNonDataSavingModeDefaultResolution = value; } public void setSimultaneousAutoplayLimit(int limit) { mSimultaneousAutoplayLimit = limit; multiPlayPlayerSelector.setSimultaneousAutoplayLimit(limit); } @OptIn(markerClass = UnstableApi.class) @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof PostViewHolder) { if (mHandleReadPost && mMarkPostsAsReadOnScroll) { int position = ((PostViewHolder) holder).currentPosition; if (position < getItemCount() && position >= 0) { Post post = getItem(position); ((PostViewHolder) holder).markPostRead(post, false); } } ((PostViewHolder) holder).setItemViewBackgroundColor(false); ((PostViewHolder) holder).titleTextView.setTextColor(mPostTitleColor); mGlide.clear(((PostViewHolder) holder).iconGifImageView); ((PostViewHolder) holder).stickiedPostImageView.setVisibility(View.GONE); if (((PostViewHolder) holder).crosspostImageView != null) { ((PostViewHolder) holder).crosspostImageView.setVisibility(View.GONE); } if (((PostViewHolder) holder).archivedImageView != null) { ((PostViewHolder) holder).archivedImageView.setVisibility(View.GONE); } if (((PostViewHolder) holder).lockedImageView != null) { ((PostViewHolder) holder).lockedImageView.setVisibility(View.GONE); } if (((PostViewHolder) holder).nsfwTextView != null) { ((PostViewHolder) holder).nsfwTextView.setVisibility(View.GONE); } if (((PostViewHolder) holder).spoilerTextView != null) { ((PostViewHolder) holder).spoilerTextView.setVisibility(View.GONE); } if (((PostViewHolder) holder).flairTextView != null) { ((PostViewHolder) holder).flairTextView.setText(""); ((PostViewHolder) holder).flairTextView.setVisibility(View.GONE); } ((PostViewHolder) holder).upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); ((PostViewHolder) holder).upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); ((PostViewHolder) holder).scoreTextView.setTextColor(mPostIconAndInfoColor); ((PostViewHolder) holder).downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); ((PostViewHolder) holder).downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (holder instanceof PostBaseViewHolder) { if (holder instanceof PostBaseVideoAutoplayViewHolder) { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.mediaUri = null; if (((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.fetchRedgifsOrStreamableVideoCall != null && !((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.fetchRedgifsOrStreamableVideoCall.isCanceled()) { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.fetchRedgifsOrStreamableVideoCall.cancel(); ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.fetchRedgifsOrStreamableVideoCall = null; } ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.errorLoadingRedgifsImageView.setVisibility(View.GONE); ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.videoQualityButton.setVisibility(View.GONE); ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.muteButton.setVisibility(View.GONE); if (!((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.isManuallyPaused) { ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.resetVolume(); } mGlide.clear(((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.previewImageView); ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.previewImageView.setVisibility(View.GONE); ((PostBaseVideoAutoplayViewHolder) holder).toroPlayer.setDefaultResolutionAlready = false; } else if (holder instanceof PostWithPreviewTypeViewHolder) { mGlide.clear(((PostWithPreviewTypeViewHolder) holder).imageView); if (((PostWithPreviewTypeViewHolder) holder).imageWrapperFrameLayout != null) { ((PostWithPreviewTypeViewHolder) holder).imageWrapperFrameLayout.setVisibility(View.GONE); } ((PostWithPreviewTypeViewHolder) holder).imageView.setVisibility(View.GONE); ((PostWithPreviewTypeViewHolder) holder).loadImageErrorTextView.setVisibility(View.GONE); ((PostWithPreviewTypeViewHolder) holder).imageViewNoPreviewGallery.setVisibility(View.GONE); ((PostWithPreviewTypeViewHolder) holder).loadingIndicator.setVisibility(View.GONE); ((PostWithPreviewTypeViewHolder) holder).videoOrGifIndicator.setVisibility(View.GONE); ((PostWithPreviewTypeViewHolder) holder).linkTextView.setVisibility(View.GONE); } else if (holder instanceof PostBaseGalleryTypeViewHolder) { ((PostBaseGalleryTypeViewHolder) holder).frameLayout.setVisibility(View.GONE); ((PostBaseGalleryTypeViewHolder) holder).noPreviewImageView.setVisibility(View.GONE); ((PostBaseGalleryTypeViewHolder) holder).adapter.setGalleryImages(null); } else if (holder instanceof PostTextTypeViewHolder) { ((PostTextTypeViewHolder) holder).contentTextView.setText(""); ((PostTextTypeViewHolder) holder).contentTextView.setTextColor(mPostContentColor); ((PostTextTypeViewHolder) holder).contentTextView.setVisibility(View.GONE); } } else if (holder instanceof PostCompactBaseViewHolder) { mGlide.clear(((PostCompactBaseViewHolder) holder).imageView); ((PostCompactBaseViewHolder) holder).relativeLayout.setVisibility(View.GONE); if (((PostCompactBaseViewHolder) holder).linkTextView != null) { ((PostCompactBaseViewHolder) holder).linkTextView.setVisibility(View.GONE); } ((PostCompactBaseViewHolder) holder).loadingIndicator.setVisibility(View.GONE); ((PostCompactBaseViewHolder) holder).imageView.setVisibility(View.GONE); ((PostCompactBaseViewHolder) holder).playButtonImageView.setVisibility(View.GONE); ((PostCompactBaseViewHolder) holder).noPreviewPostImageFrameLayout.setVisibility(View.GONE); } } else if (holder instanceof PostGalleryViewHolder) { if (mHandleReadPost && mMarkPostsAsReadOnScroll) { int position = ((PostGalleryViewHolder) holder).currentPosition; if (position < super.getItemCount() && position >= 0) { Post post = getItem(position); ((PostGalleryViewHolder) holder).markPostRead(post, false); } } holder.itemView.setBackgroundTintList(ColorStateList.valueOf(mCardViewBackgroundColor)); ((PostGalleryViewHolder) holder).binding.titleTextViewItemPostGallery.setText(""); ((PostGalleryViewHolder) holder).binding.titleTextViewItemPostGallery.setVisibility(View.GONE); mGlide.clear(((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery); ((PostGalleryViewHolder) holder).binding.imageViewItemPostGallery.setVisibility(View.GONE); ((PostGalleryViewHolder) holder).binding.progressBarItemPostGallery.setVisibility(View.GONE); ((PostGalleryViewHolder) holder).binding.loadImageErrorTextViewItemGallery.setVisibility(View.GONE); ((PostGalleryViewHolder) holder).binding.videoOrGifIndicatorImageViewItemPostGallery.setVisibility(View.GONE); ((PostGalleryViewHolder) holder).binding.imageViewNoPreviewItemPostGallery.setVisibility(View.GONE); } else if (holder instanceof PostGalleryBaseGalleryTypeViewHolder) { if (mHandleReadPost && mMarkPostsAsReadOnScroll) { int position = ((PostGalleryBaseGalleryTypeViewHolder) holder).currentPosition; if (position < super.getItemCount() && position >= 0) { Post post = getItem(position); ((PostGalleryBaseGalleryTypeViewHolder) holder).markPostRead(post, false); } } holder.itemView.setBackgroundTintList(ColorStateList.valueOf(mCardViewBackgroundColor)); ((PostGalleryBaseGalleryTypeViewHolder) holder).frameLayout.setVisibility(View.GONE); ((PostGalleryBaseGalleryTypeViewHolder) holder).noPreviewImageView.setVisibility(View.GONE); } } @Nullable @Override public Object getKeyForOrder(int order) { if (super.getItemCount() <= 0 || order >= super.getItemCount()) { return null; } return order; } @Nullable @Override public Integer getOrderForKey(@NonNull Object key) { if (key instanceof Integer) { return (Integer) key; } return null; } public void onItemSwipe(RecyclerView.ViewHolder viewHolder, int direction, int swipeLeftAction, int swipeRightAction) { if (viewHolder instanceof PostBaseViewHolder) { if (direction == ItemTouchHelper.LEFT || direction == ItemTouchHelper.START) { if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((PostBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((PostBaseViewHolder) viewHolder).downvoteButton.performClick(); } } else { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((PostBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((PostBaseViewHolder) viewHolder).downvoteButton.performClick(); } } } else if (viewHolder instanceof PostCompactBaseViewHolder) { if (direction == ItemTouchHelper.LEFT || direction == ItemTouchHelper.START) { if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((PostCompactBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((PostCompactBaseViewHolder) viewHolder).downvoteButton.performClick(); } } else { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { ((PostCompactBaseViewHolder) viewHolder).upvoteButton.performClick(); } else if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { ((PostCompactBaseViewHolder) viewHolder).downvoteButton.performClick(); } } } } public interface Callback { void typeChipClicked(int filter); void flairChipClicked(String flair); void nsfwChipClicked(); void currentlyBindItem(int position); void delayTransition(); } private void openViewPostDetailActivity(Post post, int position) { if (canStartActivity) { canStartActivity = false; Intent intent = new Intent(mActivity, ViewPostDetailActivity.class); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_DATA, post); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_LIST_POSITION, position); intent.putExtra(ViewPostDetailActivity.EXTRA_POST_FRAGMENT_ID, mFragment.getPostFragmentId()); intent.putExtra(ViewPostDetailActivity.EXTRA_IS_NSFW_SUBREDDIT, mFragment.getIsNsfwSubreddit()); mActivity.startActivity(intent); } } private void openMedia(Post post) { openMedia(post, 0, false); } private void openMedia(Post post, boolean peekMedia) { openMedia(post, 0, peekMedia); } private void openMedia(Post post, int galleryItemIndex, boolean peekMedia) { openMedia(post, galleryItemIndex, -1, peekMedia); } private void openMedia(Post post, long videoProgress) { openMedia(post, 0, videoProgress, false); } private void openMedia(Post post, int galleryItemIndex, long videoProgress, boolean peekMedia) { if (canStartActivity) { canStartActivity = false; if (post.getPostType() == Post.VIDEO_TYPE) { if (peekMedia) { mActivity.setShouldTrackFullscreenMediaPeekTouchEvent(true); } Intent intent = new Intent(mActivity, ViewVideoActivity.class); if (post.isImgur()) { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_IMGUR); } else if (post.isRedgifs()) { intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_REDGIFS); intent.putExtra(ViewVideoActivity.EXTRA_REDGIFS_ID, post.getRedgifsId()); intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); /*if (post.isLoadRedgifsOrStreamableVideoSuccess()) { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); }*/ } else if (post.isStreamable()) { intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_STREAMABLE); intent.putExtra(ViewVideoActivity.EXTRA_STREAMABLE_SHORT_CODE, post.getStreamableShortCode()); if (post.isLoadedStreamableVideoAlready()) { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); } } else { intent.setData(Uri.parse(post.getVideoUrl())); intent.putExtra(ViewVideoActivity.EXTRA_SUBREDDIT, post.getSubredditName()); intent.putExtra(ViewVideoActivity.EXTRA_ID, post.getId()); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getVideoDownloadUrl()); } intent.putExtra(ViewVideoActivity.EXTRA_POST, post); if (videoProgress > 0) { intent.putExtra(ViewVideoActivity.EXTRA_PROGRESS_SECONDS, videoProgress); } intent.putExtra(ViewVideoActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } else if (post.getPostType() == Post.IMAGE_TYPE) { if (peekMedia) { mActivity.setShouldTrackFullscreenMediaPeekTouchEvent(true); } Intent intent = new Intent(mActivity, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, post.getUrl()); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, post.getSubredditName() + "-" + post.getId() + ".jpg"); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } else if (post.getPostType() == Post.GIF_TYPE) { if (peekMedia) { mActivity.setShouldTrackFullscreenMediaPeekTouchEvent(true); } if (post.getMp4Variant() != null) { Intent intent = new Intent(mActivity, ViewVideoActivity.class); intent.setData(Uri.parse(post.getMp4Variant())); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_TYPE, ViewVideoActivity.VIDEO_TYPE_DIRECT); intent.putExtra(ViewVideoActivity.EXTRA_SUBREDDIT, post.getSubredditName()); intent.putExtra(ViewVideoActivity.EXTRA_ID, post.getId()); intent.putExtra(ViewVideoActivity.EXTRA_VIDEO_DOWNLOAD_URL, post.getMp4Variant()); intent.putExtra(ViewVideoActivity.EXTRA_POST, post); intent.putExtra(ViewVideoActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } else { Intent intent = new Intent(mActivity, ViewImageOrGifActivity.class); intent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, post.getSubredditName() + "-" + post.getId() + ".gif"); intent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, post.getVideoUrl()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); intent.putExtra(ViewImageOrGifActivity.EXTRA_POST_ID_KEY, post.getId()); intent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, post.getSubredditName()); intent.putExtra(ViewImageOrGifActivity.EXTRA_IS_NSFW, post.isNSFW()); mActivity.startActivity(intent); } } else if (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { if (peekMedia) { canStartActivity = true; } else { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(post.getUrl()); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_IS_NSFW, post.isNSFW()); intent.putExtra(LinkResolverActivity.EXTRA_SUBREDDIT_NAME, post.getSubredditName()); intent.putExtra(LinkResolverActivity.EXTRA_POST_TITLE_KEY, post.getTitle()); mActivity.startActivity(intent); } } else if (post.getPostType() == Post.GALLERY_TYPE) { if (peekMedia) { mActivity.setShouldTrackFullscreenMediaPeekTouchEvent(true); } Intent intent = new Intent(mActivity, ViewRedditGalleryActivity.class); intent.putExtra(ViewRedditGalleryActivity.EXTRA_POST, post); intent.putExtra(ViewRedditGalleryActivity.EXTRA_GALLERY_ITEM_INDEX, galleryItemIndex); mActivity.startActivity(intent); } } } public void setCanPlayVideo(boolean canPlayVideo) { this.canPlayVideo = canPlayVideo; } public abstract class PostViewHolder extends RecyclerView.ViewHolder { AspectRatioGifImageView iconGifImageView; ImageView stickiedPostImageView; TextView postTimeTextView; TextView titleTextView; @Nullable CustomTextView typeTextView; @Nullable ImageView archivedImageView; @Nullable ImageView lockedImageView; @Nullable ImageView crosspostImageView; @Nullable CustomTextView nsfwTextView; @Nullable CustomTextView spoilerTextView; @Nullable CustomTextView flairTextView; MaterialButton upvoteButton; TextView scoreTextView; MaterialButton downvoteButton; @Nullable MaterialButton commentsCountButton; @Nullable MaterialButton saveButton; @Nullable MaterialButton shareButton; Post post; int currentPosition; public PostViewHolder(@NonNull View itemView) { super(itemView); } void startSubredditOrUserActivity(String subredditName) { if (subredditName.startsWith("u_")) { Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, subredditName.substring(2)); mActivity.startActivity(intent); } else { Intent intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditName); mActivity.startActivity(intent); } } void setBaseView(AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable ImageView crosspostImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, @Nullable MaterialButton commentsCountButton, @Nullable MaterialButton saveButton, @Nullable MaterialButton shareButton) { this.iconGifImageView = iconGifImageView; this.stickiedPostImageView = stickiedPostImageView; this.postTimeTextView = postTimeTextView; this.titleTextView = titleTextView; this.typeTextView = typeTextView; this.archivedImageView = archivedImageView; this.lockedImageView = lockedImageView; this.crosspostImageView = crosspostImageView; this.nsfwTextView = nsfwTextView; this.spoilerTextView = spoilerTextView; this.flairTextView = flairTextView; this.upvoteButton = upvoteButton; this.scoreTextView = scoreTextView; this.downvoteButton = downvoteButton; this.commentsCountButton = commentsCountButton; this.saveButton = saveButton; this.shareButton = shareButton; if (mDisplaySubredditName) { subredditTextView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { if (canStartActivity) { canStartActivity = false; startSubredditOrUserActivity(post.getSubredditName()); } } }); iconGifImageView.setOnClickListener(view -> subredditTextView.performClick()); } else { subredditTextView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { if (canStartActivity) { canStartActivity = false; startSubredditOrUserActivity(post.getSubredditName()); } } }); iconGifImageView.setOnClickListener(view -> userTextView.performClick()); } userTextView.setOnClickListener(view -> { if (!canStartActivity) { return; } int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post == null || post.isAuthorDeleted()) { return; } canStartActivity = false; Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, post.getAuthor()); mActivity.startActivity(intent); }); setOnClickListeners(typeTextView, nsfwTextView, flairTextView, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); } void setBaseView(AspectRatioGifImageView iconGifImageView, TextView nameTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable ImageView crosspostImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, @Nullable MaterialButton commentsCountButton, @Nullable MaterialButton saveButton, @Nullable MaterialButton shareButton) { this.iconGifImageView = iconGifImageView; this.stickiedPostImageView = stickiedPostImageView; this.postTimeTextView = postTimeTextView; this.titleTextView = titleTextView; this.typeTextView = typeTextView; this.archivedImageView = archivedImageView; this.lockedImageView = lockedImageView; this.crosspostImageView = crosspostImageView; this.nsfwTextView = nsfwTextView; this.spoilerTextView = spoilerTextView; this.flairTextView = flairTextView; this.upvoteButton = upvoteButton; this.scoreTextView = scoreTextView; this.downvoteButton = downvoteButton; this.commentsCountButton = commentsCountButton; this.saveButton = saveButton; this.shareButton = shareButton; nameTextView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null && canStartActivity) { canStartActivity = false; if (mDisplaySubredditName) { startSubredditOrUserActivity(post.getSubredditName()); } else if (!post.isAuthorDeleted()) { Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, post.getAuthor()); mActivity.startActivity(intent); } } }); iconGifImageView.setOnClickListener(view -> nameTextView.performClick()); setOnClickListeners(typeTextView, nsfwTextView, flairTextView, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); } void setOnClickListeners(MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, @Nullable MaterialButton commentsCountButton, @Nullable MaterialButton saveButton, @Nullable MaterialButton shareButton) { itemView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position >= 0 && canStartActivity) { Post post = getItem(position); if (post != null) { markPostRead(post, true); openViewPostDetailActivity(post, getBindingAdapterPosition()); } } }); upvoteButton.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } if (mMarkPostsAsReadAfterVoting) { markPostRead(post, true); } if (post.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_vote_unavailable, Toast.LENGTH_SHORT).show(); return; } ColorStateList previousUpvoteButtonIconTint = upvoteButton.getIconTint(); ColorStateList previousDownvoteButtonIconTint = downvoteButton.getIconTint(); int previousScoreTextViewColor = scoreTextView.getCurrentTextColor(); Drawable previousUpvoteButtonDrawable = upvoteButton.getIcon(); Drawable previousDownvoteButtonDrawable = downvoteButton.getIcon(); int previousVoteType = post.getVoteType(); String newVoteType; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (previousVoteType != 1) { //Not upvoted before post.setVoteType(1); newVoteType = APIUtils.DIR_UPVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); } else { //Upvoted before post.setVoteType(0); newVoteType = APIUtils.DIR_UNVOTE; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, post.getScore() + post.getVoteType())); } VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingListener() { @Override public void onVoteThingSuccess(int position1) { int currentPosition = getBindingAdapterPosition(); if (newVoteType.equals(APIUtils.DIR_UPVOTE)) { post.setVoteType(1); if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_filled_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mUpvotedColor)); scoreTextView.setTextColor(mUpvotedColor); } } else { post.setVoteType(0); if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } } if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, post.getScore() + post.getVoteType())); } } EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } @Override public void onVoteThingFail(int position1) { Toast.makeText(mActivity, R.string.vote_failed, Toast.LENGTH_SHORT).show(); post.setVoteType(previousVoteType); if (getBindingAdapterPosition() == position) { if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, post.getScore() + previousVoteType)); } upvoteButton.setIcon(previousUpvoteButtonDrawable); upvoteButton.setIconTint(previousUpvoteButtonIconTint); scoreTextView.setTextColor(previousScoreTextViewColor); downvoteButton.setIcon(previousDownvoteButtonDrawable); downvoteButton.setIconTint(previousDownvoteButtonIconTint); } EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } }, post.getFullName(), newVoteType, getBindingAdapterPosition()); } }); scoreTextView.setOnClickListener(view -> { upvoteButton.performClick(); }); downvoteButton.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } if (mMarkPostsAsReadAfterVoting) { markPostRead(post, true); } if (post.isArchived()) { Toast.makeText(mActivity, R.string.archived_post_vote_unavailable, Toast.LENGTH_SHORT).show(); return; } ColorStateList previousUpvoteButtonIconTint = upvoteButton.getIconTint(); ColorStateList previousDownvoteButtonIconTint = downvoteButton.getIconTint(); int previousScoreTextViewColor = scoreTextView.getCurrentTextColor(); Drawable previousUpvoteButtonDrawable = upvoteButton.getIcon(); Drawable previousDownvoteButtonDrawable = downvoteButton.getIcon(); int previousVoteType = post.getVoteType(); String newVoteType; upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (previousVoteType != -1) { //Not downvoted before post.setVoteType(-1); newVoteType = APIUtils.DIR_DOWNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); } else { //Downvoted before post.setVoteType(0); newVoteType = APIUtils.DIR_UNVOTE; downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, post.getScore() + post.getVoteType())); } VoteThing.voteThing(mActivity, mOauthRetrofit, mAccessToken, new VoteThing.VoteThingListener() { @Override public void onVoteThingSuccess(int position1) { int currentPosition = getBindingAdapterPosition(); if (newVoteType.equals(APIUtils.DIR_DOWNVOTE)) { post.setVoteType(-1); if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_filled_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mDownvotedColor)); scoreTextView.setTextColor(mDownvotedColor); } } else { post.setVoteType(0); if (currentPosition == position) { downvoteButton.setIconResource(R.drawable.ic_downvote_24dp); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); } } if (currentPosition == position) { upvoteButton.setIconResource(R.drawable.ic_upvote_24dp); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, post.getScore() + post.getVoteType())); } } EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } @Override public void onVoteThingFail(int position1) { Toast.makeText(mActivity, R.string.vote_failed, Toast.LENGTH_SHORT).show(); post.setVoteType(previousVoteType); if (getBindingAdapterPosition() == position) { if (!mHideTheNumberOfVotes) { scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, post.getScore() + previousVoteType)); } upvoteButton.setIcon(previousUpvoteButtonDrawable); upvoteButton.setIconTint(previousUpvoteButtonIconTint); scoreTextView.setTextColor(previousScoreTextViewColor); downvoteButton.setIcon(previousDownvoteButtonDrawable); downvoteButton.setIconTint(previousDownvoteButtonIconTint); } EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } }, post.getFullName(), newVoteType, getBindingAdapterPosition()); } }); if (commentsCountButton != null) { commentsCountButton.setOnClickListener(view -> itemView.performClick()); } if (saveButton != null) { saveButton.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { if (mAccountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return; } if (post.isSaved()) { saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); SaveThing.unsaveThing(mOauthRetrofit, mAccessToken, post.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { post.setSaved(false); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } Toast.makeText(mActivity, R.string.post_unsaved_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } @Override public void failed() { post.setSaved(true); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } Toast.makeText(mActivity, R.string.post_unsaved_failed, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } }); } else { saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); SaveThing.saveThing(mOauthRetrofit, mAccessToken, post.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { post.setSaved(true); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_grey_24dp); } Toast.makeText(mActivity, R.string.post_saved_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } @Override public void failed() { post.setSaved(false); if (getBindingAdapterPosition() == position) { saveButton.setIconResource(R.drawable.ic_bookmark_border_grey_24dp); } Toast.makeText(mActivity, R.string.post_saved_failed, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(post)); } }); } } }); } if (shareButton != null) { shareButton.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { shareLink(post); } }); shareButton.setOnLongClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return false; } Post post = getItem(position); if (post != null) { mActivity.copyLink(post.getPermalink()); return true; } return false; }); } } void setOnClickListeners(@Nullable CustomTextView typeTextView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView flairTextView, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, @Nullable MaterialButton commentsCountButton, @Nullable MaterialButton saveButton, @Nullable MaterialButton shareButton) { setOnClickListeners(upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); if (!(mActivity instanceof FilteredPostsActivity)) { if (nsfwTextView != null) { nsfwTextView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { mCallback.nsfwChipClicked(); } }); } if (typeTextView != null) { typeTextView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { mCallback.typeChipClicked(post.getPostType()); } }); } if (flairTextView != null) { flairTextView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { mCallback.flairChipClicked(post.getFlair()); } }); } } } abstract void setItemViewBackgroundColor(boolean isReadPost); abstract void markPostRead(Post post, boolean changePostItemColor); } @UnstableApi public abstract class VideoAutoplayImpl implements ToroPlayer { View itemView; AspectRatioFrameLayout aspectRatioFrameLayout; GifImageView previewImageView; ImageView errorLoadingRedgifsImageView; PlayerView videoPlayer; ImageView videoQualityButton; ImageView muteButton; ImageView fullscreenButton; ImageView playPauseButton; @Nullable Container container; @Nullable ExoPlayerViewHelper helper; private Uri mediaUri; private float volume; public Call fetchRedgifsOrStreamableVideoCall; private boolean isManuallyPaused; private Drawable playDrawable; private Drawable pauseDrawable; private boolean setDefaultResolutionAlready; public VideoAutoplayImpl(View itemView, AspectRatioFrameLayout aspectRatioFrameLayout, GifImageView previewImageView, ImageView errorLoadingRedgifsImageView, PlayerView videoPlayer, ImageView videoQualityButton, ImageView muteButton, ImageView fullscreenButton, ImageView playPauseButton, DefaultTimeBar progressBar, Drawable playDrawable, Drawable pauseDrawable) { this.itemView = itemView; this.aspectRatioFrameLayout = aspectRatioFrameLayout; this.previewImageView = previewImageView; this.errorLoadingRedgifsImageView = errorLoadingRedgifsImageView; this.videoPlayer = videoPlayer; this.videoQualityButton = videoQualityButton; this.muteButton = muteButton; this.fullscreenButton = fullscreenButton; this.playPauseButton = playPauseButton; this.playDrawable = playDrawable; this.pauseDrawable = pauseDrawable; aspectRatioFrameLayout.setOnClickListener(null); muteButton.setOnClickListener(view -> { if (helper != null) { if (helper.getVolume() != 0) { muteButton.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_mute_24dp)); helper.setVolume(0f); volume = 0f; mFragment.videoAutoplayChangeMutingOption(true); } else { muteButton.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_unmute_24dp)); helper.setVolume(1f); volume = 1f; mFragment.videoAutoplayChangeMutingOption(false); } } }); fullscreenButton.setOnClickListener(view -> { Post post = getPost(); if (post != null) { markPostRead(post, true); if (helper != null) { openMedia(post, helper.getLatestPlaybackInfo().getResumePosition()); } else { openMedia(post, -1); } } }); playPauseButton.setOnClickListener(view -> { if (isPlaying()) { pause(); isManuallyPaused = true; savePlaybackInfo(getPlayerOrder(), getCurrentPlaybackInfo()); } else { isManuallyPaused = false; play(); } }); progressBar.addListener(new TimeBar.OnScrubListener() { @Override public void onScrubStart(TimeBar timeBar, long position) { } @Override public void onScrubMove(TimeBar timeBar, long position) { } @Override public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { if (!canceled) { savePlaybackInfo(getPlayerOrder(), getCurrentPlaybackInfo()); } } }); previewImageView.setOnClickListener(view -> fullscreenButton.performClick()); videoPlayer.setOnClickListener(view -> { if (mEasierToWatchInFullScreen && videoPlayer.isControllerFullyVisible()) { fullscreenButton.performClick(); } }); } void bindVideoUri(Uri videoUri) { mediaUri = videoUri; } void setVolume(float volume) { this.volume = volume; } void resetVolume() { volume = 0f; } private void savePlaybackInfo(int order, @Nullable PlaybackInfo playbackInfo) { if (container != null) container.savePlaybackInfo(order, playbackInfo); } void loadVideo(int position) { Post post = getPost(); /*if (post.isRedgifs() && !post.isLoadedStreamableVideoAlready()) { fetchRedgifsOrStreamableVideoCall = mRedgifsRetrofit.create(RedgifsAPI.class).getRedgifsData( APIUtils.getRedgifsOAuthHeader(mCurrentAccountSharedPreferences .getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")), post.getRedgifsId(), APIUtils.USER_AGENT); FetchRedgifsVideoLinks.fetchRedgifsVideoLinksInRecyclerViewAdapter(mExecutor, new Handler(), fetchRedgifsOrStreamableVideoCall, new FetchVideoLinkListener() { @Override public void onFetchRedgifsVideoLinkSuccess(String webm, String mp4) { post.setVideoDownloadUrl(mp4); post.setVideoUrl(mp4); post.setLoadedStreamableVideoAlready(true); if (position == getAdapterPosition()) { bindVideoUri(Uri.parse(post.getVideoUrl())); } } @Override public void failed(@Nullable Integer messageRes) { if (position == getAdapterPosition()) { loadFallbackDirectVideo(); } } }); } else */if(post.isStreamable() && !post.isLoadedStreamableVideoAlready()) { fetchRedgifsOrStreamableVideoCall = mStreamableApiProvider.get().getStreamableData(post.getStreamableShortCode()); FetchStreamableVideo.fetchStreamableVideoInRecyclerViewAdapter(mExecutor, new Handler(), fetchRedgifsOrStreamableVideoCall, new FetchVideoLinkListener() { @Override public void onFetchStreamableVideoLinkSuccess(StreamableVideo streamableVideo) { StreamableVideo.Media media = streamableVideo.mp4 == null ? streamableVideo.mp4Mobile : streamableVideo.mp4; post.setVideoDownloadUrl(media.url); post.setVideoUrl(media.url); post.setLoadedStreamableVideoAlready(true); if (position == getAdapterPosition()) { bindVideoUri(Uri.parse(post.getVideoUrl())); } } @Override public void failed(@Nullable Integer messageRes) { if (position == getAdapterPosition()) { loadFallbackDirectVideo(); } } }); } else { bindVideoUri(Uri.parse(post.getVideoUrl())); } } void loadFallbackDirectVideo() { Post post = getPost(); if (post.getVideoFallBackDirectUrl() != null) { mediaUri = Uri.parse(post.getVideoFallBackDirectUrl()); post.setVideoDownloadUrl(post.getVideoFallBackDirectUrl()); post.setVideoUrl(post.getVideoFallBackDirectUrl()); post.setLoadedStreamableVideoAlready(true); if (container != null) { container.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); } } } @NonNull @Override public View getPlayerView() { return videoPlayer; } @NonNull @Override public PlaybackInfo getCurrentPlaybackInfo() { return helper != null && mediaUri != null ? helper.getLatestPlaybackInfo() : new PlaybackInfo(); } @OptIn(markerClass = UnstableApi.class) @Override public void initialize(@NonNull Container container, @NonNull PlaybackInfo playbackInfo) { if (this.container == null) { this.container = container; this.container.setPlayerSelector(multiPlayPlayerSelector); } if (mediaUri == null) { return; } if (helper == null) { helper = new ExoPlayerViewHelper(this, mediaUri, null, mExoCreator); helper.addEventListener(new Playable.DefaultEventListener() { @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { if (events.containsAny( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { playPauseButton.setImageDrawable(Util.shouldShowPlayButton(player) ? playDrawable : pauseDrawable); } } @Override public void onTracksChanged(@NonNull Tracks tracks) { ImmutableList trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { if (getPost().isNormalVideo()) { videoQualityButton.setVisibility(View.VISIBLE); videoQualityButton.setOnClickListener(view -> { TrackSelectionDialogBuilder builder = new TrackSelectionDialogBuilder(mActivity, mActivity.getString(R.string.select_video_quality), helper.getPlayer(), C.TRACK_TYPE_VIDEO); builder.setShowDisableOption(true); builder.setAllowAdaptiveSelections(false); Dialog dialog = builder.setTheme(R.style.MaterialAlertDialogTheme).build(); dialog.show(); if (dialog instanceof AlertDialog) { ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); } }); if (!setDefaultResolutionAlready) { int desiredResolution = 0; if (mDataSavingMode) { if (mDataSavingModeDefaultResolution > 0) { desiredResolution = mDataSavingModeDefaultResolution; } } else if (mNonDataSavingModeDefaultResolution > 0) { desiredResolution = mNonDataSavingModeDefaultResolution; } if (desiredResolution > 0) { TrackSelectionOverride trackSelectionOverride = null; int bestTrackIndex = -1; int bestResolution = -1; int worstResolution = Integer.MAX_VALUE; int worstTrackIndex = -1; Tracks.Group bestTrackGroup = null; Tracks.Group worstTrackGroup = null; for (Tracks.Group trackGroup : tracks.getGroups()) { if (trackGroup.getType() == C.TRACK_TYPE_VIDEO) { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { int trackResolution = Math.min(trackGroup.getTrackFormat(trackIndex).height, trackGroup.getTrackFormat(trackIndex).width); if (trackResolution <= desiredResolution && trackResolution > bestResolution) { bestTrackIndex = trackIndex; bestResolution = trackResolution; bestTrackGroup = trackGroup; } if (trackResolution < worstResolution) { worstTrackIndex = trackIndex; worstResolution = trackResolution; worstTrackGroup = trackGroup; } } } } if (bestTrackIndex != -1 && bestTrackGroup != null) { trackSelectionOverride = new TrackSelectionOverride( bestTrackGroup.getMediaTrackGroup(), ImmutableList.of(bestTrackIndex) ); } else if (worstTrackIndex != -1 && worstTrackGroup != null) { trackSelectionOverride = new TrackSelectionOverride( worstTrackGroup.getMediaTrackGroup(), ImmutableList.of(worstTrackIndex) ); } if (trackSelectionOverride != null) { helper.getPlayer().setTrackSelectionParameters( helper.getPlayer().getTrackSelectionParameters() .buildUpon() .addOverride(trackSelectionOverride) .build() ); } } setDefaultResolutionAlready = true; } } for (int i = 0; i < trackGroups.size(); i++) { String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { if (mFragment.getMasterMutingOption() != null) { volume = mFragment.getMasterMutingOption() ? 0f : 1f; } helper.setVolume(volume); muteButton.setVisibility(View.VISIBLE); if (volume != 0f) { muteButton.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_unmute_24dp)); } else { muteButton.setImageDrawable(ContextCompat.getDrawable(mActivity, R.drawable.ic_mute_24dp)); } break; } } } else { muteButton.setVisibility(View.GONE); } } @Override public void onRenderedFirstFrame() { // Don't clear the preview image - just hide it // This allows it to be shown again if the player is released while scrolling previewImageView.setVisibility(View.GONE); } @Override public void onPlayerError(@NonNull PlaybackException error) { Post post = getPost(); if (post.getVideoFallBackDirectUrl() == null || post.getVideoFallBackDirectUrl().equals(mediaUri.toString())) { errorLoadingRedgifsImageView.setVisibility(View.VISIBLE); } else { loadFallbackDirectVideo(); } } }); } helper.initialize(container, playbackInfo); } @Override public void play() { if (helper != null && mediaUri != null) { if (!isPlaying() && isManuallyPaused) { helper.play(); pause(); helper.setVolume(volume); } else { helper.play(); } } } @Override public void pause() { if (helper != null) helper.pause(); } @Override public boolean isPlaying() { return helper != null && helper.isPlaying(); } @Override public void release() { if (helper != null) { helper.release(); helper = null; } // Show the preview image again when player is released if (previewImageView != null) { previewImageView.setVisibility(View.VISIBLE); } isManuallyPaused = false; container = null; } @Override public boolean wantsToPlay() { return canPlayVideo && mediaUri != null && ToroUtil.visibleAreaOffset(this, itemView.getParent()) >= mStartAutoplayVisibleAreaOffset; } abstract int getAdapterPosition(); abstract Post getPost(); abstract void markPostRead(Post post, boolean changePostItemColor); } public abstract class PostBaseViewHolder extends PostViewHolder { TextView subredditTextView; TextView userTextView; Post.Preview preview; PostBaseViewHolder(@NonNull View itemView) { super(itemView); } void setBaseView(AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable ImageView crosspostImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, MaterialButton commentsCountButton, MaterialButton saveButton, MaterialButton shareButton) { super.setBaseView( iconGifImageView, subredditTextView, userTextView, stickiedPostImageView, postTimeTextView, titleTextView, typeTextView, archivedImageView, lockedImageView, crosspostImageView, nsfwTextView, spoilerTextView, flairTextView, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); this.subredditTextView = subredditTextView; this.userTextView = userTextView; if (mVoteButtonsOnTheRight) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(bottomConstraintLayout); constraintSet.clear(upvoteButton.getId(), ConstraintSet.START); constraintSet.clear(scoreTextView.getId(), ConstraintSet.START); constraintSet.clear(downvoteButton.getId(), ConstraintSet.START); constraintSet.clear(saveButton.getId(), ConstraintSet.END); constraintSet.clear(shareButton.getId(), ConstraintSet.END); constraintSet.connect(upvoteButton.getId(), ConstraintSet.END, scoreTextView.getId(), ConstraintSet.START); constraintSet.connect(scoreTextView.getId(), ConstraintSet.END, downvoteButton.getId(), ConstraintSet.START); constraintSet.connect(downvoteButton.getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect(commentsCountButton.getId(), ConstraintSet.START, saveButton.getId(), ConstraintSet.END); constraintSet.connect(commentsCountButton.getId(), ConstraintSet.END, upvoteButton.getId(), ConstraintSet.START); constraintSet.connect(saveButton.getId(), ConstraintSet.START, shareButton.getId(), ConstraintSet.END); constraintSet.connect(shareButton.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.setHorizontalBias(commentsCountButton.getId(), 0); constraintSet.applyTo(bottomConstraintLayout); } setItemViewBackgroundColor(false); if (mActivity.typeface != null) { subredditTextView.setTypeface(mActivity.typeface); userTextView.setTypeface(mActivity.typeface); postTimeTextView.setTypeface(mActivity.typeface); if (typeTextView != null) { typeTextView.setTypeface(mActivity.typeface); } if (spoilerTextView != null) { spoilerTextView.setTypeface(mActivity.typeface); } if (nsfwTextView != null) { nsfwTextView.setTypeface(mActivity.typeface); } if (flairTextView != null) { flairTextView.setTypeface(mActivity.typeface); } upvoteButton.setTypeface(mActivity.typeface); commentsCountButton.setTypeface(mActivity.typeface); } if (mActivity.titleTypeface != null) { titleTextView.setTypeface(mActivity.titleTypeface); } subredditTextView.setTextColor(mSubredditColor); userTextView.setTextColor(mUsernameColor); postTimeTextView.setTextColor(mSecondaryTextColor); titleTextView.setTextColor(mPostTitleColor); stickiedPostImageView.setColorFilter(mStickiedPostIconTint, PorterDuff.Mode.SRC_IN); if (typeTextView != null) { typeTextView.setBackgroundColor(mPostTypeBackgroundColor); typeTextView.setBorderColor(mPostTypeBackgroundColor); typeTextView.setTextColor(mPostTypeTextColor); } if (spoilerTextView != null) { spoilerTextView.setBackgroundColor(mSpoilerBackgroundColor); spoilerTextView.setBorderColor(mSpoilerBackgroundColor); spoilerTextView.setTextColor(mSpoilerTextColor); } if (nsfwTextView != null) { nsfwTextView.setBackgroundColor(mNSFWBackgroundColor); nsfwTextView.setBorderColor(mNSFWBackgroundColor); nsfwTextView.setTextColor(mNSFWTextColor); } if (flairTextView != null) { flairTextView.setBackgroundColor(mFlairBackgroundColor); flairTextView.setBorderColor(mFlairBackgroundColor); flairTextView.setTextColor(mFlairTextColor); } if (archivedImageView != null) { archivedImageView.setColorFilter(mArchivedIconTint, PorterDuff.Mode.SRC_IN); } if (lockedImageView != null) { lockedImageView.setColorFilter(mLockedIconTint, PorterDuff.Mode.SRC_IN); } if (crosspostImageView != null) { crosspostImageView.setColorFilter(mCrosspostIconTint, PorterDuff.Mode.SRC_IN); } upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); commentsCountButton.setTextColor(mPostIconAndInfoColor); commentsCountButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); saveButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); shareButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); itemView.setOnLongClickListener(v -> { Post post = getItem(getBindingAdapterPosition()); if (post == null || mLongPressPostNonMediaAreaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_NONE)) { return false; } if (mLongPressPostNonMediaAreaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS)) { showPostOptions(); } else if (mLongPressPostNonMediaAreaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_PREVIEW_IN_FULLSCREEN)) { markPostRead(post, true); openMedia(post, true); } return true; }); } void showPostOptions() { PostOptionsBottomSheetFragment postOptionsBottomSheetFragment; if (post.getPostType() == Post.GALLERY_TYPE && this instanceof PostBaseGalleryTypeViewHolder) { postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(post, getBindingAdapterPosition(), ((LinearLayoutManagerBugFixed) ((PostBaseGalleryTypeViewHolder) this).galleryRecyclerView.getLayoutManager()).findFirstVisibleItemPosition()); } else { postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(post, getBindingAdapterPosition()); } postOptionsBottomSheetFragment.show(mFragment.getChildFragmentManager(), postOptionsBottomSheetFragment.getTag()); } @Override void markPostRead(Post post, boolean changePostItemColor) { if (!mHandleReadPost) { return; } if (!mAccountName.equals(Account.ANONYMOUS_ACCOUNT) && !post.isRead() && mMarkPostsAsRead) { post.markAsRead(); if (changePostItemColor) { setItemViewBackgroundColor(true); titleTextView.setTextColor(mReadPostTitleColor); if (this instanceof PostTextTypeViewHolder) { ((PostTextTypeViewHolder) this).contentTextView.setTextColor(mReadPostContentColor); } } if (mActivity != null && mActivity instanceof MarkPostAsReadInterface) { ((MarkPostAsReadInterface) mActivity).markPostAsRead(post); } } } } @UnstableApi abstract class PostBaseVideoAutoplayViewHolder extends PostBaseViewHolder implements ToroPlayer { VideoAutoplayImpl toroPlayer; @OptIn(markerClass = UnstableApi.class) PostBaseVideoAutoplayViewHolder(View rootView, AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView crosspostImageView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, AspectRatioFrameLayout aspectRatioFrameLayout, GifImageView previewImageView, ImageView errorLoadingRedgifsImageView, PlayerView videoPlayer, ImageView videoQualityButton, ImageView muteButton, ImageView fullscreenButton, ImageView playPauseButton, DefaultTimeBar progressBar, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, MaterialButton commentsCountButton, MaterialButton saveButton, MaterialButton shareButton) { super(rootView); setBaseView( iconGifImageView, subredditTextView, userTextView, stickiedPostImageView, postTimeTextView, titleTextView, typeTextView, archivedImageView, lockedImageView, crosspostImageView, nsfwTextView, spoilerTextView, flairTextView, bottomConstraintLayout, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); toroPlayer = new VideoAutoplayImpl(rootView, aspectRatioFrameLayout, previewImageView, errorLoadingRedgifsImageView, videoPlayer, videoQualityButton, muteButton, fullscreenButton, playPauseButton, progressBar, AppCompatResources.getDrawable(mActivity, R.drawable.ic_play_arrow_24dp), AppCompatResources.getDrawable(mActivity, R.drawable.ic_pause_24dp)) { @Override public int getPlayerOrder() { return getBindingAdapterPosition(); } @Override int getAdapterPosition() { return getBindingAdapterPosition(); } @Override Post getPost() { return post; } @Override void markPostRead(Post post, boolean changePostItemColor) { PostBaseVideoAutoplayViewHolder.this.markPostRead(post, changePostItemColor); } }; } @NonNull @Override public View getPlayerView() { return toroPlayer.getPlayerView(); } @NonNull @Override public PlaybackInfo getCurrentPlaybackInfo() { return toroPlayer.getCurrentPlaybackInfo(); } @Override public void initialize(@NonNull Container container, @NonNull PlaybackInfo playbackInfo) { toroPlayer.initialize(container, playbackInfo); } @Override public void play() { toroPlayer.play(); mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @Override public void pause() { toroPlayer.pause(); mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @Override public boolean isPlaying() { return toroPlayer.isPlaying(); } @Override public void release() { toroPlayer.release(); } @Override public boolean wantsToPlay() { return toroPlayer.wantsToPlay(); } @Override public int getPlayerOrder() { return toroPlayer.getPlayerOrder(); } } @UnstableApi class PostVideoAutoplayViewHolder extends PostBaseVideoAutoplayViewHolder { PostVideoAutoplayViewHolder(ItemPostVideoTypeAutoplayBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostVideoTypeAutoplay, binding.subredditNameTextViewItemPostVideoTypeAutoplay, binding.userTextViewItemPostVideoTypeAutoplay, binding.stickiedPostImageViewItemPostVideoTypeAutoplay, binding.postTimeTextViewItemPostVideoTypeAutoplay, binding.titleTextViewItemPostVideoTypeAutoplay, binding.typeTextViewItemPostVideoTypeAutoplay, binding.crosspostImageViewItemPostVideoTypeAutoplay, binding.archivedImageViewItemPostVideoTypeAutoplay, binding.lockedImageViewItemPostVideoTypeAutoplay, binding.nsfwTextViewItemPostVideoTypeAutoplay, binding.spoilerCustomTextViewItemPostVideoTypeAutoplay, binding.flairCustomTextViewItemPostVideoTypeAutoplay, binding.aspectRatioFrameLayoutItemPostVideoTypeAutoplay, binding.previewImageViewItemPostVideoTypeAutoplay, binding.errorLoadingVideoImageViewItemPostVideoTypeAutoplay, binding.playerViewItemPostVideoTypeAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.bottomConstraintLayoutItemPostVideoTypeAutoplay, binding.upvoteButtonItemPostVideoTypeAutoplay, binding.scoreTextViewItemPostVideoTypeAutoplay, binding.downvoteButtonItemPostVideoTypeAutoplay, binding.commentsCountButtonItemPostVideoTypeAutoplay, binding.saveButtonItemPostVideoTypeAutoplay, binding.shareButtonItemPostVideoTypeAutoplay); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor)); } } @UnstableApi class PostVideoAutoplayLegacyControllerViewHolder extends PostBaseVideoAutoplayViewHolder { PostVideoAutoplayLegacyControllerViewHolder(ItemPostVideoTypeAutoplayLegacyControllerBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostVideoTypeAutoplay, binding.subredditNameTextViewItemPostVideoTypeAutoplay, binding.userTextViewItemPostVideoTypeAutoplay, binding.stickiedPostImageViewItemPostVideoTypeAutoplay, binding.postTimeTextViewItemPostVideoTypeAutoplay, binding.titleTextViewItemPostVideoTypeAutoplay, binding.typeTextViewItemPostVideoTypeAutoplay, binding.crosspostImageViewItemPostVideoTypeAutoplay, binding.archivedImageViewItemPostVideoTypeAutoplay, binding.lockedImageViewItemPostVideoTypeAutoplay, binding.nsfwTextViewItemPostVideoTypeAutoplay, binding.spoilerCustomTextViewItemPostVideoTypeAutoplay, binding.flairCustomTextViewItemPostVideoTypeAutoplay, binding.aspectRatioFrameLayoutItemPostVideoTypeAutoplay, binding.previewImageViewItemPostVideoTypeAutoplay, binding.errorLoadingVideoImageViewItemPostVideoTypeAutoplay, binding.playerViewItemPostVideoTypeAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.bottomConstraintLayoutItemPostVideoTypeAutoplay, binding.upvoteButtonItemPostVideoTypeAutoplay, binding.scoreTextViewItemPostVideoTypeAutoplay, binding.downvoteButtonItemPostVideoTypeAutoplay, binding.commentsCountButtonItemPostVideoTypeAutoplay, binding.saveButtonItemPostVideoTypeAutoplay, binding.shareButtonItemPostVideoTypeAutoplay); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor)); } } class PostWithPreviewTypeViewHolder extends PostBaseViewHolder { TextView linkTextView; ImageView imageViewNoPreviewGallery; LoadingIndicator loadingIndicator; ImageView videoOrGifIndicator; TextView loadImageErrorTextView; @Nullable FrameLayout imageWrapperFrameLayout; AspectRatioGifImageView imageView; RequestListener glideRequestListener; PostWithPreviewTypeViewHolder(@NonNull View itemView) { super(itemView); } PostWithPreviewTypeViewHolder(@NonNull ItemPostWithPreviewBinding binding) { super(binding.getRoot()); setBaseView( binding.iconGifImageViewItemPostWithPreview, binding.subredditNameTextViewItemPostWithPreview, binding.userTextViewItemPostWithPreview, binding.stickiedPostImageViewItemPostWithPreview, binding.postTimeTextViewItemPostWithPreview, binding.titleTextViewItemPostWithPreview, binding.typeTextViewItemPostWithPreview, binding.archivedImageViewItemPostWithPreview, binding.lockedImageViewItemPostWithPreview, binding.crosspostImageViewItemPostWithPreview, binding.nsfwTextViewItemPostWithPreview, binding.spoilerCustomTextViewItemPostWithPreview, binding.flairCustomTextViewItemPostWithPreview, binding.bottomConstraintLayoutItemPostWithPreview, binding.upvoteButtonItemPostWithPreview, binding.scoreTextViewItemPostWithPreview, binding.downvoteButtonItemPostWithPreview, binding.commentsCountButtonItemPostWithPreview, binding.saveButtonItemPostWithPreview, binding.shareButtonItemPostWithPreview, binding.linkTextViewItemPostWithPreview, binding.imageViewNoPreviewGalleryItemPostWithPreview, binding.progressBarItemPostWithPreview, binding.videoOrGifIndicatorImageViewItemPostWithPreview, binding.loadImageErrorTextViewItemPostWithPreview, binding.imageWrapperRelativeLayoutItemPostWithPreview, binding.imageViewItemPostWithPreview); } void setBaseView(AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable ImageView crosspostImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, MaterialButton commentsCountButton, MaterialButton saveButton, MaterialButton shareButton, TextView linkTextView, ImageView imageViewNoPreviewGallery, LoadingIndicator loadingIndicator, ImageView videoOrGifIndicator, TextView loadImageErrorTextView, @Nullable FrameLayout imageWrapperFrameLayout, AspectRatioGifImageView imageView) { super.setBaseView( iconGifImageView, subredditTextView, userTextView, stickiedPostImageView, postTimeTextView, titleTextView, typeTextView, archivedImageView, lockedImageView, crosspostImageView, nsfwTextView, spoilerTextView, flairTextView, bottomConstraintLayout, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); this.linkTextView = linkTextView; this.imageViewNoPreviewGallery = imageViewNoPreviewGallery; this.loadingIndicator = loadingIndicator; this.videoOrGifIndicator = videoOrGifIndicator; this.loadImageErrorTextView = loadImageErrorTextView; this.imageWrapperFrameLayout = imageWrapperFrameLayout; this.imageView = imageView; if (mActivity.typeface != null) { linkTextView.setTypeface(mActivity.typeface); loadImageErrorTextView.setTypeface(mActivity.typeface); } linkTextView.setTextColor(mSecondaryTextColor); imageViewNoPreviewGallery.setBackgroundColor(mNoPreviewPostTypeBackgroundColor); imageViewNoPreviewGallery.setColorFilter(mNoPreviewPostTypeIconTint, android.graphics.PorterDuff.Mode.SRC_IN); loadingIndicator.setIndicatorColor(mColorAccent); videoOrGifIndicator.setColorFilter(mMediaIndicatorIconTint, PorterDuff.Mode.SRC_IN); videoOrGifIndicator.setBackgroundTintList(ColorStateList.valueOf(mMediaIndicatorBackgroundColor)); loadImageErrorTextView.setTextColor(mPrimaryTextColor); imageView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { markPostRead(post, true); openMedia(post); } }); imageView.setOnLongClickListener(view -> { if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS)) { showPostOptions(); return true; } else if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_PREVIEW_IN_FULLSCREEN)) { markPostRead(post, true); openMedia(post, true); return true; } return false; }); loadImageErrorTextView.setOnClickListener(view -> { loadingIndicator.setVisibility(View.VISIBLE); loadImageErrorTextView.setVisibility(View.GONE); loadImage(this); }); imageViewNoPreviewGallery.setOnClickListener(view -> { imageView.performClick(); }); imageViewNoPreviewGallery.setOnLongClickListener(view -> imageView.performLongClick()); glideRequestListener = new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { loadingIndicator.setVisibility(View.GONE); loadImageErrorTextView.setVisibility(View.VISIBLE); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { loadImageErrorTextView.setVisibility(View.GONE); loadingIndicator.setVisibility(View.GONE); return false; } }; } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor)); } } public abstract class PostBaseGalleryTypeViewHolder extends PostBaseViewHolder { FrameLayout frameLayout; RecyclerView galleryRecyclerView; CustomTextView imageIndexTextView; ImageView noPreviewImageView; PostGalleryTypeImageRecyclerViewAdapter adapter; private boolean swipeLocked; PostBaseGalleryTypeViewHolder(View rootView, AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable ImageView crosspostImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, FrameLayout frameLayout, RecyclerView galleryRecyclerView, CustomTextView imageIndexTextView, ImageView noPreviewImageView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, MaterialButton commentsCountButton, MaterialButton saveButton, MaterialButton shareButton) { super(rootView); setBaseView( iconGifImageView, subredditTextView, userTextView, stickiedPostImageView, postTimeTextView, titleTextView, typeTextView, archivedImageView, lockedImageView, crosspostImageView, nsfwTextView, spoilerTextView, flairTextView, bottomConstraintLayout, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); this.frameLayout = frameLayout; this.galleryRecyclerView = galleryRecyclerView; this.imageIndexTextView = imageIndexTextView; this.noPreviewImageView = noPreviewImageView; imageIndexTextView.setTextColor(mMediaIndicatorIconTint); imageIndexTextView.setBackgroundColor(mMediaIndicatorBackgroundColor); imageIndexTextView.setBorderColor(mMediaIndicatorBackgroundColor); if (mActivity.typeface != null) { imageIndexTextView.setTypeface(mActivity.typeface); } noPreviewImageView.setBackgroundColor(mNoPreviewPostTypeBackgroundColor); noPreviewImageView.setColorFilter(mNoPreviewPostTypeIconTint, android.graphics.PorterDuff.Mode.SRC_IN); adapter = new PostGalleryTypeImageRecyclerViewAdapter(mGlide, mActivity.typeface, mSaveMemoryCenterInsideDownsampleStrategy, mColorAccent, mPrimaryTextColor, mScale); galleryRecyclerView.setAdapter(adapter); new PagerSnapHelper().attachToRecyclerView(galleryRecyclerView); galleryRecyclerView.setRecycledViewPool(mGalleryRecycledViewPool); LinearLayoutManagerBugFixed layoutManager = new LinearLayoutManagerBugFixed(mActivity, RecyclerView.HORIZONTAL, false); galleryRecyclerView.setLayoutManager(layoutManager); galleryRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); imageIndexTextView.setText(mActivity.getString(R.string.image_index_in_gallery, layoutManager.findFirstVisibleItemPosition() + 1, post.getGallery().size())); } }); galleryRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { private float downX; private float downY; private boolean dragged; private long downTime; private final int minTouchSlop = ViewConfiguration.get(mActivity).getScaledTouchSlop(); private final int longClickThreshold = ViewConfiguration.getLongPressTimeout(); private boolean longPressed; @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { int action = e.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downX = e.getRawX(); downY = e.getRawY(); downTime = System.currentTimeMillis(); if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(true); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(false); } mActivity.lockSwipeRightToGoBack(); swipeLocked = true; break; case MotionEvent.ACTION_MOVE: if (Math.abs(e.getRawX() - downX) > minTouchSlop || Math.abs(e.getRawY() - downY) > minTouchSlop) { dragged = true; } if (!dragged && !longPressed) { if (System.currentTimeMillis() - downTime >= longClickThreshold) { if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS)) { galleryRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); showPostOptions(); longPressed = true; } else if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_PREVIEW_IN_FULLSCREEN)) { galleryRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); markPostRead(post, true); openMedia(post, layoutManager.findFirstVisibleItemPosition(), true); longPressed = true; } } } if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(true); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(false); } mActivity.lockSwipeRightToGoBack(); swipeLocked = true; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (e.getActionMasked() == MotionEvent.ACTION_UP && !dragged) { if (System.currentTimeMillis() - downTime < longClickThreshold) { int position = getBindingAdapterPosition(); if (position >= 0) { if (post != null) { markPostRead(post, true); openMedia(post, layoutManager.findFirstVisibleItemPosition(), false); } } } } downX = 0; downY = 0; dragged = false; longPressed = false; if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(false); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(!mDisableSwipingBetweenTabs); } mActivity.unlockSwipeRightToGoBack(); swipeLocked = false; } return false; } @Override public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } }); noPreviewImageView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } if (post != null) { markPostRead(post, true); openMedia(post, 0); } }); noPreviewImageView.setOnLongClickListener(view -> { if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS)) { showPostOptions(); return true; } else if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_PREVIEW_IN_FULLSCREEN)) { markPostRead(post, true); openMedia(post, layoutManager.findFirstVisibleItemPosition(), true); return true; } return false; }); } public boolean isSwipeLocked() { return swipeLocked; } } public class PostGalleryTypeViewHolder extends PostBaseGalleryTypeViewHolder { PostGalleryTypeViewHolder(ItemPostGalleryTypeBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostGalleryType, binding.subredditNameTextViewItemPostGalleryType, binding.userTextViewItemPostGalleryType, binding.stickiedPostImageViewItemPostGalleryType, binding.postTimeTextViewItemPostGalleryType, binding.titleTextViewItemPostGalleryType, binding.typeTextViewItemPostGalleryType, binding.archivedImageViewItemPostGalleryType, binding.lockedImageViewItemPostGalleryType, binding.crosspostImageViewItemPostGalleryType, binding.nsfwTextViewItemPostGalleryType, binding.spoilerTextViewItemPostGalleryType, binding.flairTextViewItemPostGalleryType, binding.galleryFrameLayoutItemPostGalleryType, binding.galleryRecyclerViewItemPostGalleryType, binding.imageIndexTextViewItemPostGalleryType, binding.noPreviewImageViewItemPostGalleryType, binding.bottomConstraintLayoutItemPostGalleryType, binding.upvoteButtonItemPostGalleryType, binding.scoreTextViewItemPostGalleryType, binding.downvoteButtonItemPostGalleryType, binding.commentsCountButtonItemPostGalleryType, binding.saveButtonItemPostGalleryType, binding.shareButtonItemPostGalleryType); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor)); } } class PostTextTypeViewHolder extends PostBaseViewHolder { TextView contentTextView; PostTextTypeViewHolder(@NonNull View itemView) { super(itemView); } PostTextTypeViewHolder(@NonNull ItemPostTextBinding binding) { super(binding.getRoot()); setBaseView( binding.iconGifImageViewItemPostTextType, binding.subredditNameTextViewItemPostTextType, binding.userTextViewItemPostTextType, binding.stickiedPostImageViewItemPostTextType, binding.postTimeTextViewItemPostTextType, binding.titleTextViewItemPostTextType, binding.typeTextViewItemPostTextType, binding.archivedImageViewItemPostTextType, binding.lockedImageViewItemPostTextType, binding.crosspostImageViewItemPostTextType, binding.nsfwTextViewItemPostTextType, binding.spoilerCustomTextViewItemPostTextType, binding.flairCustomTextViewItemPostTextType, binding.bottomConstraintLayoutItemPostTextType, binding.upvoteButtonItemPostTextType, binding.scoreTextViewItemPostTextType, binding.downvoteButtonItemPostTextType, binding.commentsCountButtonItemPostTextType, binding.saveButtonItemPostTextType, binding.shareButtonItemPostTextType, binding.contentTextViewItemPostTextType); } void setBaseView(AspectRatioGifImageView iconGifImageView, TextView subredditTextView, TextView userTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable ImageView crosspostImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, MaterialButton commentsCountButton, MaterialButton saveButton, MaterialButton shareButton, TextView contentTextView) { super.setBaseView( iconGifImageView, subredditTextView, userTextView, stickiedPostImageView, postTimeTextView, titleTextView, typeTextView, archivedImageView, lockedImageView, crosspostImageView, nsfwTextView, spoilerTextView, flairTextView, bottomConstraintLayout, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); this.contentTextView = contentTextView; if (mActivity.contentTypeface != null) { contentTextView.setTypeface(mActivity.titleTypeface); } contentTextView.setTextColor(mPostContentColor); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor)); } } public class PostCompactBaseViewHolder extends PostViewHolder { TextView nameTextView; @Nullable TextView linkTextView; RelativeLayout relativeLayout; LoadingIndicator loadingIndicator; ImageView imageView; ImageView playButtonImageView; FrameLayout noPreviewPostImageFrameLayout; ImageView noPreviewPostImageView; @Nullable ConstraintLayout bottomConstraintLayout; View divider; RequestListener requestListener; PostCompactBaseViewHolder(View itemView) { super(itemView); } void setBaseView(AspectRatioGifImageView iconGifImageView, TextView nameTextView, ImageView stickiedPostImageView, TextView postTimeTextView, TextView titleTextView, @Nullable CustomTextView typeTextView, @Nullable ImageView archivedImageView, @Nullable ImageView lockedImageView, @Nullable ImageView crosspostImageView, @Nullable CustomTextView nsfwTextView, @Nullable CustomTextView spoilerTextView, @Nullable CustomTextView flairTextView, @Nullable TextView linkTextView, RelativeLayout relativeLayout, LoadingIndicator loadingIndicator, ImageView imageView, ImageView playButtonImageView, FrameLayout noPreviewLinkImageFrameLayout, ImageView noPreviewLinkImageView, @Nullable ConstraintLayout bottomConstraintLayout, MaterialButton upvoteButton, TextView scoreTextView, MaterialButton downvoteButton, @Nullable MaterialButton commentsCountButton, @Nullable MaterialButton saveButton, @Nullable MaterialButton shareButton, View divider) { super.setBaseView(iconGifImageView, nameTextView, stickiedPostImageView, postTimeTextView, titleTextView, typeTextView, archivedImageView, lockedImageView, crosspostImageView, nsfwTextView, spoilerTextView, flairTextView, upvoteButton, scoreTextView, downvoteButton, commentsCountButton, saveButton, shareButton); this.nameTextView = nameTextView; this.linkTextView = linkTextView; this.relativeLayout = relativeLayout; this.loadingIndicator = loadingIndicator; this.imageView = imageView; this.playButtonImageView = playButtonImageView; this.noPreviewPostImageFrameLayout = noPreviewLinkImageFrameLayout; this.noPreviewPostImageView = noPreviewLinkImageView; this.bottomConstraintLayout = bottomConstraintLayout; this.divider = divider; if (mVoteButtonsOnTheRight) { if (bottomConstraintLayout != null) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(bottomConstraintLayout); constraintSet.clear(upvoteButton.getId(), ConstraintSet.START); constraintSet.clear(scoreTextView.getId(), ConstraintSet.START); constraintSet.clear(downvoteButton.getId(), ConstraintSet.START); constraintSet.clear(saveButton.getId(), ConstraintSet.END); constraintSet.clear(shareButton.getId(), ConstraintSet.END); constraintSet.connect(upvoteButton.getId(), ConstraintSet.END, scoreTextView.getId(), ConstraintSet.START); constraintSet.connect(scoreTextView.getId(), ConstraintSet.END, downvoteButton.getId(), ConstraintSet.START); constraintSet.connect(downvoteButton.getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); constraintSet.connect(commentsCountButton.getId(), ConstraintSet.START, saveButton.getId(), ConstraintSet.END); constraintSet.connect(commentsCountButton.getId(), ConstraintSet.END, upvoteButton.getId(), ConstraintSet.START); constraintSet.connect(saveButton.getId(), ConstraintSet.START, shareButton.getId(), ConstraintSet.END); constraintSet.connect(shareButton.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); constraintSet.setHorizontalBias(commentsCountButton.getId(), 0); constraintSet.applyTo(bottomConstraintLayout); } } if (((ViewGroup) itemView).getLayoutTransition() != null) { ((ViewGroup) itemView).getLayoutTransition().setAnimateParentHierarchy(false); } if (mActivity.typeface != null) { nameTextView.setTypeface(mActivity.typeface); postTimeTextView.setTypeface(mActivity.typeface); if (typeTextView != null) { typeTextView.setTypeface(mActivity.typeface); } if (spoilerTextView != null) { spoilerTextView.setTypeface(mActivity.typeface); } if (nsfwTextView != null) { nsfwTextView.setTypeface(mActivity.typeface); } if (flairTextView != null) { flairTextView.setTypeface(mActivity.typeface); } if (linkTextView != null) { linkTextView.setTypeface(mActivity.typeface); } upvoteButton.setTypeface(mActivity.typeface); if (commentsCountButton != null) { commentsCountButton.setTypeface(mActivity.typeface); } } if (mActivity.titleTypeface != null) { titleTextView.setTypeface(mActivity.titleTypeface); } itemView.setBackgroundColor(mCardViewBackgroundColor); postTimeTextView.setTextColor(mSecondaryTextColor); titleTextView.setTextColor(mPostTitleColor); stickiedPostImageView.setColorFilter(mStickiedPostIconTint, PorterDuff.Mode.SRC_IN); if (typeTextView != null) { typeTextView.setBackgroundColor(mPostTypeBackgroundColor); typeTextView.setBorderColor(mPostTypeBackgroundColor); typeTextView.setTextColor(mPostTypeTextColor); } if (spoilerTextView != null) { spoilerTextView.setBackgroundColor(mSpoilerBackgroundColor); spoilerTextView.setBorderColor(mSpoilerBackgroundColor); spoilerTextView.setTextColor(mSpoilerTextColor); } if (nsfwTextView != null) { nsfwTextView.setBackgroundColor(mNSFWBackgroundColor); nsfwTextView.setBorderColor(mNSFWBackgroundColor); nsfwTextView.setTextColor(mNSFWTextColor); } if (flairTextView != null) { flairTextView.setBackgroundColor(mFlairBackgroundColor); flairTextView.setBorderColor(mFlairBackgroundColor); flairTextView.setTextColor(mFlairTextColor); } if (archivedImageView != null) { archivedImageView.setColorFilter(mArchivedIconTint, PorterDuff.Mode.SRC_IN); } if (lockedImageView != null) { lockedImageView.setColorFilter(mLockedIconTint, PorterDuff.Mode.SRC_IN); } if (crosspostImageView != null) { crosspostImageView.setColorFilter(mCrosspostIconTint, PorterDuff.Mode.SRC_IN); } if (linkTextView != null) { linkTextView.setTextColor(mSecondaryTextColor); } playButtonImageView.setColorFilter(mMediaIndicatorIconTint, PorterDuff.Mode.SRC_IN); playButtonImageView.setBackgroundTintList(ColorStateList.valueOf(mMediaIndicatorBackgroundColor)); loadingIndicator.setIndicatorColor(mColorAccent); loadingIndicator.setVisibility(View.GONE); noPreviewLinkImageView.setBackgroundColor(mNoPreviewPostTypeBackgroundColor); noPreviewLinkImageView.setColorFilter(mNoPreviewPostTypeIconTint, android.graphics.PorterDuff.Mode.SRC_IN); upvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); scoreTextView.setTextColor(mPostIconAndInfoColor); downvoteButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); if (commentsCountButton != null) { commentsCountButton.setTextColor(mPostIconAndInfoColor); commentsCountButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); } if (saveButton != null) { saveButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); } if (shareButton != null) { shareButton.setIconTint(ColorStateList.valueOf(mPostIconAndInfoColor)); } divider.setBackgroundColor(mDividerColor); imageView.setClipToOutline(true); noPreviewLinkImageFrameLayout.setClipToOutline(true); itemView.setOnLongClickListener(view -> { if (bottomConstraintLayout != null && mLongPressToHideToolbarInCompactLayout) { if (bottomConstraintLayout.getLayoutParams().height == 0) { ViewGroup.LayoutParams params = bottomConstraintLayout.getLayoutParams(); params.height = LinearLayout.LayoutParams.WRAP_CONTENT; bottomConstraintLayout.setLayoutParams(params); mCallback.delayTransition(); } else { mCallback.delayTransition(); ViewGroup.LayoutParams params = bottomConstraintLayout.getLayoutParams(); params.height = 0; bottomConstraintLayout.setLayoutParams(params); } return true; } else if (mLongPressPostNonMediaAreaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS)) { showPostOptions(); return true; } else if (mLongPressPostNonMediaAreaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_PREVIEW_IN_FULLSCREEN)) { markPostRead(post, true); openMedia(post, true); return true; } return false; }); imageView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position < 0) { return; } Post post = getItem(position); if (post != null) { markPostRead(post, true); openMedia(post); } }); imageView.setOnLongClickListener(v -> { if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS)) { showPostOptions(); return true; } else if (mLongPressPostMediaAction.equals(SharedPreferencesUtils.LONG_PRESS_POST_VALUE_PREVIEW_IN_FULLSCREEN)) { markPostRead(post, true); openMedia(post, true); return true; } return false; }); noPreviewLinkImageFrameLayout.setOnClickListener(view -> { imageView.performClick(); }); noPreviewLinkImageFrameLayout.setOnLongClickListener(view -> imageView.performLongClick()); requestListener = new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { loadingIndicator.setVisibility(View.GONE); return false; } @Override public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { loadingIndicator.setVisibility(View.GONE); return false; } }; } void showPostOptions() { Post post = getItem(getBindingAdapterPosition()); if (post == null) { return; } PostOptionsBottomSheetFragment postOptionsBottomSheetFragment; postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(post, getBindingAdapterPosition()); postOptionsBottomSheetFragment.show(mFragment.getChildFragmentManager(), postOptionsBottomSheetFragment.getTag()); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundColor(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor); } @Override void markPostRead(Post post, boolean changePostItemColor) { if (!mHandleReadPost) { return; } if (!mAccountName.equals(Account.ANONYMOUS_ACCOUNT) && !post.isRead() && mMarkPostsAsRead) { post.markAsRead(); if (changePostItemColor) { itemView.setBackgroundColor(mReadPostCardViewBackgroundColor); titleTextView.setTextColor(mReadPostTitleColor); } if (mActivity != null && mActivity instanceof MarkPostAsReadInterface) { ((MarkPostAsReadInterface) mActivity).markPostAsRead(post); } } } } class PostCompactLeftThumbnailViewHolder extends PostCompactBaseViewHolder { PostCompactLeftThumbnailViewHolder(@NonNull ItemPostCompactBinding binding) { super(binding.getRoot()); setBaseView(binding.iconGifImageViewItemPostCompact, binding.nameTextViewItemPostCompact, binding.stickiedPostImageViewItemPostCompact, binding.postTimeTextViewItemPostCompact, binding.titleTextViewItemPostCompact, binding.typeTextViewItemPostCompact, binding.archivedImageViewItemPostCompact, binding.lockedImageViewItemPostCompact, binding.crosspostImageViewItemPostCompact, binding.nsfwTextViewItemPostCompact, binding.spoilerCustomTextViewItemPostCompact, binding.flairCustomTextViewItemPostCompact, binding.linkTextViewItemPostCompact, binding.imageViewWrapperItemPostCompact, binding.progressBarItemPostCompact, binding.imageViewItemPostCompact, binding.playButtonImageViewItemPostCompact, binding.frameLayoutImageViewNoPreviewLinkItemPostCompact, binding.imageViewNoPreviewLinkItemPostCompact, binding.bottomConstraintLayoutItemPostCompact, binding.upvoteButtonItemPostCompact, binding.scoreTextViewItemPostCompact, binding.downvoteButtonItemPostCompact, binding.commentsCountButtonItemPostCompact, binding.saveButtonItemPostCompact, binding.shareButtonItemPostCompact, binding.dividerItemPostCompact); } } class PostCompactRightThumbnailViewHolder extends PostCompactBaseViewHolder { PostCompactRightThumbnailViewHolder(@NonNull ItemPostCompactRightThumbnailBinding binding) { super(binding.getRoot()); setBaseView(binding.iconGifImageViewItemPostCompactRightThumbnail, binding.nameTextViewItemPostCompactRightThumbnail, binding.stickiedPostImageViewItemPostCompactRightThumbnail, binding.postTimeTextViewItemPostCompactRightThumbnail, binding.titleTextViewItemPostCompactRightThumbnail, binding.typeTextViewItemPostCompactRightThumbnail, binding.archivedImageViewItemPostCompactRightThumbnail, binding.lockedImageViewItemPostCompactRightThumbnail, binding.crosspostImageViewItemPostCompactRightThumbnail, binding.nsfwTextViewItemPostCompactRightThumbnail, binding.spoilerCustomTextViewItemPostCompactRightThumbnail, binding.flairCustomTextViewItemPostCompactRightThumbnail, binding.linkTextViewItemPostCompactRightThumbnail, binding.imageViewWrapperItemPostCompactRightThumbnail, binding.progressBarItemPostCompactRightThumbnail, binding.imageViewItemPostCompactRightThumbnail, binding.playButtonImageViewItemPostCompactRightThumbnail, binding.frameLayoutImageViewNoPreviewLinkItemPostCompactRightThumbnail, binding.imageViewNoPreviewLinkItemPostCompactRightThumbnail, binding.bottomConstraintLayoutItemPostCompactRightThumbnail, binding.upvoteButtonItemPostCompactRightThumbnail, binding.scoreTextViewItemPostCompactRightThumbnail, binding.downvoteButtonItemPostCompactRightThumbnail, binding.commentsCountButtonItemPostCompactRightThumbnail, binding.saveButtonItemPostCompactRightThumbnail, binding.shareButtonItemPostCompactRightThumbnail, binding.dividerItemPostCompactRightThumbnail); } } class PostCompact2LeftThumbnailViewHolder extends PostCompactBaseViewHolder { PostCompact2LeftThumbnailViewHolder(@NonNull ItemPostCompact2Binding binding) { super(binding.getRoot()); setBaseView(binding.iconGifImageViewItemPostCompact2, binding.nameTextViewItemPostCompact2, binding.stickiedPostImageViewItemPostCompact2, binding.postTimeTextViewItemPostCompact2, binding.titleTextViewItemPostCompact2, null, null, null, null, null, null, null, null, binding.imageViewWrapperItemPostCompact2, binding.progressBarItemPostCompact2, binding.imageViewItemPostCompact2, binding.playButtonImageViewItemPostCompact2, binding.frameLayoutImageViewNoPreviewLinkItemPostCompact2, binding.imageViewNoPreviewLinkItemPostCompact2, null, binding.upvoteButtonItemPostCompact2, binding.scoreTextViewItemPostCompact2, binding.downvoteButtonItemPostCompact2, null, null, null, binding.dividerItemPostCompact2); } } class PostCompact2RightThumbnailViewHolder extends PostCompactBaseViewHolder { PostCompact2RightThumbnailViewHolder(@NonNull ItemPostCompact2RightThumbnailBinding binding) { super(binding.getRoot()); setBaseView(binding.iconGifImageViewItemPostCompact2RightThumbnail, binding.nameTextViewItemPostCompact2RightThumbnail, binding.stickiedPostImageViewItemPostCompact2RightThumbnail, binding.postTimeTextViewItemPostCompact2RightThumbnail, binding.titleTextViewItemPostCompact2RightThumbnail, null, null, null, null, null, null, null, null, binding.imageViewWrapperItemPostCompact2RightThumbnail, binding.progressBarItemPostCompact2RightThumbnail, binding.imageViewItemPostCompact2RightThumbnail, binding.playButtonImageViewItemPostCompact2RightThumbnail, binding.frameLayoutImageViewNoPreviewLinkItemPostCompact2RightThumbnail, binding.imageViewNoPreviewLinkItemPostCompact2RightThumbnail, null, binding.upvoteButtonItemPostCompact2RightThumbnail, binding.scoreTextViewItemPostCompact2RightThumbnail, binding.downvoteButtonItemPostCompact2RightThumbnail, null, null, null, binding.dividerItemPostCompact2RightThumbnail); } } class PostGalleryViewHolder extends RecyclerView.ViewHolder { ItemPostGalleryBinding binding; RequestListener requestListener; Post post; Post.Preview preview; int currentPosition; public PostGalleryViewHolder(@NonNull ItemPostGalleryBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.loadImageErrorTextViewItemGallery.setTypeface(mActivity.typeface); } if (mActivity.titleTypeface != null) { binding.titleTextViewItemPostGallery.setTypeface(mActivity.titleTypeface); } itemView.setBackgroundTintList(ColorStateList.valueOf(mCardViewBackgroundColor)); binding.titleTextViewItemPostGallery.setTextColor(mPostTitleColor); binding.progressBarItemPostGallery.setIndicatorColor(mColorAccent); binding.progressBarItemPostGallery.setVisibility(View.GONE); binding.imageViewNoPreviewItemPostGallery.setBackgroundColor(mNoPreviewPostTypeBackgroundColor); binding.imageViewNoPreviewItemPostGallery.setColorFilter(mNoPreviewPostTypeIconTint, android.graphics.PorterDuff.Mode.SRC_IN); binding.videoOrGifIndicatorImageViewItemPostGallery.setColorFilter(mMediaIndicatorIconTint, PorterDuff.Mode.SRC_IN); binding.videoOrGifIndicatorImageViewItemPostGallery.setBackgroundTintList(ColorStateList.valueOf(mMediaIndicatorBackgroundColor)); binding.loadImageErrorTextViewItemGallery.setTextColor(mPrimaryTextColor); itemView.setOnClickListener(view -> { int position = getBindingAdapterPosition(); if (position >= 0 && canStartActivity) { Post post = getItem(position); if (post != null) { markPostRead(post, true); if (post.getPostType() == Post.TEXT_TYPE || !mSharedPreferences.getBoolean(SharedPreferencesUtils.CLICK_TO_SHOW_MEDIA_IN_GALLERY_LAYOUT, false)) { openViewPostDetailActivity(post, getBindingAdapterPosition()); } else { openMedia(post); } } } }); itemView.setOnLongClickListener(view -> { int position = getBindingAdapterPosition(); if (position >= 0 && canStartActivity) { Post post = getItem(position); if (post != null) { markPostRead(post, true); if (post.getPostType() == Post.TEXT_TYPE || mSharedPreferences.getBoolean(SharedPreferencesUtils.CLICK_TO_SHOW_MEDIA_IN_GALLERY_LAYOUT, false)) { openViewPostDetailActivity(post, getBindingAdapterPosition()); } else { openMedia(post); } } } return true; }); binding.loadImageErrorTextViewItemGallery.setOnClickListener(view -> { binding.progressBarItemPostGallery.setVisibility(View.VISIBLE); binding.loadImageErrorTextViewItemGallery.setVisibility(View.GONE); loadImage(this); }); binding.imageViewNoPreviewItemPostGallery.setOnClickListener(view -> { itemView.performClick(); }); requestListener = new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { binding.progressBarItemPostGallery.setVisibility(View.GONE); binding.loadImageErrorTextViewItemGallery.setVisibility(View.VISIBLE); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { binding.loadImageErrorTextViewItemGallery.setVisibility(View.GONE); binding.progressBarItemPostGallery.setVisibility(View.GONE); return false; } }; } void markPostRead(Post post, boolean changePostItemColor) { if (!mHandleReadPost) { return; } if (!mAccountName.equals(Account.ANONYMOUS_ACCOUNT) && !post.isRead() && mMarkPostsAsRead) { post.markAsRead(); if (changePostItemColor) { itemView.setBackgroundTintList(ColorStateList.valueOf(mReadPostCardViewBackgroundColor)); binding.titleTextViewItemPostGallery.setTextColor(mReadPostTitleColor); } if (mActivity != null && mActivity instanceof MarkPostAsReadInterface) { ((MarkPostAsReadInterface) mActivity).markPostAsRead(post); } } } } class PostGalleryBaseGalleryTypeViewHolder extends RecyclerView.ViewHolder { FrameLayout frameLayout; RecyclerView recyclerView; CustomTextView imageIndexTextView; ImageView noPreviewImageView; PostGalleryTypeImageRecyclerViewAdapter adapter; private final LinearLayoutManagerBugFixed layoutManager; Post post; Post.Preview preview; int currentPosition; public PostGalleryBaseGalleryTypeViewHolder(@NonNull View itemView, FrameLayout frameLayout, RecyclerView recyclerView, CustomTextView imageIndexTextView, ImageView noPreviewImageView) { super(itemView); this.frameLayout = frameLayout; this.recyclerView = recyclerView; this.imageIndexTextView = imageIndexTextView; this.noPreviewImageView = noPreviewImageView; if (mActivity.typeface != null) { imageIndexTextView.setTypeface(mActivity.typeface); } itemView.setBackgroundTintList(ColorStateList.valueOf(mCardViewBackgroundColor)); noPreviewImageView.setBackgroundColor(mNoPreviewPostTypeBackgroundColor); noPreviewImageView.setColorFilter(mNoPreviewPostTypeIconTint, android.graphics.PorterDuff.Mode.SRC_IN); imageIndexTextView.setTextColor(mMediaIndicatorIconTint); imageIndexTextView.setBackgroundColor(mMediaIndicatorBackgroundColor); imageIndexTextView.setBorderColor(mMediaIndicatorBackgroundColor); adapter = new PostGalleryTypeImageRecyclerViewAdapter(mGlide, mActivity.typeface, mSaveMemoryCenterInsideDownsampleStrategy, mColorAccent, mPrimaryTextColor, mScale); recyclerView.setAdapter(adapter); new PagerSnapHelper().attachToRecyclerView(recyclerView); recyclerView.setRecycledViewPool(mGalleryRecycledViewPool); layoutManager = new LinearLayoutManagerBugFixed(mActivity, RecyclerView.HORIZONTAL, false); recyclerView.setLayoutManager(layoutManager); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); imageIndexTextView.setText(mActivity.getString(R.string.image_index_in_gallery, layoutManager.findFirstVisibleItemPosition() + 1, post.getGallery().size())); } }); recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { private float downX; private float downY; private boolean dragged; private long downTime; private final int minTouchSlop = ViewConfiguration.get(mActivity).getScaledTouchSlop(); private final int longClickThreshold = ViewConfiguration.getLongPressTimeout(); private boolean longPressed; @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { int action = e.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downX = e.getRawX(); downY = e.getRawY(); downTime = System.currentTimeMillis(); if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(true); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(false); } mActivity.lockSwipeRightToGoBack(); break; case MotionEvent.ACTION_MOVE: if (Math.abs(e.getRawX() - downX) > minTouchSlop || Math.abs(e.getRawY() - downY) > minTouchSlop) { dragged = true; } if (!dragged && !longPressed) { if (System.currentTimeMillis() - downTime >= longClickThreshold) { onLongClick(); longPressed = true; } } if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(true); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(false); } mActivity.lockSwipeRightToGoBack(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (e.getActionMasked() == MotionEvent.ACTION_UP && !dragged) { if (System.currentTimeMillis() - downTime < longClickThreshold) { onClick(); } } downX = 0; downY = 0; dragged = false; longPressed = false; if (mActivity.mSliderPanel != null) { mActivity.mSliderPanel.requestDisallowInterceptTouchEvent(false); } if (mActivity.mViewPager2 != null) { mActivity.mViewPager2.setUserInputEnabled(!mDisableSwipingBetweenTabs); } mActivity.unlockSwipeRightToGoBack(); } return false; } @Override public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } }); noPreviewImageView.setOnClickListener(view -> { onClick(); }); noPreviewImageView.setOnLongClickListener(view -> onLongClick()); } void onClick() { int position = getBindingAdapterPosition(); if (position >= 0 && canStartActivity) { Post post = getItem(position); if (post != null) { markPostRead(post, true); if (post.getPostType() == Post.TEXT_TYPE || !mSharedPreferences.getBoolean(SharedPreferencesUtils.CLICK_TO_SHOW_MEDIA_IN_GALLERY_LAYOUT, false)) { openViewPostDetailActivity(post, getBindingAdapterPosition()); } else { openMedia(post, layoutManager.findFirstVisibleItemPosition()); } } } } boolean onLongClick() { int position = getBindingAdapterPosition(); if (position >= 0 && canStartActivity) { Post post = getItem(position); if (post != null) { markPostRead(post, true); if (post.getPostType() == Post.TEXT_TYPE || mSharedPreferences.getBoolean(SharedPreferencesUtils.CLICK_TO_SHOW_MEDIA_IN_GALLERY_LAYOUT, false)) { openViewPostDetailActivity(post, getBindingAdapterPosition()); } else { openMedia(post, layoutManager.findFirstVisibleItemPosition()); } } } return true; } void markPostRead(Post post, boolean changePostItemColor) { if (!mHandleReadPost) { return; } if (!mAccountName.equals(Account.ANONYMOUS_ACCOUNT) && !post.isRead() && mMarkPostsAsRead) { post.markAsRead(); if (changePostItemColor) { itemView.setBackgroundTintList(ColorStateList.valueOf(mReadPostCardViewBackgroundColor)); } if (mActivity != null && mActivity instanceof MarkPostAsReadInterface) { ((MarkPostAsReadInterface) mActivity).markPostAsRead(post); } } } } class PostGalleryGalleryTypeViewHolder extends PostGalleryBaseGalleryTypeViewHolder { public PostGalleryGalleryTypeViewHolder(@NonNull ItemPostGalleryGalleryTypeBinding binding) { super(binding.getRoot(), binding.galleryFrameLayoutItemPostGalleryGalleryType, binding.galleryRecyclerViewItemPostGalleryGalleryType, binding.imageIndexTextViewItemPostGalleryGalleryType, binding.imageViewNoPreviewItemPostGalleryGalleryType); } } @UnstableApi class PostCard2VideoAutoplayViewHolder extends PostBaseVideoAutoplayViewHolder { PostCard2VideoAutoplayViewHolder(ItemPostCard2VideoAutoplayBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostCard2VideoAutoplay, binding.subredditNameTextViewItemPostCard2VideoAutoplay, binding.userTextViewItemPostCard2VideoAutoplay, binding.stickiedPostImageViewItemPostCard2VideoAutoplay, binding.postTimeTextViewItemPostCard2VideoAutoplay, binding.titleTextViewItemPostCard2VideoAutoplay, binding.typeTextViewItemPostCard2VideoAutoplay, binding.crosspostImageViewItemPostCard2VideoAutoplay, binding.archivedImageViewItemPostCard2VideoAutoplay, binding.lockedImageViewItemPostCard2VideoAutoplay, binding.nsfwTextViewItemPostCard2VideoAutoplay, binding.spoilerCustomTextViewItemPostCard2VideoAutoplay, binding.flairCustomTextViewItemPostCard2VideoAutoplay, binding.aspectRatioFrameLayoutItemPostCard2VideoAutoplay, binding.previewImageViewItemPostCard2VideoAutoplay, binding.errorLoadingVideoImageViewItemPostCard2VideoAutoplay, binding.playerViewItemPostCard2VideoAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.bottomConstraintLayoutItemPostCard2VideoAutoplay, binding.upvoteButtonItemPostCard2VideoAutoplay, binding.scoreTextViewItemPostCard2VideoAutoplay, binding.downvoteButtonItemPostCard2VideoAutoplay, binding.commentsCountButtonItemPostCard2VideoAutoplay, binding.saveButtonItemPostCard2VideoAutoplay, binding.shareButtonItemPostCard2VideoAutoplay); binding.dividerItemPostCard2VideoAutoplay.setBackgroundColor(mDividerColor); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundColor(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor); } } @UnstableApi class PostCard2VideoAutoplayLegacyControllerViewHolder extends PostBaseVideoAutoplayViewHolder { PostCard2VideoAutoplayLegacyControllerViewHolder(ItemPostCard2VideoAutoplayLegacyControllerBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostCard2VideoAutoplay, binding.subredditNameTextViewItemPostCard2VideoAutoplay, binding.userTextViewItemPostCard2VideoAutoplay, binding.stickiedPostImageViewItemPostCard2VideoAutoplay, binding.postTimeTextViewItemPostCard2VideoAutoplay, binding.titleTextViewItemPostCard2VideoAutoplay, binding.typeTextViewItemPostCard2VideoAutoplay, binding.crosspostImageViewItemPostCard2VideoAutoplay, binding.archivedImageViewItemPostCard2VideoAutoplay, binding.lockedImageViewItemPostCard2VideoAutoplay, binding.nsfwTextViewItemPostCard2VideoAutoplay, binding.spoilerCustomTextViewItemPostCard2VideoAutoplay, binding.flairCustomTextViewItemPostCard2VideoAutoplay, binding.aspectRatioFrameLayoutItemPostCard2VideoAutoplay, binding.previewImageViewItemPostCard2VideoAutoplay, binding.errorLoadingVideoImageViewItemPostCard2VideoAutoplay, binding.playerViewItemPostCard2VideoAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.bottomConstraintLayoutItemPostCard2VideoAutoplay, binding.upvoteButtonItemPostCard2VideoAutoplay, binding.scoreTextViewItemPostCard2VideoAutoplay, binding.downvoteButtonItemPostCard2VideoAutoplay, binding.commentsCountButtonItemPostCard2VideoAutoplay, binding.saveButtonItemPostCard2VideoAutoplay, binding.shareButtonItemPostCard2VideoAutoplay); binding.dividerItemPostCard2VideoAutoplay.setBackgroundColor(mDividerColor); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundColor(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor); } } class PostCard2WithPreviewViewHolder extends PostWithPreviewTypeViewHolder { PostCard2WithPreviewViewHolder(@NonNull ItemPostCard2WithPreviewBinding binding) { super(binding.getRoot()); setBaseView( binding.iconGifImageViewItemPostCard2WithPreview, binding.subredditNameTextViewItemPostCard2WithPreview, binding.userTextViewItemPostCard2WithPreview, binding.stickiedPostImageViewItemPostCard2WithPreview, binding.postTimeTextViewItemPostCard2WithPreview, binding.titleTextViewItemPostCard2WithPreview, binding.typeTextViewItemPostCard2WithPreview, binding.archivedImageViewItemPostCard2WithPreview, binding.lockedImageViewItemPostCard2WithPreview, binding.crosspostImageViewItemPostCard2WithPreview, binding.nsfwTextViewItemPostCard2WithPreview, binding.spoilerCustomTextViewItemPostCard2WithPreview, binding.flairCustomTextViewItemPostCard2WithPreview, binding.bottomConstraintLayoutItemPostCard2WithPreview, binding.upvoteButtonItemPostCard2WithPreview, binding.scoreTextViewItemPostCard2WithPreview, binding.downvoteButtonItemPostCard2WithPreview, binding.commentsCountButtonItemPostCard2WithPreview, binding.saveButtonItemPostCard2WithPreview, binding.shareButtonItemPostCard2WithPreview, binding.linkTextViewItemPostCard2WithPreview, binding.imageViewNoPreviewGalleryItemPostCard2WithPreview, binding.progressBarItemPostCard2WithPreview, binding.videoOrGifIndicatorImageViewItemPostCard2WithPreview, binding.loadImageErrorTextViewItemPostCard2WithPreview, null, binding.imageViewItemPostCard2WithPreview); binding.dividerItemPostCard2WithPreview.setBackgroundColor(mDividerColor); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundColor(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor); } } public class PostCard2GalleryTypeViewHolder extends PostBaseGalleryTypeViewHolder { PostCard2GalleryTypeViewHolder(ItemPostCard2GalleryTypeBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostCard2GalleryType, binding.subredditNameTextViewItemPostCard2GalleryType, binding.userTextViewItemPostCard2GalleryType, binding.stickiedPostImageViewItemPostCard2GalleryType, binding.postTimeTextViewItemPostCard2GalleryType, binding.titleTextViewItemPostCard2GalleryType, binding.typeTextViewItemPostCard2GalleryType, binding.archivedImageViewItemPostCard2GalleryType, binding.lockedImageViewItemPostCard2GalleryType, binding.crosspostImageViewItemPostCard2GalleryType, binding.nsfwTextViewItemPostCard2GalleryType, binding.spoilerCustomTextViewItemPostCard2GalleryType, binding.flairCustomTextViewItemPostCard2GalleryType, binding.galleryFrameLayoutItemPostCard2GalleryType, binding.galleryRecyclerViewItemPostCard2GalleryType, binding.imageIndexTextViewItemPostCard2GalleryType, binding.noPreviewImageViewItemPostCard2GalleryType, binding.bottomConstraintLayoutItemPostCard2GalleryType, binding.upvoteButtonItemPostCard2GalleryType, binding.scoreTextViewItemPostCard2GalleryType, binding.downvoteButtonItemPostCard2GalleryType, binding.commentsCountButtonItemPostCard2GalleryType, binding.saveButtonItemPostCard2GalleryType, binding.shareButtonItemPostCard2GalleryType); binding.dividerItemPostCard2GalleryType.setBackgroundColor(mDividerColor); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundColor(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor); } } class PostCard2TextTypeViewHolder extends PostTextTypeViewHolder { PostCard2TextTypeViewHolder(@NonNull ItemPostCard2TextBinding binding) { super(binding.getRoot()); setBaseView( binding.iconGifImageViewItemPostCard2Text, binding.subredditNameTextViewItemPostCard2Text, binding.userTextViewItemPostCard2Text, binding.stickiedPostImageViewItemPostCard2Text, binding.postTimeTextViewItemPostCard2Text, binding.titleTextViewItemPostCard2Text, binding.typeTextViewItemPostCard2Text, binding.archivedImageViewItemPostCard2Text, binding.lockedImageViewItemPostCard2Text, binding.crosspostImageViewItemPostCard2Text, binding.nsfwTextViewItemPostCard2Text, binding.spoilerCustomTextViewItemPostCard2Text, binding.flairCustomTextViewItemPostCard2Text, binding.bottomConstraintLayoutItemPostCard2Text, binding.upvoteButtonItemPostCard2Text, binding.scoreTextViewItemPostCard2Text, binding.downvoteButtonItemPostCard2Text, binding.commentsCountButtonItemPostCard2Text, binding.saveButtonItemPostCard2Text, binding.shareButtonItemPostCard2Text, binding.contentTextViewItemPostCard2Text); binding.dividerItemPostCard2Text.setBackgroundColor(mDividerColor); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundColor(isReadPost ? mReadPostCardViewBackgroundColor : mCardViewBackgroundColor); } } @UnstableApi public class PostMaterial3CardVideoAutoplayViewHolder extends PostBaseVideoAutoplayViewHolder { PostMaterial3CardVideoAutoplayViewHolder(ItemPostCard3VideoTypeAutoplayBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostCard3VideoTypeAutoplay, binding.subredditNameTextViewItemPostCard3VideoTypeAutoplay, binding.userTextViewItemPostCard3VideoTypeAutoplay, binding.stickiedPostImageViewItemPostCard3VideoTypeAutoplay, binding.postTimeTextViewItemPostCard3VideoTypeAutoplay, binding.titleTextViewItemPostCard3VideoTypeAutoplay, null, null, null, null, null, null, null, binding.aspectRatioFrameLayoutItemPostCard3VideoTypeAutoplay, binding.previewImageViewItemPostCard3VideoTypeAutoplay, binding.errorLoadingVideoImageViewItemPostCard3VideoTypeAutoplay, binding.playerViewItemPostCard3VideoTypeAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.bottomConstraintLayoutItemPostCard3VideoTypeAutoplay, binding.upvoteButtonItemPostCard3VideoTypeAutoplay, binding.scoreTextViewItemPostCard3VideoTypeAutoplay, binding.downvoteButtonItemPostCard3VideoTypeAutoplay, binding.commentsCountButtonItemPostCard3VideoTypeAutoplay, binding.saveButtonItemPostCard3VideoTypeAutoplay, binding.shareButtonItemPostCard3VideoTypeAutoplay); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostFilledCardViewBackgroundColor : mFilledCardViewBackgroundColor)); } } @UnstableApi public class PostMaterial3CardVideoAutoplayLegacyControllerViewHolder extends PostBaseVideoAutoplayViewHolder { PostMaterial3CardVideoAutoplayLegacyControllerViewHolder(ItemPostCard3VideoTypeAutoplayLegacyControllerBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostCard3VideoTypeAutoplay, binding.subredditNameTextViewItemPostCard3VideoTypeAutoplay, binding.userTextViewItemPostCard3VideoTypeAutoplay, binding.stickiedPostImageViewItemPostCard3VideoTypeAutoplay, binding.postTimeTextViewItemPostCard3VideoTypeAutoplay, binding.titleTextViewItemPostCard3VideoTypeAutoplay, null, null, null, null, null, null, null, binding.aspectRatioFrameLayoutItemPostCard3VideoTypeAutoplay, binding.previewImageViewItemPostCard3VideoTypeAutoplay, binding.errorLoadingVideoImageViewItemPostCard3VideoTypeAutoplay, binding.playerViewItemPostCard3VideoTypeAutoplay, binding.getRoot().findViewById(R.id.video_quality_exo_playback_control_view), binding.getRoot().findViewById(R.id.mute_exo_playback_control_view), binding.getRoot().findViewById(R.id.fullscreen_exo_playback_control_view), binding.getRoot().findViewById(R.id.exo_play), binding.getRoot().findViewById(R.id.exo_progress), binding.bottomConstraintLayoutItemPostCard3VideoTypeAutoplay, binding.upvoteButtonItemPostCard3VideoTypeAutoplay, binding.scoreTextViewItemPostCard3VideoTypeAutoplay, binding.downvoteButtonItemPostCard3VideoTypeAutoplay, binding.commentsCountButtonItemPostCard3VideoTypeAutoplay, binding.saveButtonItemPostCard3VideoTypeAutoplay, binding.shareButtonItemPostCard3VideoTypeAutoplay); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostFilledCardViewBackgroundColor : mFilledCardViewBackgroundColor)); } } public class PostMaterial3CardWithPreviewViewHolder extends PostWithPreviewTypeViewHolder { PostMaterial3CardWithPreviewViewHolder(@NonNull ItemPostCard3WithPreviewBinding binding) { super(binding.getRoot()); setBaseView(binding.iconGifImageViewItemPostCard3WithPreview, binding.subredditNameTextViewItemPostCard3WithPreview, binding.userTextViewItemPostCard3WithPreview, binding.stickiedPostImageViewItemPostCard3WithPreview, binding.postTimeTextViewItemPostCard3WithPreview, binding.titleTextViewItemPostCard3WithPreview, null, null, null, null, null, null, null, binding.bottomConstraintLayoutItemPostCard3WithPreview, binding.upvoteButtonItemPostCard3WithPreview, binding.scoreTextViewItemPostCard3WithPreview, binding.downvoteButtonItemPostCard3WithPreview, binding.commentsCountButtonItemPostCard3WithPreview, binding.saveButtonItemPostCard3WithPreview, binding.shareButtonItemPostCard3WithPreview, binding.linkTextViewItemPostCard3WithPreview, binding.imageViewNoPreviewGalleryItemPostCard3WithPreview, binding.progressBarItemPostCard3WithPreview, binding.videoOrGifIndicatorImageViewItemPostCard3WithPreview, binding.loadImageErrorTextViewItemPostCard3WithPreview, binding.imageWrapperRelativeLayoutItemPostCard3WithPreview, binding.imageViewItemPostCard3WithPreview); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostFilledCardViewBackgroundColor : mFilledCardViewBackgroundColor)); } } public class PostMaterial3CardGalleryTypeViewHolder extends PostBaseGalleryTypeViewHolder { PostMaterial3CardGalleryTypeViewHolder(ItemPostCard3GalleryTypeBinding binding) { super(binding.getRoot(), binding.iconGifImageViewItemPostCard3GalleryType, binding.subredditNameTextViewItemPostCard3GalleryType, binding.userTextViewItemPostCard3GalleryType, binding.stickiedPostImageViewItemPostCard3GalleryType, binding.postTimeTextViewItemPostCard3GalleryType, binding.titleTextViewItemPostCard3GalleryType, null, null, null, null, null, null, null, binding.galleryFrameLayoutItemPostCard3GalleryType, binding.galleryRecyclerViewItemPostCard3GalleryType, binding.imageIndexTextViewItemPostCard3GalleryType, binding.noPreviewImageViewItemPostCard3GalleryType, binding.bottomConstraintLayoutItemPostCard3GalleryType, binding.upvoteButtonItemPostCard3GalleryType, binding.scoreTextViewItemPostCard3GalleryType, binding.downvoteButtonItemPostCard3GalleryType, binding.commentsCountButtonItemPostCard3GalleryType, binding.saveButtonItemPostCard3GalleryType, binding.shareButtonItemPostCard3GalleryType); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostFilledCardViewBackgroundColor : mFilledCardViewBackgroundColor)); } } public class PostMaterial3CardTextTypeViewHolder extends PostTextTypeViewHolder { PostMaterial3CardTextTypeViewHolder(@NonNull ItemPostCard3TextBinding binding) { super(binding.getRoot()); setBaseView( binding.iconGifImageViewItemPostCard3TextType, binding.subredditNameTextViewItemPostCard3TextType, binding.userTextViewItemPostCard3TextType, binding.stickiedPostImageViewItemPostCard3TextType, binding.postTimeTextViewItemPostCard3TextType, binding.titleTextViewItemPostCard3TextType, null, null, null, null, null, null, null, binding.bottomConstraintLayoutItemPostCard3TextType, binding.upvoteButtonItemPostCard3TextType, binding.scoreTextViewItemPostCard3TextType, binding.downvoteButtonItemPostCard3TextType, binding.commentsCountButtonItemPostCard3TextType, binding.saveButtonItemPostCard3TextType, binding.shareButtonItemPostCard3TextType, binding.contentTextViewItemPostCard3TextType); } @Override void setItemViewBackgroundColor(boolean isReadPost) { itemView.setBackgroundTintList(ColorStateList.valueOf(isReadPost ? mReadPostFilledCardViewBackgroundColor : mFilledCardViewBackgroundColor)); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PrivateMessagesDetailRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.net.Uri; import android.text.Spanned; import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.Locale; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; import io.noties.markwon.inlineparser.BangInlineProcessor; import io.noties.markwon.inlineparser.HtmlInlineProcessor; import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.movement.MovementMethodPlugin; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewPrivateMessagesActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemPrivateMessageReceivedBinding; import ml.docilealligator.infinityforreddit.databinding.ItemPrivateMessageSentBinding; import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin; import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod; import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin; import ml.docilealligator.infinityforreddit.markdown.SuperscriptPlugin; import ml.docilealligator.infinityforreddit.message.Message; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class PrivateMessagesDetailRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MESSAGE_SENT = 0; private static final int VIEW_TYPE_MESSAGE_RECEIVED = 1; private Message mMessage; private final ViewPrivateMessagesActivity mViewPrivateMessagesActivity; private final RequestManager mGlide; private final Locale mLocale; private final String mAccountName; private final Markwon mMarkwon; private final boolean mShowElapsedTime; private final String mTimeFormatPattern; private final int mSecondaryTextColor; private final int mReceivedMessageTextColor; private final int mSentMessageTextColor; private final int mReceivedMessageBackgroundColor; private final int mSentMessageBackgroundColor; public PrivateMessagesDetailRecyclerViewAdapter(ViewPrivateMessagesActivity viewPrivateMessagesActivity, SharedPreferences sharedPreferences, Locale locale, Message message, @NonNull String accountName, CustomThemeWrapper customThemeWrapper) { mMessage = message; mViewPrivateMessagesActivity = viewPrivateMessagesActivity; mGlide = Glide.with(viewPrivateMessagesActivity); mLocale = locale; mAccountName = accountName; int commentColor = customThemeWrapper.getCommentColor(); // todo:https://github.com/Docile-Alligator/Infinity-For-Reddit/issues/1027 // add tables support and replace with MarkdownUtils#commonPostMarkwonBuilder mMarkwon = Markwon.builder(viewPrivateMessagesActivity) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) .usePlugin(new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (mViewPrivateMessagesActivity.contentTypeface != null) { textView.setTypeface(mViewPrivateMessagesActivity.contentTypeface); } } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(viewPrivateMessagesActivity, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); viewPrivateMessagesActivity.startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(customThemeWrapper.getLinkColor()); } }) .usePlugin(SuperscriptPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(SpoilerParserPlugin.create(commentColor, commentColor | 0xFF000000)) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod())) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .build(); mShowElapsedTime = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ELAPSED_TIME_KEY, false); mTimeFormatPattern = sharedPreferences.getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE); mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); mReceivedMessageTextColor = customThemeWrapper.getReceivedMessageTextColor(); mSentMessageTextColor = customThemeWrapper.getSentMessageTextColor(); mReceivedMessageBackgroundColor = customThemeWrapper.getReceivedMessageBackgroundColor(); mSentMessageBackgroundColor = customThemeWrapper.getSentMessageBackgroundColor(); } @Override public int getItemViewType(int position) { if (position == 0) { return mMessage.getAuthor().equals(mAccountName) ? VIEW_TYPE_MESSAGE_SENT : VIEW_TYPE_MESSAGE_RECEIVED; } else { return mMessage.getReplies().get(position - 1).getAuthor().equals(mAccountName) ? VIEW_TYPE_MESSAGE_SENT : VIEW_TYPE_MESSAGE_RECEIVED; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MESSAGE_SENT) { return new SentMessageViewHolder(ItemPrivateMessageSentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new ReceivedMessageViewHolder(ItemPrivateMessageReceivedBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { Message message; if (holder.getBindingAdapterPosition() == 0) { message = mMessage; } else { message = mMessage.getReplies().get(holder.getBindingAdapterPosition() - 1); } if (message != null) { if (holder instanceof MessageViewHolder) { mMarkwon.setMarkdown(((MessageViewHolder) holder).messageTextView, message.getBody()); if (mShowElapsedTime) { ((MessageViewHolder) holder).timeTextView.setText(Utils.getElapsedTime(mViewPrivateMessagesActivity, message.getTimeUTC())); } else { ((MessageViewHolder) holder).timeTextView.setText(Utils.getFormattedTime(mLocale, message.getTimeUTC(), mTimeFormatPattern)); } } if (holder instanceof SentMessageViewHolder) { ((SentMessageViewHolder) holder).messageTextView.setBackground(Utils.getTintedDrawable(mViewPrivateMessagesActivity, R.drawable.private_message_ballon, mSentMessageBackgroundColor)); } else if (holder instanceof ReceivedMessageViewHolder) { mViewPrivateMessagesActivity.fetchUserAvatar(message.getAuthor(), userAvatarUrl -> { if (userAvatarUrl == null || userAvatarUrl.equals("")) { mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((ReceivedMessageViewHolder) holder).binding.avatarImageViewItemPrivateMessageReceived); } else { mGlide.load(userAvatarUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(mGlide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((ReceivedMessageViewHolder) holder).binding.avatarImageViewItemPrivateMessageReceived); } }); ((ReceivedMessageViewHolder) holder).binding.avatarImageViewItemPrivateMessageReceived.setOnClickListener(view -> { if (message.isAuthorDeleted()) { return; } Intent intent = new Intent(mViewPrivateMessagesActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, message.getAuthor()); mViewPrivateMessagesActivity.startActivity(intent); }); ((ReceivedMessageViewHolder) holder).messageTextView.setBackground( Utils.getTintedDrawable(mViewPrivateMessagesActivity, R.drawable.private_message_ballon, mReceivedMessageBackgroundColor)); } } } @Override public int getItemCount() { if (mMessage == null) { return 0; } else if (mMessage.getReplies() == null) { return 1; } else { return 1 + mMessage.getReplies().size(); } } public void setMessage(Message message) { mMessage = message; notifyDataSetChanged(); } public void addReply(Message reply) { int currentSize = getItemCount(); if (mMessage != null) { mMessage.addReply(reply); } else { mMessage = reply; } notifyItemInserted(currentSize); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof MessageViewHolder) { ((MessageViewHolder) holder).messageTextView.setBackground(null); ((MessageViewHolder) holder).timeTextView.setVisibility(View.GONE); } if (holder instanceof ReceivedMessageViewHolder) { mGlide.clear(((ReceivedMessageViewHolder) holder).binding.avatarImageViewItemPrivateMessageReceived); } } class MessageViewHolder extends RecyclerView.ViewHolder { TextView messageTextView; TextView timeTextView; ImageView copyImageView; public MessageViewHolder(@NonNull View itemView) { super(itemView); } void setBaseView(TextView messageTextView, TextView timeTextView, ImageView copyImageView) { this.messageTextView = messageTextView; this.timeTextView = timeTextView; this.copyImageView = copyImageView; messageTextView.setTextColor(Color.WHITE); timeTextView.setTextColor(mSecondaryTextColor); itemView.setOnClickListener(view -> { if (timeTextView.getVisibility() != View.VISIBLE) { timeTextView.setVisibility(View.VISIBLE); copyImageView.setVisibility(View.VISIBLE); } else { timeTextView.setVisibility(View.GONE); copyImageView.setVisibility(View.GONE); } mViewPrivateMessagesActivity.delayTransition(); }); messageTextView.setOnClickListener(view -> { if (messageTextView.getSelectionStart() == -1 && messageTextView.getSelectionEnd() == -1) { itemView.performClick(); } }); copyImageView.setColorFilter(mSecondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); copyImageView.setOnClickListener(view -> { Message message; if (getBindingAdapterPosition() == 0) { message = mMessage; } else { message = mMessage.getReplies().get(getBindingAdapterPosition() - 1); } if (message != null) { ClipboardManager clipboard = (ClipboardManager) mViewPrivateMessagesActivity.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", message.getBody()); clipboard.setPrimaryClip(clip); if (android.os.Build.VERSION.SDK_INT < 33) { Toast.makeText(mViewPrivateMessagesActivity, R.string.copy_success, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(mViewPrivateMessagesActivity, R.string.copy_failed, Toast.LENGTH_SHORT).show(); } } }); } } class SentMessageViewHolder extends MessageViewHolder { SentMessageViewHolder(@NonNull ItemPrivateMessageSentBinding binding) { super(binding.getRoot()); setBaseView(binding.messageTextViewItemPrivateMessageSent, binding.timeTextViewItemPrivateMessageSent, binding.copyImageViewItemPrivateMessageSent); binding.messageTextViewItemPrivateMessageSent.setTextColor(mSentMessageTextColor); } } class ReceivedMessageViewHolder extends MessageViewHolder { ItemPrivateMessageReceivedBinding binding; ReceivedMessageViewHolder(@NonNull ItemPrivateMessageReceivedBinding binding) { super(binding.getRoot()); this.binding = binding; setBaseView(binding.messageTextViewItemPrivateMessageReceived, binding.timeTextViewItemPrivateMessageReceived, binding.copyImageViewItemPrivateMessageReceived); binding.messageTextViewItemPrivateMessageReceived.setTextColor(mReceivedMessageTextColor); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/RedditGallerySubmissionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.post.RedditGalleryPayload; import ml.docilealligator.infinityforreddit.activities.PostGalleryActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SetRedditGalleryItemCaptionAndUrlBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemRedditGallerySubmissionImageBinding; public class RedditGallerySubmissionRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_IMAGE = 1; private static final int VIEW_TYPE_ADD_IMAGE = 2; private final PostGalleryActivity activity; private ArrayList redditGalleryImageInfoList; private final CustomThemeWrapper customThemeWrapper; private final ItemClickListener itemClickListener; private final RequestManager glide; public RedditGallerySubmissionRecyclerViewAdapter(PostGalleryActivity activity, CustomThemeWrapper customThemeWrapper, ItemClickListener itemClickListener) { this.activity = activity; glide = Glide.with(activity); this.customThemeWrapper = customThemeWrapper; this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { if (redditGalleryImageInfoList == null || position >= redditGalleryImageInfoList.size()) { return VIEW_TYPE_ADD_IMAGE; } return VIEW_TYPE_IMAGE; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_ADD_IMAGE) { return new AddImageViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_reddit_gallery_submission_add_image, parent, false)); } return new ImageViewHolder(ItemRedditGallerySubmissionImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof ImageViewHolder) { glide.load(redditGalleryImageInfoList.get(position).imageUrlString) .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(48))) .listener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { ((ImageViewHolder) holder).binding.progressBarItemRedditGallerySubmissionImage.setVisibility(View.GONE); ((ImageViewHolder) holder).binding.closeImageViewItemRedditGallerySubmissionImage.setVisibility(View.VISIBLE); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { return false; } }) .into(((ImageViewHolder) holder).binding.aspectRatioGifImageViewItemRedditGallerySubmissionImage); if (redditGalleryImageInfoList.get(position).payload != null) { ((ImageViewHolder) holder).binding.progressBarItemRedditGallerySubmissionImage.setVisibility(View.GONE); ((ImageViewHolder) holder).binding.closeImageViewItemRedditGallerySubmissionImage.setVisibility(View.VISIBLE); } } } @Override public int getItemCount() { return redditGalleryImageInfoList == null ? 1 : (redditGalleryImageInfoList.size() >= 20 ? 20 : redditGalleryImageInfoList.size() + 1); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof ImageViewHolder) { glide.clear(((ImageViewHolder) holder).binding.aspectRatioGifImageViewItemRedditGallerySubmissionImage); ((ImageViewHolder) holder).binding.progressBarItemRedditGallerySubmissionImage.setVisibility(View.VISIBLE); ((ImageViewHolder) holder).binding.closeImageViewItemRedditGallerySubmissionImage.setVisibility(View.GONE); } } public ArrayList getRedditGalleryImageInfoList() { return redditGalleryImageInfoList; } public void setRedditGalleryImageInfoList(ArrayList redditGalleryImageInfoList) { this.redditGalleryImageInfoList = redditGalleryImageInfoList; notifyDataSetChanged(); } public void addImage(String imageUrl) { if (redditGalleryImageInfoList == null) { redditGalleryImageInfoList = new ArrayList<>(); } redditGalleryImageInfoList.add(new RedditGalleryImageInfo(imageUrl)); notifyItemInserted(redditGalleryImageInfoList.size() - 1); } public void setImageAsUploaded(String mediaId) { redditGalleryImageInfoList.get(redditGalleryImageInfoList.size() - 1).payload = new RedditGalleryPayload.Item("", "", mediaId); notifyItemChanged(redditGalleryImageInfoList.size() - 1); } public void removeFailedToUploadImage() { redditGalleryImageInfoList.remove(redditGalleryImageInfoList.size() - 1); notifyItemRemoved(redditGalleryImageInfoList.size()); } public void setCaptionAndUrl(int position, String caption, String url) { if (redditGalleryImageInfoList.size() > position && position >= 0) { redditGalleryImageInfoList.get(position).payload.setCaption(caption); redditGalleryImageInfoList.get(position).payload.setOutboundUrl(url); } } class ImageViewHolder extends RecyclerView.ViewHolder { ItemRedditGallerySubmissionImageBinding binding; public ImageViewHolder(@NonNull ItemRedditGallerySubmissionImageBinding binding) { super(binding.getRoot()); this.binding = binding; binding.aspectRatioGifImageViewItemRedditGallerySubmissionImage.setRatio(1); binding.aspectRatioGifImageViewItemRedditGallerySubmissionImage.setOnClickListener(view -> { RedditGalleryPayload.Item payload = redditGalleryImageInfoList.get(getBindingAdapterPosition()).payload; if (payload != null) { SetRedditGalleryItemCaptionAndUrlBottomSheetFragment fragment = new SetRedditGalleryItemCaptionAndUrlBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putInt(SetRedditGalleryItemCaptionAndUrlBottomSheetFragment.EXTRA_POSITION, getBindingAdapterPosition()); bundle.putString(SetRedditGalleryItemCaptionAndUrlBottomSheetFragment.EXTRA_CAPTION, payload.getCaption()); bundle.putString(SetRedditGalleryItemCaptionAndUrlBottomSheetFragment.EXTRA_URL, payload.getOutboundUrl()); fragment.setArguments(bundle); fragment.show(activity.getSupportFragmentManager(), fragment.getTag()); } }); binding.closeImageViewItemRedditGallerySubmissionImage.setOnClickListener(view -> { redditGalleryImageInfoList.remove(getBindingAdapterPosition()); notifyItemRemoved(getBindingAdapterPosition()); }); } } class AddImageViewHolder extends RecyclerView.ViewHolder { public AddImageViewHolder(@NonNull View itemView) { super(itemView); FloatingActionButton fab = itemView.findViewById(R.id.fab_item_gallery_submission_add_image); fab.setBackgroundTintList(ColorStateList.valueOf(customThemeWrapper.getColorAccent())); fab.setImageTintList(ColorStateList.valueOf(customThemeWrapper.getFABIconColor())); itemView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { itemView.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = itemView.getMeasuredWidth(); ViewGroup.LayoutParams params = itemView.getLayoutParams(); params.height = width; itemView.setLayoutParams(params); } }); fab.setOnClickListener(view -> itemClickListener.onAddImageClicked()); itemView.setOnClickListener(view -> fab.performClick()); } } public static class RedditGalleryImageInfo implements Parcelable { public String imageUrlString; public RedditGalleryPayload.Item payload; public RedditGalleryImageInfo(String imageUrlString) { this.imageUrlString = imageUrlString; } protected RedditGalleryImageInfo(Parcel in) { imageUrlString = in.readString(); payload = in.readParcelable(RedditGalleryPayload.Item.class.getClassLoader()); } public static final Creator CREATOR = new Creator() { @Override public RedditGalleryImageInfo createFromParcel(Parcel in) { return new RedditGalleryImageInfo(in); } @Override public RedditGalleryImageInfo[] newArray(int size) { return new RedditGalleryImageInfo[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(imageUrlString); parcel.writeParcelable(payload, i); } } public interface ItemClickListener { void onAddImageClicked(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/ReportReasonRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.thing.ReportReason; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemReportReasonBinding; public class ReportReasonRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private final ArrayList generalReasons; private ArrayList rules; private final int primaryTextColor; private final int colorAccent; public ReportReasonRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, ArrayList generalReasons) { this.activity = activity; this.generalReasons = generalReasons; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); colorAccent = customThemeWrapper.getColorAccent(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ReasonViewHolder(ItemReportReasonBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof ReasonViewHolder) { ReportReason reportReason; if (position >= generalReasons.size()) { reportReason = rules.get(holder.getBindingAdapterPosition() - generalReasons.size()); } else { reportReason = generalReasons.get(holder.getBindingAdapterPosition()); } ((ReasonViewHolder) holder).binding.reasonTextViewItemReportReason.setText(reportReason.getReportReason()); ((ReasonViewHolder) holder).binding.checkBoxItemReportReason.setChecked(reportReason.isSelected()); } } @Override public int getItemCount() { return rules == null ? generalReasons.size() : rules.size() + generalReasons.size(); } public void setRules(ArrayList reportReasons) { this.rules = reportReasons; notifyDataSetChanged(); } public ReportReason getSelectedReason() { if (rules != null) { for (ReportReason reportReason : rules) { if (reportReason.isSelected()) { return reportReason; } } } for (ReportReason reportReason : generalReasons) { if (reportReason.isSelected()) { return reportReason; } } return null; } public ArrayList getGeneralReasons() { return generalReasons; } public ArrayList getRules() { return rules; } class ReasonViewHolder extends RecyclerView.ViewHolder { ItemReportReasonBinding binding; ReasonViewHolder(@NonNull ItemReportReasonBinding binding) { super(binding.getRoot()); this.binding = binding; binding.reasonTextViewItemReportReason.setTextColor(primaryTextColor); binding.checkBoxItemReportReason.setButtonTintList(ColorStateList.valueOf(colorAccent)); if (activity.typeface != null) { binding.reasonTextViewItemReportReason.setTypeface(activity.typeface); } binding.checkBoxItemReportReason.setOnClickListener(view -> { for (int i = 0; i < generalReasons.size(); i++) { if (generalReasons.get(i).isSelected()) { generalReasons.get(i).setSelected(false); notifyItemChanged(i); } } if (rules != null) { for (int i = 0; i < rules.size(); i++) { if (rules.get(i).isSelected()) { rules.get(i).setSelected(false); notifyItemChanged(i + generalReasons.size()); } } } if (getBindingAdapterPosition() >= generalReasons.size()) { rules.get(getBindingAdapterPosition() - generalReasons.size()).setSelected(binding.checkBoxItemReportReason.isChecked()); } else { generalReasons.get(getBindingAdapterPosition()).setSelected(binding.checkBoxItemReportReason.isChecked()); } }); itemView.setOnClickListener(view -> binding.checkBoxItemReportReason.performClick()); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/RulesRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.net.Uri; import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import java.util.ArrayList; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.recycler.MarkwonAdapter; import ml.docilealligator.infinityforreddit.subreddit.Rule; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.SwipeLockInterface; import ml.docilealligator.infinityforreddit.customviews.SwipeLockLinearLayoutManager; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; import ml.docilealligator.infinityforreddit.databinding.ItemRuleBinding; import ml.docilealligator.infinityforreddit.markdown.EmoteCloseBracketInlineProcessor; import ml.docilealligator.infinityforreddit.markdown.EmotePlugin; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class RulesRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private final EmoteCloseBracketInlineProcessor emoteCloseBracketInlineProcessor; private final EmotePlugin emotePlugin; private final ImageAndGifPlugin imageAndGifPlugin; private final ImageAndGifEntry imageAndGifEntry; private final Markwon markwon; @Nullable private final SliderPanel sliderPanel; private ArrayList rules; private final int mPrimaryTextColor; public RulesRecyclerViewAdapter(@NonNull BaseActivity activity, @NonNull CustomThemeWrapper customThemeWrapper, @Nullable SliderPanel sliderPanel, String subredditName) { this.activity = activity; this.sliderPanel = sliderPanel; mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); int spoilerBackgroundColor = mPrimaryTextColor | 0xFF000000; MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (activity.typeface != null) { textView.setTypeface(activity.typeface); } textView.setTextColor(mPrimaryTextColor); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(activity, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); activity.startActivity(intent); }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(customThemeWrapper.getLinkColor()); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { if (!activity.isDestroyed() && !activity.isFinishing()) { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(activity.getSupportFragmentManager(), null); } return true; }; emoteCloseBracketInlineProcessor = new EmoteCloseBracketInlineProcessor(); emotePlugin = EmotePlugin.create(activity, SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, mediaMetadata -> { Intent imageIntent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, subredditName); imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); imageAndGifPlugin = new ImageAndGifPlugin(); imageAndGifEntry = new ImageAndGifEntry(activity, Glide.with(activity), SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, (mediaMetadata, commentId, postId) -> { Intent imageIntent = new Intent(activity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, subredditName); imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); markwon = MarkdownUtils.createFullRedditMarkwon(activity, miscPlugin, emoteCloseBracketInlineProcessor, emotePlugin, imageAndGifPlugin, mPrimaryTextColor, spoilerBackgroundColor, onLinkLongClickListener); } @NonNull @Override public RuleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new RuleViewHolder(ItemRuleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RuleViewHolder holder, int position) { Rule rule = rules.get(holder.getBindingAdapterPosition()); holder.binding.shortNameTextViewItemRule.setText(rule.getShortName()); if (rule.getDescriptionHtml() == null) { holder.binding.descriptionMarkwonViewItemRule.setVisibility(View.GONE); } else { holder.markwonAdapter.setMarkdown(markwon, rule.getDescriptionHtml()); //noinspection NotifyDatasetChanged holder.markwonAdapter.notifyDataSetChanged(); } } @Override public int getItemCount() { return rules == null ? 0 : rules.size(); } @Override public void onViewRecycled(@NonNull RuleViewHolder holder) { super.onViewRecycled(holder); holder.binding.descriptionMarkwonViewItemRule.setVisibility(View.VISIBLE); } public void changeDataset(ArrayList rules) { this.rules = rules; notifyDataSetChanged(); } public void setDataSavingMode(boolean dataSavingMode) { emotePlugin.setDataSavingMode(dataSavingMode); imageAndGifEntry.setDataSavingMode(dataSavingMode); } class RuleViewHolder extends RecyclerView.ViewHolder { ItemRuleBinding binding; @NonNull final MarkwonAdapter markwonAdapter; RuleViewHolder(@NonNull ItemRuleBinding binding) { super(binding.getRoot()); this.binding = binding; binding.shortNameTextViewItemRule.setTextColor(mPrimaryTextColor); if (activity.typeface != null) { binding.shortNameTextViewItemRule.setTypeface(activity.typeface); } markwonAdapter = MarkdownUtils.createCustomTablesAndImagesAdapter(activity, imageAndGifEntry); SwipeLockLinearLayoutManager swipeLockLinearLayoutManager = new SwipeLockLinearLayoutManager(activity, new SwipeLockInterface() { @Override public void lockSwipe() { if (sliderPanel != null) { sliderPanel.lock(); } } @Override public void unlockSwipe() { if (sliderPanel != null) { sliderPanel.unlock(); } } }); binding.descriptionMarkwonViewItemRule.setLayoutManager(swipeLockLinearLayoutManager); binding.descriptionMarkwonViewItemRule.setAdapter(markwonAdapter); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/SearchActivityRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemRecentSearchQueryBinding; import ml.docilealligator.infinityforreddit.recentsearchquery.RecentSearchQuery; public class SearchActivityRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private List recentSearchQueries; private final int filledCardViewBackgroundColor; private final int primaryTextColor; private final int secondaryTextColor; private final int primaryIconColor; private final int subredditTextColor; private final int userTextColor; private final ItemOnClickListener itemOnClickListener; public interface ItemOnClickListener { void onClick(RecentSearchQuery recentSearchQuery, boolean searchImmediately); void onDelete(RecentSearchQuery recentSearchQuery); } public SearchActivityRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, ItemOnClickListener itemOnClickListener) { this.activity = activity; this.filledCardViewBackgroundColor = customThemeWrapper.getFilledCardViewBackgroundColor(); this.primaryTextColor = customThemeWrapper.getPrimaryTextColor(); this.secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); this.primaryIconColor = customThemeWrapper.getPrimaryIconColor(); this.subredditTextColor = customThemeWrapper.getSubreddit(); this.userTextColor = customThemeWrapper.getUsername(); this.itemOnClickListener = itemOnClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new RecentSearchQueryViewHolder(ItemRecentSearchQueryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof RecentSearchQueryViewHolder) { if (recentSearchQueries != null && !recentSearchQueries.isEmpty() && position < recentSearchQueries.size()) { RecentSearchQuery recentSearchQuery = recentSearchQueries.get(position); ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryTextViewItemRecentSearchQuery.setText(recentSearchQuery.getSearchQuery()); switch (recentSearchQuery.getSearchInThingType()) { case SelectThingReturnKey.THING_TYPE.SUBREDDIT: if (recentSearchQuery.getSearchInSubredditOrUserName() != null && !recentSearchQuery.getSearchInSubredditOrUserName().isEmpty()) { ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setTextColor(subredditTextColor); ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setText("r/" + recentSearchQuery.getSearchInSubredditOrUserName()); } else { ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setTextColor(secondaryTextColor); ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setText(R.string.all_subreddits); } break; case SelectThingReturnKey.THING_TYPE.USER: ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setTextColor(userTextColor); ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setText("u/" + recentSearchQuery.getSearchInSubredditOrUserName()); break; case SelectThingReturnKey.THING_TYPE.MULTIREDDIT: ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setTextColor(secondaryTextColor); ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery .setText(recentSearchQuery.getMultiRedditDisplayName()); } holder.itemView.postDelayed(() -> { ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryTextViewItemRecentSearchQuery.setSelected(true); ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery.setSelected(true); }, 1000); } } } @Override public int getItemCount() { return recentSearchQueries == null ? 0 : recentSearchQueries.size(); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof RecentSearchQueryViewHolder) { ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryTextViewItemRecentSearchQuery.setSelected(false); ((RecentSearchQueryViewHolder) holder).binding.recentSearchQueryWhereTextViewItemRecentSearchQuery.setSelected(false); } } public void setRecentSearchQueries(List recentSearchQueries) { this.recentSearchQueries = recentSearchQueries; notifyDataSetChanged(); } class RecentSearchQueryViewHolder extends RecyclerView.ViewHolder { ItemRecentSearchQueryBinding binding; public RecentSearchQueryViewHolder(@NonNull ItemRecentSearchQueryBinding binding) { super(binding.getRoot()); this.binding = binding; itemView.setBackgroundTintList(ColorStateList.valueOf(filledCardViewBackgroundColor)); binding.recentSearchQueryTextViewItemRecentSearchQuery.setTextColor(primaryTextColor); if (activity.typeface != null) { binding.recentSearchQueryTextViewItemRecentSearchQuery.setTypeface(activity.typeface); } binding.selectQueryImageViewItemRecentSearchQuery.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); itemView.setOnClickListener(view -> { if (recentSearchQueries != null && !recentSearchQueries.isEmpty()) { itemOnClickListener.onClick(recentSearchQueries.get(getBindingAdapterPosition()), true); } }); itemView.setOnLongClickListener(view -> { if (recentSearchQueries != null && !recentSearchQueries.isEmpty()) { itemOnClickListener.onDelete(recentSearchQueries.get(getBindingAdapterPosition())); } return true; }); binding.selectQueryImageViewItemRecentSearchQuery.setOnClickListener(view -> { if (recentSearchQueries != null && !recentSearchQueries.isEmpty()) { itemOnClickListener.onClick(recentSearchQueries.get(getBindingAdapterPosition()), false); } }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/SelectedSubredditsRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.RequestOptions; import java.util.ArrayList; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemSelectedSubredditBinding; import ml.docilealligator.infinityforreddit.multireddit.ExpandedSubredditInMultiReddit; public class SelectedSubredditsRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private final CustomThemeWrapper customThemeWrapper; private final RequestManager glide; private final ArrayList subreddits; public SelectedSubredditsRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, RequestManager glide, ArrayList subreddits) { this.activity = activity; this.customThemeWrapper = customThemeWrapper; this.glide = glide; if (subreddits == null) { this.subreddits = new ArrayList<>(); } else { this.subreddits = subreddits; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new SubredditViewHolder(ItemSelectedSubredditBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof SubredditViewHolder) { glide.load(subreddits.get(holder.getBindingAdapterPosition()).getIconUrl()) .apply(RequestOptions.bitmapTransform(new RoundedCornersTransformation(72, 0))) .error(glide.load(R.drawable.subreddit_default_icon) .apply(RequestOptions.bitmapTransform(new RoundedCornersTransformation(72, 0)))) .into(((SubredditViewHolder) holder).binding.iconImageViewItemSelectedSubreddit); ((SubredditViewHolder) holder).binding.subredditNameItemSelectedSubreddit.setText(subreddits.get(holder.getBindingAdapterPosition()).getName()); ((SubredditViewHolder) holder).binding.deleteImageViewItemSelectedSubreddit.setOnClickListener(view -> { subreddits.remove(holder.getBindingAdapterPosition()); notifyItemRemoved(holder.getBindingAdapterPosition()); }); } } @Override public int getItemCount() { return subreddits.size(); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof SubredditViewHolder) { glide.clear(((SubredditViewHolder) holder).binding.iconImageViewItemSelectedSubreddit); } } public void addSubreddits(ArrayList newSubreddits) { int oldSize = subreddits.size(); subreddits.addAll(newSubreddits); notifyItemRangeInserted(oldSize, newSubreddits.size()); } public void addUserInSubredditType(String username) { subreddits.add(new ExpandedSubredditInMultiReddit(username, null)); notifyItemInserted(subreddits.size()); } public ArrayList getSubreddits() { return subreddits; } class SubredditViewHolder extends RecyclerView.ViewHolder { ItemSelectedSubredditBinding binding; public SubredditViewHolder(@NonNull ItemSelectedSubredditBinding binding) { super(binding.getRoot()); this.binding = binding; binding.subredditNameItemSelectedSubreddit.setTextColor(customThemeWrapper.getPrimaryIconColor()); binding.deleteImageViewItemSelectedSubreddit.setColorFilter(customThemeWrapper.getPrimaryIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); if (activity.typeface != null) { binding.subredditNameItemSelectedSubreddit.setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/SettingsSearchAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.settings.SettingsSearchItem; import ml.docilealligator.infinityforreddit.settings.SettingsSearchRegistry; public class SettingsSearchAdapter extends RecyclerView.Adapter { public interface OnItemClickListener { void onItemClick(SettingsSearchItem item); } private final OnItemClickListener mListener; private List mFilteredItems = new ArrayList<>(); public SettingsSearchAdapter(OnItemClickListener listener) { mListener = listener; } public void filter(String query) { String q = query == null ? "" : query.trim().toLowerCase(); List all = SettingsSearchRegistry.getInstance().getItems(); if (q.isEmpty()) { mFilteredItems = new ArrayList<>(all); } else { List result = new ArrayList<>(); for (SettingsSearchItem item : all) { if (item.title.toLowerCase().contains(q) || (item.summary != null && item.summary.toLowerCase().contains(q)) || item.breadcrumb.toLowerCase().contains(q)) { result.add(item); } } mFilteredItems = result; } notifyDataSetChanged(); } public boolean isEmpty() { return mFilteredItems.isEmpty(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_settings_search, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { SettingsSearchItem item = mFilteredItems.get(position); holder.titleView.setText(item.title); holder.breadcrumbView.setText(item.breadcrumb); holder.itemView.setOnClickListener(v -> mListener.onItemClick(item)); } @Override public int getItemCount() { return mFilteredItems.size(); } static class ViewHolder extends RecyclerView.ViewHolder { final TextView titleView; final TextView breadcrumbView; ViewHolder(View itemView) { super(itemView); titleView = itemView.findViewById(R.id.title_item_settings_search); breadcrumbView = itemView.findViewById(R.id.breadcrumb_item_settings_search); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/SubredditAutocompleteRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.google.android.material.checkbox.MaterialCheckBox; import java.util.List; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import pl.droidsonroids.gif.GifImageView; public class SubredditAutocompleteRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private List subreddits; private final RequestManager glide; private final CustomThemeWrapper customThemeWrapper; private final ItemOnClickListener itemOnClickListener; public SubredditAutocompleteRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, ItemOnClickListener itemOnClickListener) { this.activity = activity; glide = Glide.with(activity); this.customThemeWrapper = customThemeWrapper; this.itemOnClickListener = itemOnClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new SubredditViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_subreddit_listing, parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof SubredditViewHolder) { glide.load(subreddits.get(position).getIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((SubredditViewHolder) holder).iconImageView); ((SubredditViewHolder) holder).subredditNameTextView.setText(subreddits.get(position).getName()); ((SubredditViewHolder) holder).subscriberCountTextView.setText(activity.getString(R.string.subscribers_number_detail, subreddits.get(position).getNSubscribers())); } } @Override public int getItemCount() { return subreddits == null ? 0 : subreddits.size(); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof SubredditViewHolder) { glide.clear(((SubredditViewHolder) holder).iconImageView); } } public void setSubreddits(List subreddits) { this.subreddits = subreddits; notifyDataSetChanged(); } class SubredditViewHolder extends RecyclerView.ViewHolder { GifImageView iconImageView; TextView subredditNameTextView; TextView subscriberCountTextView; ImageView subscribeImageView; MaterialCheckBox checkBox; public SubredditViewHolder(@NonNull View itemView) { super(itemView); iconImageView = itemView.findViewById(R.id.subreddit_icon_gif_image_view_item_subreddit_listing); subredditNameTextView = itemView.findViewById(R.id.subreddit_name_text_view_item_subreddit_listing); subscriberCountTextView = itemView.findViewById(R.id.subscriber_count_text_view_item_subreddit_listing); subscribeImageView = itemView.findViewById(R.id.subscribe_image_view_item_subreddit_listing); checkBox = itemView.findViewById(R.id.checkbox_item_subreddit_listing); subscribeImageView.setVisibility(View.GONE); checkBox.setVisibility(View.GONE); subredditNameTextView.setTextColor(customThemeWrapper.getPrimaryTextColor()); subscriberCountTextView.setTextColor(customThemeWrapper.getSecondaryTextColor()); if (activity.typeface != null) { subredditNameTextView.setTypeface(activity.typeface); subscriberCountTextView.setTypeface(activity.typeface); } itemView.setOnClickListener(view -> { itemOnClickListener.onClick(subreddits.get(getBindingAdapterPosition())); }); } } public interface ItemOnClickListener { void onClick(SubredditData subredditData); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/SubredditListingRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.paging.PagedListAdapter; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.concurrent.Executor; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.asynctasks.CheckIsSubscribedToSubreddit; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemFooterErrorBinding; import ml.docilealligator.infinityforreddit.databinding.ItemFooterLoadingBinding; import ml.docilealligator.infinityforreddit.databinding.ItemSubredditListingBinding; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditSubscription; import retrofit2.Retrofit; public class SubredditListingRecyclerViewAdapter extends PagedListAdapter { private static final int VIEW_TYPE_DATA = 0; private static final int VIEW_TYPE_ERROR = 1; private static final int VIEW_TYPE_LOADING = 2; private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(@NonNull SubredditData oldItem, @NonNull SubredditData newItem) { return oldItem.getId().equals(newItem.getId()); } @Override public boolean areContentsTheSame(@NonNull SubredditData oldItem, @NonNull SubredditData newItem) { return true; } }; private final RequestManager glide; private final BaseActivity activity; private final Executor executor; private final Retrofit retrofit; private final Retrofit oauthRetrofit; private final String accessToken; private final String accountName; private final RedditDataRoomDatabase redditDataRoomDatabase; private final boolean isMultiSelection; private final int colorPrimaryLightTheme; private final int primaryTextColor; private final int secondaryTextColor; private final int colorAccent; private final int buttonTextColor; private final int unsubscribed; private NetworkState networkState; private final Callback callback; public SubredditListingRecyclerViewAdapter(BaseActivity activity, Executor executor, Retrofit oauthRetrofit, Retrofit retrofit, CustomThemeWrapper customThemeWrapper, @Nullable String accessToken, @NonNull String accountName, RedditDataRoomDatabase redditDataRoomDatabase, boolean isMultiSelection, Callback callback) { super(DIFF_CALLBACK); this.activity = activity; this.executor = executor; this.oauthRetrofit = oauthRetrofit; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.redditDataRoomDatabase = redditDataRoomDatabase; this.isMultiSelection = isMultiSelection; this.callback = callback; glide = Glide.with(this.activity); colorPrimaryLightTheme = customThemeWrapper.getColorPrimaryLightTheme(); primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); colorAccent = customThemeWrapper.getColorAccent(); buttonTextColor = customThemeWrapper.getButtonTextColor(); unsubscribed = customThemeWrapper.getUnsubscribed(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_DATA) { return new DataViewHolder(ItemSubredditListingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_ERROR) { return new ErrorViewHolder(ItemFooterErrorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new LoadingViewHolder(ItemFooterLoadingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof DataViewHolder) { SubredditData subredditData = getItem(position); if (subredditData != null) { if (isMultiSelection) { ((DataViewHolder) holder).binding.checkboxItemSubredditListing.setOnCheckedChangeListener((compoundButton, b) -> subredditData.setSelected(b)); } ((DataViewHolder) holder).itemView.setOnClickListener(view -> { if (isMultiSelection) { ((DataViewHolder) holder).binding.checkboxItemSubredditListing.performClick(); } else { callback.subredditSelected(subredditData.getName(), subredditData.getIconUrl()); } }); if (!subredditData.getIconUrl().equals("")) { glide.load(subredditData.getIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((DataViewHolder) holder).binding.subredditIconGifImageViewItemSubredditListing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((DataViewHolder) holder).binding.subredditIconGifImageViewItemSubredditListing); } ((DataViewHolder) holder).binding.subredditNameTextViewItemSubredditListing.setText(subredditData.getName()); ((DataViewHolder) holder).binding.subscriberCountTextViewItemSubredditListing.setText(activity.getString(R.string.subscribers_number_detail, subredditData.getNSubscribers())); if (!isMultiSelection) { CheckIsSubscribedToSubreddit.checkIsSubscribedToSubreddit(executor, new Handler(), redditDataRoomDatabase, subredditData.getName(), accountName, new CheckIsSubscribedToSubreddit.CheckIsSubscribedToSubredditListener() { @Override public void isSubscribed() { ((DataViewHolder) holder).binding.subscribeImageViewItemSubredditListing.setVisibility(View.GONE); } @Override public void isNotSubscribed() { ((DataViewHolder) holder).binding.subscribeImageViewItemSubredditListing.setVisibility(View.VISIBLE); ((DataViewHolder) holder).binding.subscribeImageViewItemSubredditListing.setOnClickListener(view -> { if (!accountName.equals(Account.ANONYMOUS_ACCOUNT)) { SubredditSubscription.subscribeToSubreddit(executor, new Handler(), oauthRetrofit, retrofit, accessToken, subredditData.getName(), accountName, redditDataRoomDatabase, new SubredditSubscription.SubredditSubscriptionListener() { @Override public void onSubredditSubscriptionSuccess() { ((DataViewHolder) holder).binding.subscribeImageViewItemSubredditListing.setVisibility(View.GONE); Toast.makeText(activity, R.string.subscribed, Toast.LENGTH_SHORT).show(); } @Override public void onSubredditSubscriptionFail() { Toast.makeText(activity, R.string.subscribe_failed, Toast.LENGTH_SHORT).show(); } }); } else { SubredditSubscription.anonymousSubscribeToSubreddit(executor, new Handler(), retrofit, redditDataRoomDatabase, subredditData.getName(), new SubredditSubscription.SubredditSubscriptionListener() { @Override public void onSubredditSubscriptionSuccess() { ((DataViewHolder) holder).binding.subscribeImageViewItemSubredditListing.setVisibility(View.GONE); Toast.makeText(activity, R.string.subscribed, Toast.LENGTH_SHORT).show(); } @Override public void onSubredditSubscriptionFail() { Toast.makeText(activity, R.string.subscribe_failed, Toast.LENGTH_SHORT).show(); } }); } }); } }); } else { ((DataViewHolder) holder).binding.checkboxItemSubredditListing.setChecked(subredditData.isSelected()); } } } } @Override public int getItemViewType(int position) { // Reached at the end if (hasExtraRow() && position == getItemCount() - 1) { if (networkState.getStatus() == NetworkState.Status.LOADING) { return VIEW_TYPE_LOADING; } else { return VIEW_TYPE_ERROR; } } else { return VIEW_TYPE_DATA; } } @Override public int getItemCount() { if (hasExtraRow()) { return super.getItemCount() + 1; } return super.getItemCount(); } private boolean hasExtraRow() { return networkState != null && networkState.getStatus() != NetworkState.Status.SUCCESS; } public void setNetworkState(NetworkState newNetworkState) { NetworkState previousState = this.networkState; boolean previousExtraRow = hasExtraRow(); this.networkState = newNetworkState; boolean newExtraRow = hasExtraRow(); if (previousExtraRow != newExtraRow) { if (previousExtraRow) { notifyItemRemoved(super.getItemCount()); } else { notifyItemInserted(super.getItemCount()); } } else if (newExtraRow && !previousState.equals(newNetworkState)) { notifyItemChanged(getItemCount() - 1); } } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof DataViewHolder) { glide.clear(((DataViewHolder) holder).binding.subredditIconGifImageViewItemSubredditListing); ((DataViewHolder) holder).binding.subscribeImageViewItemSubredditListing.setVisibility(View.GONE); } } public interface Callback { void retryLoadingMore(); void subredditSelected(String subredditName, String iconUrl); } class DataViewHolder extends RecyclerView.ViewHolder { ItemSubredditListingBinding binding; DataViewHolder(@NonNull ItemSubredditListingBinding binding) { super(binding.getRoot()); this.binding = binding; binding.subredditNameTextViewItemSubredditListing.setTextColor(primaryTextColor); binding.subscriberCountTextViewItemSubredditListing.setTextColor(secondaryTextColor); binding.subscribeImageViewItemSubredditListing.setColorFilter(unsubscribed, android.graphics.PorterDuff.Mode.SRC_IN); if (isMultiSelection) { binding.checkboxItemSubredditListing.setVisibility(View.VISIBLE); } if (activity.typeface != null) { binding.subredditNameTextViewItemSubredditListing.setTypeface(activity.typeface); binding.subscriberCountTextViewItemSubredditListing.setTypeface(activity.typeface); } } } class ErrorViewHolder extends RecyclerView.ViewHolder { ItemFooterErrorBinding binding; ErrorViewHolder(@NonNull ItemFooterErrorBinding binding) { super(binding.getRoot()); this.binding = binding; binding.retryButtonItemFooterError.setOnClickListener(view -> callback.retryLoadingMore()); binding.errorTextViewItemFooterError.setText(R.string.load_comments_failed); binding.errorTextViewItemFooterError.setTextColor(secondaryTextColor); binding.retryButtonItemFooterError.setBackgroundTintList(ColorStateList.valueOf(colorPrimaryLightTheme)); binding.retryButtonItemFooterError.setTextColor(buttonTextColor); if (activity.typeface != null) { binding.retryButtonItemFooterError.setTypeface(activity.typeface); binding.errorTextViewItemFooterError.setTypeface(activity.typeface); } } } class LoadingViewHolder extends RecyclerView.ViewHolder { ItemFooterLoadingBinding binding; LoadingViewHolder(@NonNull ItemFooterLoadingBinding binding) { super(binding.getRoot()); this.binding = binding; binding.progressBarItemFooterLoading.setIndicatorColor(colorAccent); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/SubredditMultiselectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemSubscribedSubredditMultiSelectionBinding; import ml.docilealligator.infinityforreddit.subreddit.SubredditWithSelection; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; public class SubredditMultiselectionRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private ArrayList subscribedSubreddits; private final RequestManager glide; private final int primaryTextColor; private final int colorAccent; public SubredditMultiselectionRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper) { this.activity = activity; glide = Glide.with(activity); primaryTextColor = customThemeWrapper.getPrimaryTextColor(); colorAccent = customThemeWrapper.getColorAccent(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new SubscribedSubredditViewHolder(ItemSubscribedSubredditMultiSelectionBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof SubscribedSubredditViewHolder) { ((SubscribedSubredditViewHolder) holder).binding.nameTextViewItemSubscribedSubredditMultiselection.setText(subscribedSubreddits.get(position).getName()); glide.load(subscribedSubreddits.get(position).getIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((SubscribedSubredditViewHolder) holder).binding.iconGifImageViewItemSubscribedSubredditMultiselection); ((SubscribedSubredditViewHolder) holder).binding.checkboxItemSubscribedSubredditMultiselection.setChecked(subscribedSubreddits.get(position).isSelected()); ((SubscribedSubredditViewHolder) holder).binding.checkboxItemSubscribedSubredditMultiselection.setOnClickListener(view -> { if (subscribedSubreddits.get(position).isSelected()) { ((SubscribedSubredditViewHolder) holder).binding.checkboxItemSubscribedSubredditMultiselection.setChecked(false); subscribedSubreddits.get(position).setSelected(false); } else { ((SubscribedSubredditViewHolder) holder).binding.checkboxItemSubscribedSubredditMultiselection.setChecked(true); subscribedSubreddits.get(position).setSelected(true); } }); ((SubscribedSubredditViewHolder) holder).itemView.setOnClickListener(view -> ((SubscribedSubredditViewHolder) holder).binding.checkboxItemSubscribedSubredditMultiselection.performClick()); } } @Override public int getItemCount() { return subscribedSubreddits == null ? 0 : subscribedSubreddits.size(); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof SubscribedSubredditViewHolder) { glide.clear(((SubscribedSubredditViewHolder) holder).binding.iconGifImageViewItemSubscribedSubredditMultiselection); } } public void setSubscribedSubreddits(List subscribedSubreddits, String selectedSubreddits) { this.subscribedSubreddits = SubredditWithSelection.convertSubscribedSubreddits(subscribedSubreddits); Set selectedSet = new HashSet<>(); if (selectedSubreddits != null && !selectedSubreddits.isEmpty()) { for (String name : selectedSubreddits.split(",")) { String trimmed = name.trim(); if (!trimmed.isEmpty()) { selectedSet.add(trimmed); } } } for (SubredditWithSelection s : this.subscribedSubreddits) { s.setSelected(selectedSet.contains(s.getName())); } notifyDataSetChanged(); } public ArrayList getAllSelectedSubreddits() { ArrayList selectedSubreddits = new ArrayList<>(); for (SubredditWithSelection s : subscribedSubreddits) { if (s.isSelected()) { selectedSubreddits.add(s); } } return selectedSubreddits; } class SubscribedSubredditViewHolder extends RecyclerView.ViewHolder { ItemSubscribedSubredditMultiSelectionBinding binding; SubscribedSubredditViewHolder(@NonNull ItemSubscribedSubredditMultiSelectionBinding binding) { super(binding.getRoot()); this.binding = binding; binding.nameTextViewItemSubscribedSubredditMultiselection.setTextColor(primaryTextColor); binding.checkboxItemSubscribedSubredditMultiselection.setButtonTintList(ColorStateList.valueOf(colorAccent)); if (activity.typeface != null) { binding.nameTextViewItemSubscribedSubredditMultiselection.setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/SubscribedSubredditsRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.List; import java.util.concurrent.Executor; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import me.zhanghai.android.fastscroll.PopupTextProvider; import ml.docilealligator.infinityforreddit.thing.FavoriteThing; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemFavoriteThingDividerBinding; import ml.docilealligator.infinityforreddit.databinding.ItemSubscribedThingBinding; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import retrofit2.Retrofit; public class SubscribedSubredditsRecyclerViewAdapter extends RecyclerView.Adapter implements PopupTextProvider { private static final int VIEW_TYPE_FAVORITE_SUBREDDIT_DIVIDER = 0; private static final int VIEW_TYPE_FAVORITE_SUBREDDIT = 1; private static final int VIEW_TYPE_SUBREDDIT_DIVIDER = 2; private static final int VIEW_TYPE_SUBREDDIT = 3; private final BaseActivity mActivity; private final Executor mExecutor; private final Retrofit mOauthRetrofit; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private List mSubscribedSubredditData; private List mFavoriteSubscribedSubredditData; private final RequestManager glide; private ItemClickListener itemClickListener; private final String accessToken; private final String accountName; private String username; private String userIconUrl; private boolean hasClearSelectionRow; private final int primaryTextColor; private final int secondaryTextColor; public SubscribedSubredditsRecyclerViewAdapter(BaseActivity activity, Executor executor, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, CustomThemeWrapper customThemeWrapper, @Nullable String accessToken, @NonNull String accountName) { mActivity = activity; mExecutor = executor; glide = Glide.with(activity); mOauthRetrofit = oauthRetrofit; mRedditDataRoomDatabase = redditDataRoomDatabase; this.accessToken = accessToken; this.accountName = accountName; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); } public SubscribedSubredditsRecyclerViewAdapter(BaseActivity activity, Executor executor, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, CustomThemeWrapper customThemeWrapper, @Nullable String accessToken, @NonNull String accountName, boolean hasClearSelectionRow, ItemClickListener itemClickListener) { this(activity, executor, oauthRetrofit, redditDataRoomDatabase, customThemeWrapper, accessToken, accountName); this.hasClearSelectionRow = hasClearSelectionRow; this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { if (mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) { if (itemClickListener != null && !hasClearSelectionRow) { if (position == 0) { return VIEW_TYPE_SUBREDDIT; } else if (position == 1) { return VIEW_TYPE_FAVORITE_SUBREDDIT_DIVIDER; } else if (position == mFavoriteSubscribedSubredditData.size() + 2) { return VIEW_TYPE_SUBREDDIT_DIVIDER; } else if (position <= mFavoriteSubscribedSubredditData.size() + 1) { return VIEW_TYPE_FAVORITE_SUBREDDIT; } else { return VIEW_TYPE_SUBREDDIT; } } else if (hasClearSelectionRow) { if (position == 0) { return VIEW_TYPE_SUBREDDIT; } else if (position == 1) { return VIEW_TYPE_SUBREDDIT; } else if (position == 2) { return VIEW_TYPE_FAVORITE_SUBREDDIT_DIVIDER; } else if (position == mFavoriteSubscribedSubredditData.size() + 3) { return VIEW_TYPE_SUBREDDIT_DIVIDER; } else if (position <= mFavoriteSubscribedSubredditData.size() + 2) { return VIEW_TYPE_FAVORITE_SUBREDDIT; } else { return VIEW_TYPE_SUBREDDIT; } } else { if (position == 0) { return VIEW_TYPE_FAVORITE_SUBREDDIT_DIVIDER; } else if (position == mFavoriteSubscribedSubredditData.size() + 1) { return VIEW_TYPE_SUBREDDIT_DIVIDER; } else if (position <= mFavoriteSubscribedSubredditData.size()) { return VIEW_TYPE_FAVORITE_SUBREDDIT; } else { return VIEW_TYPE_SUBREDDIT; } } } else { return VIEW_TYPE_SUBREDDIT; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { switch (i) { case VIEW_TYPE_FAVORITE_SUBREDDIT_DIVIDER: return new FavoriteSubredditsDividerViewHolder(ItemFavoriteThingDividerBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); case VIEW_TYPE_FAVORITE_SUBREDDIT: return new FavoriteSubredditViewHolder(ItemSubscribedThingBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); case VIEW_TYPE_SUBREDDIT_DIVIDER: return new AllSubredditsDividerViewHolder(ItemFavoriteThingDividerBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); default: return new SubredditViewHolder(ItemSubscribedThingBinding .inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); } } @Override public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, int i) { if (viewHolder instanceof SubredditViewHolder) { String name; String iconUrl; if (hasClearSelectionRow && viewHolder.getBindingAdapterPosition() == 0) { ((SubredditViewHolder) viewHolder).binding.thingNameTextViewItemSubscribedThing.setText(R.string.all_subreddits); ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setVisibility(View.GONE); viewHolder.itemView.setOnClickListener(view -> itemClickListener.onClick(null, null, false)); return; } else if (itemClickListener != null && !hasClearSelectionRow && viewHolder.getBindingAdapterPosition() == 0) { ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setVisibility(View.GONE); name = username; iconUrl = userIconUrl; viewHolder.itemView.setOnClickListener(view -> itemClickListener.onClick(name, iconUrl, true)); } else if (hasClearSelectionRow && viewHolder.getBindingAdapterPosition() == 1) { ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setVisibility(View.GONE); name = username; iconUrl = userIconUrl; if (itemClickListener != null) { viewHolder.itemView.setOnClickListener(view -> itemClickListener.onClick(name, iconUrl, true)); } } else { int offset; if (itemClickListener != null) { if (hasClearSelectionRow) { offset = (mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) ? mFavoriteSubscribedSubredditData.size() + 4 : 2; } else { offset = (mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) ? mFavoriteSubscribedSubredditData.size() + 3 : 1; } } else { offset = (mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) ? mFavoriteSubscribedSubredditData.size() + 2 : 0; } name = mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).getName(); iconUrl = mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).getIconUrl(); if(mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).isFavorite()) { ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } else { ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setOnClickListener(view -> { if(mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).isFavorite()) { ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).setFavorite(false); FavoriteThing.unfavoriteSubreddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, accessToken, accountName, mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = viewHolder.getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedSubredditData.size() > position) { mSubscribedSubredditData.get(position).setFavorite(false); } ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_unfavorite_failed, Toast.LENGTH_SHORT).show(); int position = viewHolder.getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedSubredditData.size() > position) { mSubscribedSubredditData.get(position).setFavorite(true); } ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } }); } else { ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).setFavorite(true); FavoriteThing.favoriteSubreddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, accessToken, accountName, mSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = viewHolder.getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedSubredditData.size() > position) { mSubscribedSubredditData.get(position).setFavorite(true); } ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_favorite_failed, Toast.LENGTH_SHORT).show(); int position = viewHolder.getBindingAdapterPosition() - offset; if(position >= 0 && mSubscribedSubredditData.size() > position) { mSubscribedSubredditData.get(position).setFavorite(false); } ((SubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } }); } }); if (itemClickListener != null) { viewHolder.itemView.setOnClickListener(view -> itemClickListener.onClick(name, iconUrl, false)); } } if (itemClickListener == null) { viewHolder.itemView.setOnClickListener(view -> { Intent intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, name); mActivity.startActivity(intent); }); } if (iconUrl != null && !iconUrl.equals("")) { glide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((SubredditViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((SubredditViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } ((SubredditViewHolder) viewHolder).binding.thingNameTextViewItemSubscribedThing.setText(name); } else if (viewHolder instanceof FavoriteSubredditViewHolder) { int offset; if (itemClickListener != null) { if (hasClearSelectionRow) { offset = 3; } else { offset = 2; } } else { offset = 1; } String name = mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).getName(); String iconUrl = mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).getIconUrl(); if(mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).isFavorite()) { ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } else { ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setOnClickListener(view -> { if(mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).isFavorite()) { ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).setFavorite(false); FavoriteThing.unfavoriteSubreddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, accessToken, accountName, mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = viewHolder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedSubredditData.size() > position) { mFavoriteSubscribedSubredditData.get(position).setFavorite(false); } ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_unfavorite_failed, Toast.LENGTH_SHORT).show(); int position = viewHolder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedSubredditData.size() > position) { mFavoriteSubscribedSubredditData.get(position).setFavorite(true); } ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } }); } else { ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset).setFavorite(true); FavoriteThing.favoriteSubreddit(mExecutor, new Handler(), mOauthRetrofit, mRedditDataRoomDatabase, accessToken, accountName, mFavoriteSubscribedSubredditData.get(viewHolder.getBindingAdapterPosition() - offset), new FavoriteThing.FavoriteThingListener() { @Override public void success() { int position = viewHolder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedSubredditData.size() > position) { mFavoriteSubscribedSubredditData.get(position).setFavorite(true); } ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_24dp); } @Override public void failed() { Toast.makeText(mActivity, R.string.thing_favorite_failed, Toast.LENGTH_SHORT).show(); int position = viewHolder.getBindingAdapterPosition() - 1; if(position >= 0 && mFavoriteSubscribedSubredditData.size() > position) { mFavoriteSubscribedSubredditData.get(position).setFavorite(false); } ((FavoriteSubredditViewHolder) viewHolder).binding.favoriteImageViewItemSubscribedThing.setImageResource(R.drawable.ic_favorite_border_24dp); } }); } }); if (itemClickListener != null) { viewHolder.itemView.setOnClickListener(view -> itemClickListener.onClick(name, iconUrl, false)); } else { viewHolder.itemView.setOnClickListener(view -> { Intent intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, name); mActivity.startActivity(intent); }); } if (iconUrl != null && !iconUrl.equals("")) { glide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((FavoriteSubredditViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((FavoriteSubredditViewHolder) viewHolder).binding.thingIconGifImageViewItemSubscribedThing); } ((FavoriteSubredditViewHolder) viewHolder).binding.thingNameTextViewItemSubscribedThing.setText(name); } } @Override public int getItemCount() { if (mSubscribedSubredditData != null) { if(mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) { if (itemClickListener != null) { if (hasClearSelectionRow) { return mSubscribedSubredditData.size() > 0 ? mFavoriteSubscribedSubredditData.size() + mSubscribedSubredditData.size() + 4 : 0; } else { return mSubscribedSubredditData.size() > 0 ? mFavoriteSubscribedSubredditData.size() + mSubscribedSubredditData.size() + 3 : 0; } } return mSubscribedSubredditData.size() > 0 ? mFavoriteSubscribedSubredditData.size() + mSubscribedSubredditData.size() + 2 : 0; } if (itemClickListener != null) { if (hasClearSelectionRow) { return mSubscribedSubredditData.size() > 0 ? mSubscribedSubredditData.size() + 2 : 0; } else { return mSubscribedSubredditData.size() > 0 ? mSubscribedSubredditData.size() + 1 : 0; } } return mSubscribedSubredditData.size(); } return 0; } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if(holder instanceof SubredditViewHolder) { glide.clear(((SubredditViewHolder) holder).binding.thingIconGifImageViewItemSubscribedThing); ((SubredditViewHolder) holder).binding.favoriteImageViewItemSubscribedThing.setVisibility(View.VISIBLE); } else if (holder instanceof FavoriteSubredditViewHolder) { glide.clear(((FavoriteSubredditViewHolder) holder).binding.thingIconGifImageViewItemSubscribedThing); } } public void setSubscribedSubreddits(List subscribedSubreddits) { mSubscribedSubredditData = subscribedSubreddits; notifyDataSetChanged(); } public void setFavoriteSubscribedSubreddits(List favoriteSubscribedSubredditData) { mFavoriteSubscribedSubredditData = favoriteSubscribedSubredditData; notifyDataSetChanged(); } public void addUser(String username, String userIconUrl) { this.username = username; this.userIconUrl = userIconUrl; } @NonNull @Override public CharSequence getPopupText(@NonNull View view, int position) { switch (getItemViewType(position)) { case VIEW_TYPE_SUBREDDIT: if (hasClearSelectionRow && position == 0) { return ""; } else if (itemClickListener != null && !hasClearSelectionRow && position == 0) { return ""; } else if (hasClearSelectionRow && position == 1) { return ""; } else { int offset; if (itemClickListener != null) { if (hasClearSelectionRow) { offset = (mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) ? mFavoriteSubscribedSubredditData.size() + 4 : 0; } else { offset = (mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) ? mFavoriteSubscribedSubredditData.size() + 3 : 0; } } else { offset = (mFavoriteSubscribedSubredditData != null && mFavoriteSubscribedSubredditData.size() > 0) ? mFavoriteSubscribedSubredditData.size() + 2 : 0; } return mSubscribedSubredditData.get(position - offset).getName().substring(0, 1).toUpperCase(); } case VIEW_TYPE_FAVORITE_SUBREDDIT: int offset; if (itemClickListener != null) { if (hasClearSelectionRow) { offset = 3; } else { offset = 2; } } else { offset = 1; } return mFavoriteSubscribedSubredditData.get(position - offset).getName().substring(0, 1).toUpperCase(); default: return ""; } } public interface ItemClickListener { void onClick(String name, String iconUrl, boolean subredditIsUser); } class SubredditViewHolder extends RecyclerView.ViewHolder { ItemSubscribedThingBinding binding; SubredditViewHolder(@NonNull ItemSubscribedThingBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.thingNameTextViewItemSubscribedThing.setTypeface(mActivity.typeface); } binding.thingNameTextViewItemSubscribedThing.setTextColor(primaryTextColor); } } class FavoriteSubredditViewHolder extends RecyclerView.ViewHolder { ItemSubscribedThingBinding binding; FavoriteSubredditViewHolder(@NonNull ItemSubscribedThingBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.thingNameTextViewItemSubscribedThing.setTypeface(mActivity.typeface); } binding.thingNameTextViewItemSubscribedThing.setTextColor(primaryTextColor); } } class FavoriteSubredditsDividerViewHolder extends RecyclerView.ViewHolder { ItemFavoriteThingDividerBinding binding; FavoriteSubredditsDividerViewHolder(@NonNull ItemFavoriteThingDividerBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.dividerTextViewItemFavoriteThingDivider.setTypeface(mActivity.typeface); } binding.dividerTextViewItemFavoriteThingDivider.setText(R.string.favorites); binding.dividerTextViewItemFavoriteThingDivider.setTextColor(secondaryTextColor); } } class AllSubredditsDividerViewHolder extends RecyclerView.ViewHolder { ItemFavoriteThingDividerBinding binding; AllSubredditsDividerViewHolder(@NonNull ItemFavoriteThingDividerBinding binding) { super(binding.getRoot()); this.binding = binding; if (mActivity.typeface != null) { binding.dividerTextViewItemFavoriteThingDivider.setTypeface(mActivity.typeface); } binding.dividerTextViewItemFavoriteThingDivider.setText(R.string.all); binding.dividerTextViewItemFavoriteThingDivider.setTextColor(secondaryTextColor); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/TranslationFragmentRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.Intent; import android.net.Uri; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemTranslationContributorBinding; import ml.docilealligator.infinityforreddit.settings.Translation; public class TranslationFragmentRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private final int primaryTextColor; private final int secondaryTextColor; private final ArrayList translationContributors; public TranslationFragmentRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper) { this.activity = activity; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); translationContributors = Translation.getTranslationContributors(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new TranslationContributorViewHolder(ItemTranslationContributorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof TranslationContributorViewHolder) { Translation translation = translationContributors.get(position); if (translation.flagDrawableId < 0) { ((TranslationContributorViewHolder) holder).binding.countryFlagImageViewItemTranslationContributor.setImageDrawable(null); } else { ((TranslationContributorViewHolder) holder).binding.countryFlagImageViewItemTranslationContributor.setImageResource(translation.flagDrawableId); } ((TranslationContributorViewHolder) holder).binding.languageNameTextViewItemTranslationContributor.setText(translation.language); ((TranslationContributorViewHolder) holder).binding.contributorNamesTextViewItemTranslationContributor.setText(translation.contributors); } } @Override public int getItemCount() { return translationContributors.size(); } class TranslationContributorViewHolder extends RecyclerView.ViewHolder { ItemTranslationContributorBinding binding; public TranslationContributorViewHolder(@NonNull ItemTranslationContributorBinding binding) { super(binding.getRoot()); this.binding = binding; if (activity.typeface != null) { binding.languageNameTextViewItemTranslationContributor.setTypeface(activity.typeface); binding.contributorNamesTextViewItemTranslationContributor.setTypeface(activity.typeface); } binding.languageNameTextViewItemTranslationContributor.setTextColor(primaryTextColor); binding.contributorNamesTextViewItemTranslationContributor.setTextColor(secondaryTextColor); itemView.setOnClickListener(view -> { Intent intent = new Intent(activity, LinkResolverActivity.class); intent.setData(Uri.parse("https://poeditor.com/join/project?hash=b2IRyfaJv6")); activity.startActivity(intent); }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/UploadedImagesRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.app.Activity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.activities.BaseActivity; public class UploadedImagesRecyclerViewAdapter extends RecyclerView.Adapter { private BaseActivity activity; private final ArrayList uploadedImages; private final ItemClickListener itemClickListener; public UploadedImagesRecyclerViewAdapter(Activity activity, ArrayList uploadedImages, ItemClickListener itemClickListener) { if (activity instanceof BaseActivity) { this.activity = (BaseActivity) activity; } this.uploadedImages = uploadedImages; this.itemClickListener = itemClickListener; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UploadedImageViewHolder(LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_uploaded_image, parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { ((UploadedImageViewHolder) holder).imageNameTextView.setText(uploadedImages.get(position).imageName); ((UploadedImageViewHolder) holder).imageUrlTextView.setText(uploadedImages.get(position).imageUrlOrKey); } @Override public int getItemCount() { return uploadedImages == null ? 0 : uploadedImages.size(); } private class UploadedImageViewHolder extends RecyclerView.ViewHolder { TextView imageNameTextView; TextView imageUrlTextView; public UploadedImageViewHolder(@NonNull View itemView) { super(itemView); imageNameTextView = itemView.findViewById(R.id.image_name_item_uploaded_image); imageUrlTextView = itemView.findViewById(R.id.image_url_item_uploaded_image); if (activity != null && activity.typeface != null) { imageNameTextView.setTypeface(activity.typeface); imageUrlTextView.setTypeface(activity.typeface); } itemView.setOnClickListener(view -> { itemClickListener.onClick(uploadedImages.get(getBindingAdapterPosition())); }); } } public interface ItemClickListener { void onClick(UploadedImage uploadedImage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/UserFlairRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.user.UserFlair; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemUserFlairBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class UserFlairRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private final CustomThemeWrapper customThemeWrapper; private final ArrayList userFlairs; private final ItemClickListener itemClickListener; public UserFlairRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper, ArrayList userFlairs, ItemClickListener itemClickListener) { this.activity = activity; this.customThemeWrapper = customThemeWrapper; this.userFlairs = userFlairs; this.itemClickListener = itemClickListener; } public interface ItemClickListener { void onClick(UserFlair userFlair, boolean editUserFlair); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserFlairViewHolder(ItemUserFlairBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof UserFlairViewHolder) { if (position == 0) { ((UserFlairViewHolder) holder).binding.userFlairHtmlTextViewItemUserFlair.setText(R.string.clear_user_flair); ((UserFlairViewHolder) holder).binding.editUserFlairImageViewItemUserFlair.setVisibility(View.GONE); } else { UserFlair userFlair = userFlairs.get(holder.getBindingAdapterPosition() - 1); if (userFlair.getHtmlText() == null || userFlair.getHtmlText().equals("")) { ((UserFlairViewHolder) holder).binding.userFlairHtmlTextViewItemUserFlair.setText(userFlair.getText()); } else { Utils.setHTMLWithImageToTextView(((UserFlairViewHolder) holder).binding.userFlairHtmlTextViewItemUserFlair, userFlair.getHtmlText(), true); } if (userFlair.isEditable()) { ((UserFlairViewHolder) holder).binding.editUserFlairImageViewItemUserFlair.setVisibility(View.VISIBLE); } else { ((UserFlairViewHolder) holder).binding.editUserFlairImageViewItemUserFlair.setVisibility(View.GONE); } } } } @Override public int getItemCount() { return userFlairs == null ? 1 : userFlairs.size() + 1; } class UserFlairViewHolder extends RecyclerView.ViewHolder { ItemUserFlairBinding binding; public UserFlairViewHolder(@NonNull ItemUserFlairBinding binding) { super(binding.getRoot()); this.binding = binding; binding.userFlairHtmlTextViewItemUserFlair.setTextColor(customThemeWrapper.getPrimaryTextColor()); binding.editUserFlairImageViewItemUserFlair.setColorFilter(customThemeWrapper.getPrimaryTextColor(), android.graphics.PorterDuff.Mode.SRC_IN); if (activity.typeface != null) { binding.userFlairHtmlTextViewItemUserFlair.setTypeface(activity.typeface); } itemView.setOnClickListener(view -> { if (getBindingAdapterPosition() == 0) { itemClickListener.onClick(null, false); } else { itemClickListener.onClick(userFlairs.get(getBindingAdapterPosition() - 1), false); } }); binding.editUserFlairImageViewItemUserFlair.setOnClickListener(view -> { itemClickListener.onClick(userFlairs.get(getBindingAdapterPosition() - 1), true); }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/UserListingRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.paging.PagedListAdapter; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.concurrent.Executor; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.asynctasks.CheckIsFollowingUser; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemFooterErrorBinding; import ml.docilealligator.infinityforreddit.databinding.ItemFooterLoadingBinding; import ml.docilealligator.infinityforreddit.databinding.ItemUserListingBinding; import ml.docilealligator.infinityforreddit.user.UserData; import ml.docilealligator.infinityforreddit.user.UserFollowing; import retrofit2.Retrofit; public class UserListingRecyclerViewAdapter extends PagedListAdapter { private static final int VIEW_TYPE_DATA = 0; private static final int VIEW_TYPE_ERROR = 1; private static final int VIEW_TYPE_LOADING = 2; private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(@NonNull UserData oldItem, @NonNull UserData newItem) { return oldItem.getName().equals(newItem.getName()); } @Override public boolean areContentsTheSame(@NonNull UserData oldItem, @NonNull UserData newItem) { return true; } }; private final RequestManager glide; private final BaseActivity activity; private final Executor executor; private final Retrofit oauthRetrofit; private final Retrofit retrofit; private final String accessToken; private final String accountName; private final RedditDataRoomDatabase redditDataRoomDatabase; private final boolean isMultiSelection; private final int primaryTextColor; private final int buttonTextColor; private final int colorPrimaryLightTheme; private final int colorAccent; private final int unsubscribedColor; private NetworkState networkState; private final Callback callback; public UserListingRecyclerViewAdapter(BaseActivity activity, Executor executor, Retrofit oauthRetrofit, Retrofit retrofit, CustomThemeWrapper customThemeWrapper, @Nullable String accessToken, @NonNull String accountName, RedditDataRoomDatabase redditDataRoomDatabase, boolean isMultiSelection, Callback callback) { super(DIFF_CALLBACK); this.activity = activity; this.executor = executor; this.oauthRetrofit = oauthRetrofit; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.redditDataRoomDatabase = redditDataRoomDatabase; this.isMultiSelection = isMultiSelection; this.callback = callback; glide = Glide.with(activity); primaryTextColor = customThemeWrapper.getPrimaryTextColor(); buttonTextColor = customThemeWrapper.getButtonTextColor(); colorPrimaryLightTheme = customThemeWrapper.getColorPrimaryLightTheme(); colorAccent = customThemeWrapper.getColorAccent(); unsubscribedColor = customThemeWrapper.getUnsubscribed(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_DATA) { return new DataViewHolder(ItemUserListingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else if (viewType == VIEW_TYPE_ERROR) { return new UserListingRecyclerViewAdapter.ErrorViewHolder(ItemFooterErrorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new UserListingRecyclerViewAdapter.LoadingViewHolder(ItemFooterLoadingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof DataViewHolder) { UserData userData = getItem(position); if (userData != null) { ((DataViewHolder) holder).itemView.setOnClickListener(view -> { if (isMultiSelection) { ((DataViewHolder) holder).binding.checkboxItemUserListing.performClick(); } else { callback.userSelected(userData.getName(), userData.getIconUrl()); } }); if (!userData.getIconUrl().equals("")) { glide.load(userData.getIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((DataViewHolder) holder).binding.userIconGifImageViewItemUserListing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((DataViewHolder) holder).binding.userIconGifImageViewItemUserListing); } ((DataViewHolder) holder).binding.userNameTextViewItemUserListing.setText(userData.getName()); if (!isMultiSelection) { CheckIsFollowingUser.checkIsFollowingUser(executor, activity.mHandler, redditDataRoomDatabase, userData.getName(), accountName, new CheckIsFollowingUser.CheckIsFollowingUserListener() { @Override public void isSubscribed() { ((DataViewHolder) holder).binding.subscribeImageViewItemUserListing.setVisibility(View.GONE); } @Override public void isNotSubscribed() { ((DataViewHolder) holder).binding.subscribeImageViewItemUserListing.setVisibility(View.VISIBLE); ((DataViewHolder) holder).binding.subscribeImageViewItemUserListing.setOnClickListener(view -> { UserFollowing.followUser(executor, activity.mHandler, oauthRetrofit, retrofit, accessToken, userData.getName(), accountName, redditDataRoomDatabase, new UserFollowing.UserFollowingListener() { @Override public void onUserFollowingSuccess() { ((DataViewHolder) holder).binding.subscribeImageViewItemUserListing.setVisibility(View.GONE); Toast.makeText(activity, R.string.followed, Toast.LENGTH_SHORT).show(); } @Override public void onUserFollowingFail() { Toast.makeText(activity, R.string.follow_failed, Toast.LENGTH_SHORT).show(); } }); }); } }); } else { ((DataViewHolder) holder).binding.checkboxItemUserListing.setOnCheckedChangeListener((compoundButton, b) -> userData.setSelected(b)); } } } } @Override public int getItemViewType(int position) { // Reached at the end if (hasExtraRow() && position == getItemCount() - 1) { if (networkState.getStatus() == NetworkState.Status.LOADING) { return VIEW_TYPE_LOADING; } else { return VIEW_TYPE_ERROR; } } else { return VIEW_TYPE_DATA; } } @Override public int getItemCount() { if (hasExtraRow()) { return super.getItemCount() + 1; } return super.getItemCount(); } private boolean hasExtraRow() { return networkState != null && networkState.getStatus() != NetworkState.Status.SUCCESS; } public void setNetworkState(NetworkState newNetworkState) { NetworkState previousState = this.networkState; boolean previousExtraRow = hasExtraRow(); this.networkState = newNetworkState; boolean newExtraRow = hasExtraRow(); if (previousExtraRow != newExtraRow) { if (previousExtraRow) { notifyItemRemoved(super.getItemCount()); } else { notifyItemInserted(super.getItemCount()); } } else if (newExtraRow && !previousState.equals(newNetworkState)) { notifyItemChanged(getItemCount() - 1); } } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof DataViewHolder) { glide.clear(((DataViewHolder) holder).binding.userIconGifImageViewItemUserListing); ((DataViewHolder) holder).binding.subscribeImageViewItemUserListing.setVisibility(View.GONE); } } public interface Callback { void retryLoadingMore(); void userSelected(String username, String iconUrl); } class DataViewHolder extends RecyclerView.ViewHolder { ItemUserListingBinding binding; DataViewHolder(@NonNull ItemUserListingBinding binding) { super(binding.getRoot()); this.binding = binding; binding.userNameTextViewItemUserListing.setTextColor(primaryTextColor); binding.subscribeImageViewItemUserListing.setColorFilter(unsubscribedColor, android.graphics.PorterDuff.Mode.SRC_IN); if (activity.typeface != null) { binding.userNameTextViewItemUserListing.setTypeface(activity.typeface); } if (isMultiSelection) { binding.checkboxItemUserListing.setVisibility(View.VISIBLE); } } } class ErrorViewHolder extends RecyclerView.ViewHolder { ItemFooterErrorBinding binding; ErrorViewHolder(@NonNull ItemFooterErrorBinding binding) { super(binding.getRoot()); this.binding = binding; binding.retryButtonItemFooterError.setOnClickListener(view -> callback.retryLoadingMore()); binding.errorTextViewItemFooterError.setText(R.string.load_comments_failed); binding.errorTextViewItemFooterError.setTextColor(primaryTextColor); binding.retryButtonItemFooterError.setTextColor(buttonTextColor); binding.retryButtonItemFooterError.setBackgroundTintList(ColorStateList.valueOf(colorPrimaryLightTheme)); if (activity.typeface != null) { binding.retryButtonItemFooterError.setTypeface(activity.typeface); binding.errorTextViewItemFooterError.setTypeface(activity.typeface); } } } class LoadingViewHolder extends RecyclerView.ViewHolder { ItemFooterLoadingBinding binding; LoadingViewHolder(@NonNull ItemFooterLoadingBinding binding) { super(binding.getRoot()); this.binding = binding; binding.progressBarItemFooterLoading.setIndicatorColor(colorAccent); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/UserMultiselectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters; import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemSubscribedUserMultiSelectionBinding; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.user.UserWithSelection; public class UserMultiselectionRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity activity; private ArrayList subscribedUsers; private final RequestManager glide; private final int primaryTextColor; private final int colorAccent; public UserMultiselectionRecyclerViewAdapter(BaseActivity activity, CustomThemeWrapper customThemeWrapper) { this.activity = activity; glide = Glide.with(activity); primaryTextColor = customThemeWrapper.getPrimaryTextColor(); colorAccent = customThemeWrapper.getColorAccent(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new SubscribedUserViewHolder(ItemSubscribedUserMultiSelectionBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof SubscribedUserViewHolder) { ((SubscribedUserViewHolder) holder).binding.nameTextViewItemSubscribedUserMultiselection.setText(subscribedUsers.get(position).getName()); glide.load(subscribedUsers.get(position).getIconUrl()) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((SubscribedUserViewHolder) holder).binding.iconGifImageViewItemSubscribedUserMultiselection); ((SubscribedUserViewHolder) holder).binding.checkboxItemSubscribedUserMultiselection.setChecked(subscribedUsers.get(position).isSelected()); ((SubscribedUserViewHolder) holder).binding.checkboxItemSubscribedUserMultiselection.setOnClickListener(view -> { if (subscribedUsers.get(position).isSelected()) { ((SubscribedUserViewHolder) holder).binding.checkboxItemSubscribedUserMultiselection.setChecked(false); subscribedUsers.get(position).setSelected(false); } else { ((SubscribedUserViewHolder) holder).binding.checkboxItemSubscribedUserMultiselection.setChecked(true); subscribedUsers.get(position).setSelected(true); } }); ((SubscribedUserViewHolder) holder).itemView.setOnClickListener(view -> ((SubscribedUserViewHolder) holder).binding.checkboxItemSubscribedUserMultiselection.performClick()); } } @Override public int getItemCount() { return subscribedUsers == null ? 0 : subscribedUsers.size(); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof SubscribedUserViewHolder) { glide.clear(((SubscribedUserViewHolder) holder).binding.iconGifImageViewItemSubscribedUserMultiselection); } } public void setSubscribedUsers(List subscribedUsers, String selectedUsers) { this.subscribedUsers = UserWithSelection.convertSubscribedUsers(subscribedUsers); Set selectedSet = new HashSet<>(); if (selectedUsers != null && !selectedUsers.isEmpty()) { for (String name : selectedUsers.split(",")) { String trimmed = name.trim(); if (!trimmed.isEmpty()) { selectedSet.add(trimmed); } } } for (UserWithSelection u : this.subscribedUsers) { u.setSelected(selectedSet.contains(u.getName())); } notifyDataSetChanged(); } public ArrayList getAllSelectedUsers() { ArrayList selectedUsers = new ArrayList<>(); for (UserWithSelection s : subscribedUsers) { if (s.isSelected()) { selectedUsers.add(s.getName()); } } return selectedUsers; } class SubscribedUserViewHolder extends RecyclerView.ViewHolder { ItemSubscribedUserMultiSelectionBinding binding; SubscribedUserViewHolder(@NonNull ItemSubscribedUserMultiSelectionBinding binding) { super(binding.getRoot()); this.binding = binding; binding.nameTextViewItemSubscribedUserMultiselection.setTextColor(primaryTextColor); binding.checkboxItemSubscribedUserMultiselection.setButtonTintList(ColorStateList.valueOf(colorAccent)); if (activity.typeface != null) { binding.nameTextViewItemSubscribedUserMultiselection.setTypeface(activity.typeface); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/AccountManagementSectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import java.util.List; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerAccountBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuItemBinding; public class AccountManagementSectionRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_ACCOUNT = 1; private static final int VIEW_TYPE_MENU_ITEM = 2; private final BaseActivity baseActivity; private ArrayList accounts; private final RequestManager glide; private final int primaryTextColor; private final int primaryIconColor; private final boolean isLoggedIn; private final NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener; public AccountManagementSectionRecyclerViewAdapter(BaseActivity baseActivity, CustomThemeWrapper customThemeWrapper, RequestManager glide, boolean isLoggedIn, NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener) { this.baseActivity = baseActivity; this.glide = glide; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); primaryIconColor = customThemeWrapper.getPrimaryIconColor(); this.isLoggedIn = isLoggedIn; this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { if (position >= accounts.size()) { return VIEW_TYPE_MENU_ITEM; } else { return VIEW_TYPE_ACCOUNT; } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_ACCOUNT) { return new AccountViewHolder(ItemNavDrawerAccountBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } return new MenuItemViewHolder(ItemNavDrawerMenuItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof AccountViewHolder) { glide.load(accounts.get(position).getProfileImageUrl()) .error(glide.load(R.drawable.subreddit_default_icon)) .transform(new RoundedCornersTransformation(128, 0)) .into(((AccountViewHolder) holder).binding.profileImageItemAccount); ((AccountViewHolder) holder).binding.usernameTextViewItemAccount.setText(accounts.get(position).getAccountName()); } else if (holder instanceof MenuItemViewHolder) { int stringId = 0; int drawableId = 0; if (isLoggedIn) { int offset = accounts == null ? 0 : accounts.size(); if (position == offset) { stringId = R.string.add_account; drawableId = R.drawable.ic_add_circle_outline_day_night_24dp; } else if (position == offset + 1) { stringId = R.string.anonymous_account; drawableId = R.drawable.ic_anonymous_day_night_24dp; } else if (position == offset + 2) { stringId = R.string.log_out; drawableId = R.drawable.ic_log_out_day_night_24dp; } } else { stringId = R.string.add_account; drawableId = R.drawable.ic_add_circle_outline_day_night_24dp; } if (stringId != 0) { ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(stringId); ((MenuItemViewHolder) holder).binding.imageViewItemNavDrawerMenuItem.setImageDrawable(ContextCompat.getDrawable(baseActivity, drawableId)); } int finalStringId = stringId; holder.itemView.setOnClickListener(view -> itemClickListener.onMenuClick(finalStringId)); holder.itemView.setOnLongClickListener(view -> { itemClickListener.onMenuLongClick(finalStringId); return true; }); } } @Override public int getItemCount() { if (isLoggedIn) { if (accounts != null && !accounts.isEmpty()) { return 3 + accounts.size(); } else { return 3; } } else { if (accounts != null && !accounts.isEmpty()) { return 1 + accounts.size(); } else { return 1; } } } public void changeAccountsDataset(List accounts) { this.accounts = (ArrayList) accounts; notifyDataSetChanged(); } class AccountViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerAccountBinding binding; AccountViewHolder(@NonNull ItemNavDrawerAccountBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.usernameTextViewItemAccount.setTypeface(baseActivity.typeface); } binding.usernameTextViewItemAccount.setTextColor(primaryTextColor); itemView.setOnClickListener(view -> itemClickListener.onAccountClick(accounts.get(getBindingAdapterPosition()).getAccountName())); itemView.setOnLongClickListener(view -> { itemClickListener.onAccountLongClick(accounts.get(getBindingAdapterPosition()).getAccountName()); return true; }); } } class MenuItemViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuItemBinding binding; MenuItemViewHolder(@NonNull ItemNavDrawerMenuItemBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.textViewItemNavDrawerMenuItem.setTypeface(baseActivity.typeface); } binding.textViewItemNavDrawerMenuItem.setTextColor(primaryTextColor); binding.imageViewItemNavDrawerMenuItem.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/AccountSectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.content.Intent; import android.content.SharedPreferences; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.InboxActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuGroupTitleBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuItemBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class AccountSectionRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MENU_GROUP_TITLE = 1; private static final int VIEW_TYPE_MENU_ITEM = 2; private static final int ACCOUNT_SECTION_ITEMS = 5; private static final int ANONYMOUS_ACCOUNT_SECTION_ITEMS = 3; private final BaseActivity baseActivity; private int inboxCount; private final int primaryTextColor; private final int secondaryTextColor; private final int primaryIconColor; private boolean collapseAccountSection; private final boolean isLoggedIn; private final NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener; public AccountSectionRecyclerViewAdapter(BaseActivity baseActivity, CustomThemeWrapper customThemeWrapper, SharedPreferences navigationDrawerSharedPreferences, boolean isLoggedIn, NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener) { this.baseActivity = baseActivity; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); primaryIconColor = customThemeWrapper.getPrimaryIconColor(); collapseAccountSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.COLLAPSE_ACCOUNT_SECTION, false); this.isLoggedIn = isLoggedIn; this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { return position == 0 ? VIEW_TYPE_MENU_GROUP_TITLE : VIEW_TYPE_MENU_ITEM; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MENU_GROUP_TITLE) { return new MenuGroupTitleViewHolder(ItemNavDrawerMenuGroupTitleBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new MenuItemViewHolder(ItemNavDrawerMenuItemBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MenuGroupTitleViewHolder) { ((MenuGroupTitleViewHolder) holder).binding.titleTextViewItemNavDrawerMenuGroupTitle.setText(R.string.label_account); if (collapseAccountSection) { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24dp); } else { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24dp); } holder.itemView.setOnClickListener(view -> { if (collapseAccountSection) { collapseAccountSection = !collapseAccountSection; notifyItemRangeInserted(holder.getBindingAdapterPosition() + 1, isLoggedIn ? ACCOUNT_SECTION_ITEMS : ANONYMOUS_ACCOUNT_SECTION_ITEMS); } else { collapseAccountSection = !collapseAccountSection; notifyItemRangeRemoved(holder.getBindingAdapterPosition() + 1, isLoggedIn ? ACCOUNT_SECTION_ITEMS : ANONYMOUS_ACCOUNT_SECTION_ITEMS); } notifyItemChanged(holder.getBindingAdapterPosition()); }); } else if (holder instanceof MenuItemViewHolder) { int stringId = 0; int drawableId = 0; boolean setOnClickListener = true; if (isLoggedIn) { switch (position) { case 1: stringId = R.string.profile; drawableId = R.drawable.ic_account_circle_day_night_24dp; break; case 2: stringId = R.string.subscriptions; drawableId = R.drawable.ic_subscriptions_bottom_app_bar_day_night_24dp; break; case 3: stringId = R.string.multi_reddit; drawableId = R.drawable.ic_multi_reddit_day_night_24dp; break; case 4: setOnClickListener = false; if (inboxCount > 0) { ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(baseActivity.getString(R.string.inbox_with_count, inboxCount)); } else { ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(R.string.inbox); } ((MenuItemViewHolder) holder).binding.imageViewItemNavDrawerMenuItem.setImageDrawable(ContextCompat.getDrawable(baseActivity, R.drawable.ic_inbox_day_night_24dp)); holder.itemView.setOnClickListener(view -> { Intent intent = new Intent(baseActivity, InboxActivity.class); baseActivity.startActivity(intent); }); break; default: stringId = R.string.history; drawableId = R.drawable.ic_history_day_night_24dp; } } else { switch (position) { case 1: stringId = R.string.subscriptions; drawableId = R.drawable.ic_subscriptions_bottom_app_bar_day_night_24dp; break; case 2: stringId = R.string.multi_reddit; drawableId = R.drawable.ic_multi_reddit_day_night_24dp; break; default: stringId = R.string.history; drawableId = R.drawable.ic_history_day_night_24dp; } } if (stringId != 0) { ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(stringId); ((MenuItemViewHolder) holder).binding.imageViewItemNavDrawerMenuItem.setImageDrawable(ContextCompat.getDrawable(baseActivity, drawableId)); } if (setOnClickListener) { int finalStringId = stringId; holder.itemView.setOnClickListener(view -> itemClickListener.onMenuClick(finalStringId)); } } } @Override public int getItemCount() { return collapseAccountSection ? 1 : (isLoggedIn ? ACCOUNT_SECTION_ITEMS + 1 : ANONYMOUS_ACCOUNT_SECTION_ITEMS + 1); } public void setInboxCount(int inboxCount) { if (inboxCount < 0) { this.inboxCount = Math.max(0, this.inboxCount + inboxCount); } else { this.inboxCount = inboxCount; } notifyDataSetChanged(); } class MenuGroupTitleViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuGroupTitleBinding binding; MenuGroupTitleViewHolder(@NonNull ItemNavDrawerMenuGroupTitleBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.titleTextViewItemNavDrawerMenuGroupTitle.setTypeface(baseActivity.typeface); } binding.titleTextViewItemNavDrawerMenuGroupTitle.setTextColor(secondaryTextColor); binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setColorFilter(secondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); } } class MenuItemViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuItemBinding binding; MenuItemViewHolder(@NonNull ItemNavDrawerMenuItemBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.textViewItemNavDrawerMenuItem.setTypeface(baseActivity.typeface); } binding.textViewItemNavDrawerMenuItem.setTextColor(primaryTextColor); binding.imageViewItemNavDrawerMenuItem.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/FavoriteSubscribedSubredditsSectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.content.SharedPreferences; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import java.util.List; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuGroupTitleBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerSubscribedThingBinding; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class FavoriteSubscribedSubredditsSectionRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MENU_GROUP_TITLE = 1; private static final int VIEW_TYPE_FAVORITE_SUBSCRIBED_SUBREDDIT = 2; private final BaseActivity baseActivity; private final RequestManager glide; private final int primaryTextColor; private final int secondaryTextColor; private boolean collapseFavoriteSubredditsSection; private final boolean hideFavoriteSubredditsSection; private final NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener; private ArrayList favoriteSubscribedSubreddits = new ArrayList<>(); public FavoriteSubscribedSubredditsSectionRecyclerViewAdapter(BaseActivity baseActivity, RequestManager glide, CustomThemeWrapper customThemeWrapper, SharedPreferences navigationDrawerSharedPreferences, NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener) { this.baseActivity = baseActivity; this.glide = glide; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); collapseFavoriteSubredditsSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.COLLAPSE_FAVORITE_SUBREDDITS_SECTION, false); hideFavoriteSubredditsSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_FAVORITE_SUBREDDITS_SECTION, false); this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { return position == 0 ? VIEW_TYPE_MENU_GROUP_TITLE : VIEW_TYPE_FAVORITE_SUBSCRIBED_SUBREDDIT; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MENU_GROUP_TITLE) { return new MenuGroupTitleViewHolder(ItemNavDrawerMenuGroupTitleBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new FavoriteSubscribedThingViewHolder(ItemNavDrawerSubscribedThingBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MenuGroupTitleViewHolder) { ((MenuGroupTitleViewHolder) holder).binding.titleTextViewItemNavDrawerMenuGroupTitle.setText(R.string.favorites); if (collapseFavoriteSubredditsSection) { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24dp); } else { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24dp); } holder.itemView.setOnClickListener(view -> { if (collapseFavoriteSubredditsSection) { collapseFavoriteSubredditsSection = !collapseFavoriteSubredditsSection; notifyItemRangeInserted(holder.getBindingAdapterPosition() + 1, favoriteSubscribedSubreddits.size()); } else { collapseFavoriteSubredditsSection = !collapseFavoriteSubredditsSection; notifyItemRangeRemoved(holder.getBindingAdapterPosition() + 1, favoriteSubscribedSubreddits.size()); } notifyItemChanged(holder.getBindingAdapterPosition()); }); } else if (holder instanceof FavoriteSubscribedThingViewHolder) { SubscribedSubredditData subreddit = favoriteSubscribedSubreddits.get(position - 1); String subredditName = subreddit.getName(); String iconUrl = subreddit.getIconUrl(); ((FavoriteSubscribedThingViewHolder) holder).binding.thingNameTextViewItemNavDrawerSubscribedThing.setText(subredditName); if (iconUrl != null && !iconUrl.equals("")) { glide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((FavoriteSubscribedThingViewHolder) holder).binding.thingIconGifImageViewItemNavDrawerSubscribedThing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((FavoriteSubscribedThingViewHolder) holder).binding.thingIconGifImageViewItemNavDrawerSubscribedThing); } holder.itemView.setOnClickListener(view -> { itemClickListener.onSubscribedSubredditClick(subredditName); }); } } @Override public int getItemCount() { if (hideFavoriteSubredditsSection) { return 0; } return favoriteSubscribedSubreddits.isEmpty() ? 0 : (collapseFavoriteSubredditsSection ? 1 : favoriteSubscribedSubreddits.size() + 1); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof FavoriteSubscribedThingViewHolder) { glide.clear(((FavoriteSubscribedThingViewHolder) holder).binding.thingIconGifImageViewItemNavDrawerSubscribedThing); } } public void setFavoriteSubscribedSubreddits(List favoriteSubscribedSubreddits) { this.favoriteSubscribedSubreddits = (ArrayList) favoriteSubscribedSubreddits; notifyDataSetChanged(); } class MenuGroupTitleViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuGroupTitleBinding binding; MenuGroupTitleViewHolder(@NonNull ItemNavDrawerMenuGroupTitleBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.titleTextViewItemNavDrawerMenuGroupTitle.setTypeface(baseActivity.typeface); } binding.titleTextViewItemNavDrawerMenuGroupTitle.setTextColor(secondaryTextColor); binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setColorFilter(secondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); } } class FavoriteSubscribedThingViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerSubscribedThingBinding binding; FavoriteSubscribedThingViewHolder(@NonNull ItemNavDrawerSubscribedThingBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.thingNameTextViewItemNavDrawerSubscribedThing.setTypeface(baseActivity.typeface); } binding.thingNameTextViewItemNavDrawerSubscribedThing.setTextColor(primaryTextColor); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/HeaderSectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import android.content.SharedPreferences; import android.content.res.Resources; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestManager; import java.util.concurrent.Executor; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.NavHeaderMainBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class HeaderSectionRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity baseActivity; private final CustomThemeWrapper customThemeWrapper; private final Resources resources; private final RequestManager glide; private final String accountName; private String profileImageUrl; private String bannerImageUrl; private int karma; private boolean requireAuthToAccountSection; private boolean showAvatarOnTheRightInTheNavigationDrawer; private final boolean isLoggedIn; private boolean isInMainPage = true; private final PageToggle pageToggle; private boolean hideKarma; public HeaderSectionRecyclerViewAdapter(BaseActivity baseActivity, CustomThemeWrapper customThemeWrapper, RequestManager glide, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences navigationDrawerSharedPreferences, SharedPreferences securitySharedPreferences, PageToggle pageToggle) { this.baseActivity = baseActivity; this.customThemeWrapper = customThemeWrapper; resources = baseActivity.getResources(); this.glide = glide; this.accountName = accountName; isLoggedIn = !accountName.equals(Account.ANONYMOUS_ACCOUNT); this.pageToggle = pageToggle; requireAuthToAccountSection = securitySharedPreferences.getBoolean(SharedPreferencesUtils.REQUIRE_AUTHENTICATION_TO_GO_TO_ACCOUNT_SECTION_IN_NAVIGATION_DRAWER, false); showAvatarOnTheRightInTheNavigationDrawer = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_AVATAR_ON_THE_RIGHT, false); showAvatarOnTheRightInTheNavigationDrawer = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_AVATAR_ON_THE_RIGHT, false); this.hideKarma = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_ACCOUNT_KARMA_NAV_BAR, false); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new NavHeaderViewHolder(NavHeaderMainBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof NavHeaderViewHolder) { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ((NavHeaderViewHolder) holder).binding.profileImageViewNavHeaderMain.getLayoutParams(); if (showAvatarOnTheRightInTheNavigationDrawer) { params.addRule(RelativeLayout.ALIGN_PARENT_END); } else { params.removeRule(RelativeLayout.ALIGN_PARENT_END); } ((NavHeaderViewHolder) holder).binding.profileImageViewNavHeaderMain.setLayoutParams(params); if (isLoggedIn) { if (hideKarma) { int karmaTextHeight = ((NavHeaderViewHolder) holder).binding.karmaTextViewNavHeaderMain.getHeight(); ((NavHeaderViewHolder) holder).binding.karmaTextViewNavHeaderMain.setVisibility(View.GONE); ((NavHeaderViewHolder) holder).binding.nameTextViewNavHeaderMain.setTranslationY(karmaTextHeight / 2); } else { ((NavHeaderViewHolder) holder).binding.karmaTextViewNavHeaderMain.setVisibility(View.VISIBLE); ((NavHeaderViewHolder) holder).binding.karmaTextViewNavHeaderMain.setText(baseActivity.getString(R.string.karma_info, karma)); ((NavHeaderViewHolder) holder).binding.nameTextViewNavHeaderMain.setTranslationY(0); } ((NavHeaderViewHolder) holder).binding.nameTextViewNavHeaderMain.setText(accountName); if (profileImageUrl != null && !profileImageUrl.equals("")) { glide.load(profileImageUrl) .transform(new RoundedCornersTransformation(144, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(144, 0))) .into(((NavHeaderViewHolder) holder).binding.profileImageViewNavHeaderMain); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(144, 0)) .into(((NavHeaderViewHolder) holder).binding.profileImageViewNavHeaderMain); } if (bannerImageUrl != null && !bannerImageUrl.equals("")) { glide.load(bannerImageUrl).into(((NavHeaderViewHolder) holder).binding.bannerImageViewNavHeaderMain); } } else { ((NavHeaderViewHolder) holder).binding.karmaTextViewNavHeaderMain.setText(R.string.press_here_to_login); ((NavHeaderViewHolder) holder).binding.nameTextViewNavHeaderMain.setText(R.string.anonymous_account); glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(144, 0)) .into(((NavHeaderViewHolder) holder).binding.profileImageViewNavHeaderMain); } if (isInMainPage) { ((NavHeaderViewHolder) holder).binding.accountSwitcherImageViewNavHeaderMain.setImageDrawable(resources.getDrawable(R.drawable.ic_baseline_arrow_drop_down_24dp)); } else { ((NavHeaderViewHolder) holder).binding.accountSwitcherImageViewNavHeaderMain.setImageDrawable(resources.getDrawable(R.drawable.ic_baseline_arrow_drop_up_24dp)); } holder.itemView.setOnClickListener(view -> { if (isInMainPage) { if (requireAuthToAccountSection) { BiometricManager biometricManager = BiometricManager.from(baseActivity); if (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) { Executor executor = ContextCompat.getMainExecutor(baseActivity); BiometricPrompt biometricPrompt = new BiometricPrompt(baseActivity, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); pageToggle.openAccountManagement(); openAccountManagement(((NavHeaderViewHolder) holder).binding.accountSwitcherImageViewNavHeaderMain); } }); BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle(baseActivity.getString(R.string.unlock_account_section)) .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) .build(); biometricPrompt.authenticate(promptInfo); } else { pageToggle.openAccountManagement(); openAccountManagement(((NavHeaderViewHolder) holder).binding.accountSwitcherImageViewNavHeaderMain); } } else { pageToggle.openAccountManagement(); openAccountManagement(((NavHeaderViewHolder) holder).binding.accountSwitcherImageViewNavHeaderMain); } } else { ((NavHeaderViewHolder) holder).binding.accountSwitcherImageViewNavHeaderMain.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_baseline_arrow_drop_down_24dp, null)); pageToggle.closeAccountManagement(); closeAccountManagement(false); } }); } } @Override public int getItemCount() { return 1; } private void openAccountManagement(ImageView dropIconImageView) { dropIconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_baseline_arrow_drop_up_24dp, null)); isInMainPage = false; } public void closeAccountManagement(boolean notifyItemChanged) { isInMainPage = true; if (notifyItemChanged) { notifyItemChanged(0); } } public void updateAccountInfo(String profileImageUrl, String bannerImageUrl, int karma) { this.profileImageUrl = profileImageUrl; this.bannerImageUrl = bannerImageUrl; this.karma = karma; notifyItemChanged(0); } public void setRequireAuthToAccountSection(boolean requireAuthToAccountSection) { this.requireAuthToAccountSection = requireAuthToAccountSection; } public void setShowAvatarOnTheRightInTheNavigationDrawer(boolean showAvatarOnTheRightInTheNavigationDrawer) { this.showAvatarOnTheRightInTheNavigationDrawer = showAvatarOnTheRightInTheNavigationDrawer; } public void setHideKarma(boolean hideKarma) { this.hideKarma = hideKarma; notifyItemChanged(0); } class NavHeaderViewHolder extends RecyclerView.ViewHolder { NavHeaderMainBinding binding; NavHeaderViewHolder(@NonNull NavHeaderMainBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.nameTextViewNavHeaderMain.setTypeface(baseActivity.typeface); binding.karmaTextViewNavHeaderMain.setTypeface(baseActivity.typeface); } itemView.setBackgroundColor(customThemeWrapper.getColorPrimary()); binding.nameTextViewNavHeaderMain.setTextColor(customThemeWrapper.getToolbarPrimaryTextAndIconColor()); binding.karmaTextViewNavHeaderMain.setTextColor(customThemeWrapper.getToolbarSecondaryTextColor()); binding.accountSwitcherImageViewNavHeaderMain.setColorFilter(customThemeWrapper.getToolbarPrimaryTextAndIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } } public interface PageToggle { void openAccountManagement(); void closeAccountManagement(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/NavigationDrawerRecyclerViewMergedAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.recyclerview.widget.ConcatAdapter; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.List; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; public class NavigationDrawerRecyclerViewMergedAdapter { private final HeaderSectionRecyclerViewAdapter headerSectionRecyclerViewAdapter; private final AccountSectionRecyclerViewAdapter accountSectionRecyclerViewAdapter; private final RedditSectionRecyclerViewAdapter redditSectionRecyclerViewAdapter; private final PostSectionRecyclerViewAdapter postSectionRecyclerViewAdapter; private final PreferenceSectionRecyclerViewAdapter preferenceSectionRecyclerViewAdapter; private final FavoriteSubscribedSubredditsSectionRecyclerViewAdapter favoriteSubscribedSubredditsSectionRecyclerViewAdapter; private final SubscribedSubredditsRecyclerViewAdapter subscribedSubredditsRecyclerViewAdapter; private final AccountManagementSectionRecyclerViewAdapter accountManagementSectionRecyclerViewAdapter; private final ConcatAdapter mainPageConcatAdapter; public NavigationDrawerRecyclerViewMergedAdapter(BaseActivity baseActivity, SharedPreferences sharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferences, SharedPreferences navigationDrawerSharedPreferences, SharedPreferences securitySharedPreferences, CustomThemeWrapper customThemeWrapper, @NonNull String accountName, ItemClickListener itemClickListener) { RequestManager glide = Glide.with(baseActivity); headerSectionRecyclerViewAdapter = new HeaderSectionRecyclerViewAdapter(baseActivity, customThemeWrapper, glide, accountName, sharedPreferences, navigationDrawerSharedPreferences, securitySharedPreferences, new HeaderSectionRecyclerViewAdapter.PageToggle() { @Override public void openAccountManagement() { NavigationDrawerRecyclerViewMergedAdapter.this.openAccountSection(); } @Override public void closeAccountManagement() { NavigationDrawerRecyclerViewMergedAdapter.this.closeAccountManagement(false); } }); accountSectionRecyclerViewAdapter = new AccountSectionRecyclerViewAdapter(baseActivity, customThemeWrapper, navigationDrawerSharedPreferences, !accountName.equals(Account.ANONYMOUS_ACCOUNT), itemClickListener); redditSectionRecyclerViewAdapter = new RedditSectionRecyclerViewAdapter(baseActivity, customThemeWrapper, navigationDrawerSharedPreferences, itemClickListener); postSectionRecyclerViewAdapter = new PostSectionRecyclerViewAdapter(baseActivity, customThemeWrapper, navigationDrawerSharedPreferences, !accountName.equals(Account.ANONYMOUS_ACCOUNT), itemClickListener); preferenceSectionRecyclerViewAdapter = new PreferenceSectionRecyclerViewAdapter(baseActivity, customThemeWrapper, accountName, nsfwAndSpoilerSharedPreferences, navigationDrawerSharedPreferences, itemClickListener); favoriteSubscribedSubredditsSectionRecyclerViewAdapter = new FavoriteSubscribedSubredditsSectionRecyclerViewAdapter( baseActivity, glide, customThemeWrapper, navigationDrawerSharedPreferences, itemClickListener); subscribedSubredditsRecyclerViewAdapter = new SubscribedSubredditsRecyclerViewAdapter(baseActivity, glide, customThemeWrapper, navigationDrawerSharedPreferences, itemClickListener); accountManagementSectionRecyclerViewAdapter = new AccountManagementSectionRecyclerViewAdapter(baseActivity, customThemeWrapper, glide, !accountName.equals(Account.ANONYMOUS_ACCOUNT), itemClickListener); mainPageConcatAdapter = new ConcatAdapter( headerSectionRecyclerViewAdapter, accountSectionRecyclerViewAdapter, redditSectionRecyclerViewAdapter, postSectionRecyclerViewAdapter, preferenceSectionRecyclerViewAdapter, favoriteSubscribedSubredditsSectionRecyclerViewAdapter, subscribedSubredditsRecyclerViewAdapter); } public ConcatAdapter getConcatAdapter() { return mainPageConcatAdapter; } private void openAccountSection() { mainPageConcatAdapter.removeAdapter(accountSectionRecyclerViewAdapter); mainPageConcatAdapter.removeAdapter(redditSectionRecyclerViewAdapter); mainPageConcatAdapter.removeAdapter(postSectionRecyclerViewAdapter); mainPageConcatAdapter.removeAdapter(preferenceSectionRecyclerViewAdapter); mainPageConcatAdapter.removeAdapter(favoriteSubscribedSubredditsSectionRecyclerViewAdapter); mainPageConcatAdapter.removeAdapter(subscribedSubredditsRecyclerViewAdapter); mainPageConcatAdapter.addAdapter(accountManagementSectionRecyclerViewAdapter); } public void closeAccountManagement(boolean refreshHeader) { mainPageConcatAdapter.removeAdapter(accountManagementSectionRecyclerViewAdapter); mainPageConcatAdapter.addAdapter(accountSectionRecyclerViewAdapter); mainPageConcatAdapter.addAdapter(redditSectionRecyclerViewAdapter); mainPageConcatAdapter.addAdapter(postSectionRecyclerViewAdapter); mainPageConcatAdapter.addAdapter(preferenceSectionRecyclerViewAdapter); mainPageConcatAdapter.addAdapter(favoriteSubscribedSubredditsSectionRecyclerViewAdapter); mainPageConcatAdapter.addAdapter(subscribedSubredditsRecyclerViewAdapter); if (refreshHeader) { headerSectionRecyclerViewAdapter.closeAccountManagement(true); } } public void updateAccountInfo(String profileImageUrl, String bannerImageUrl, int karma) { headerSectionRecyclerViewAdapter.updateAccountInfo(profileImageUrl, bannerImageUrl, karma); } public void setRequireAuthToAccountSection(boolean requireAuthToAccountSection) { headerSectionRecyclerViewAdapter.setRequireAuthToAccountSection(requireAuthToAccountSection); } public void setShowAvatarOnTheRightInTheNavigationDrawer(boolean showAvatarOnTheRightInTheNavigationDrawer) { headerSectionRecyclerViewAdapter.setShowAvatarOnTheRightInTheNavigationDrawer(showAvatarOnTheRightInTheNavigationDrawer); } public void changeAccountsDataset(List accounts) { accountManagementSectionRecyclerViewAdapter.changeAccountsDataset(accounts); } public void setInboxCount(int inboxCount) { accountSectionRecyclerViewAdapter.setInboxCount(inboxCount); } public void setNSFWEnabled(boolean isNSFWEnabled) { preferenceSectionRecyclerViewAdapter.setNSFWEnabled(isNSFWEnabled); } public void setFavoriteSubscribedSubreddits(List favoriteSubscribedSubreddits) { favoriteSubscribedSubredditsSectionRecyclerViewAdapter.setFavoriteSubscribedSubreddits(favoriteSubscribedSubreddits); } public void setSubscribedSubreddits(List subscribedSubreddits) { subscribedSubredditsRecyclerViewAdapter.setSubscribedSubreddits(subscribedSubreddits); } public void setHideKarma(boolean hideKarma) { headerSectionRecyclerViewAdapter.setHideKarma(hideKarma); } public interface ItemClickListener { void onMenuClick(int stringId); void onMenuLongClick(int stringId); void onSubscribedSubredditClick(String subredditName); void onAccountClick(@NonNull String accountName); void onAccountLongClick(@NonNull String accountName); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/PostFilterUsageEmbeddedRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.databinding.ItemPostFilterUsageEmbeddedBinding; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; public class PostFilterUsageEmbeddedRecyclerViewAdapter extends RecyclerView.Adapter { private final BaseActivity baseActivity; private List postFilterUsageList; public PostFilterUsageEmbeddedRecyclerViewAdapter(BaseActivity baseActivity) { this.baseActivity = baseActivity; } @NonNull @Override public EntryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new EntryViewHolder(ItemPostFilterUsageEmbeddedBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(@NonNull EntryViewHolder holder, int position) { if (postFilterUsageList == null || postFilterUsageList.isEmpty()) { holder.textView.setText(R.string.click_to_apply_post_filter); } else if (holder.getBindingAdapterPosition() > 4) { holder.textView.setText(baseActivity.getString(R.string.post_filter_usage_embedded_more_count, postFilterUsageList.size() - 5)); } else { PostFilterUsage postFilterUsage = postFilterUsageList.get(holder.getBindingAdapterPosition()); switch (postFilterUsage.usage) { case PostFilterUsage.HOME_TYPE: holder.textView.setText(R.string.post_filter_usage_home); break; case PostFilterUsage.SUBREDDIT_TYPE: if (postFilterUsage.nameOfUsage.equals(PostFilterUsage.NO_USAGE)) { holder.textView.setText(R.string.post_filter_usage_embedded_subreddit_all); } else { holder.textView.setText("r/" + postFilterUsage.nameOfUsage); } break; case PostFilterUsage.USER_TYPE: if (postFilterUsage.nameOfUsage.equals(PostFilterUsage.NO_USAGE)) { holder.textView.setText(R.string.post_filter_usage_embedded_user_all); } else { holder.textView.setText("u/" + postFilterUsage.nameOfUsage); } break; case PostFilterUsage.SEARCH_TYPE: holder.textView.setText(R.string.post_filter_usage_search); break; case PostFilterUsage.MULTIREDDIT_TYPE: if (postFilterUsage.nameOfUsage.equals(PostFilterUsage.NO_USAGE)) { holder.textView.setText(R.string.post_filter_usage_embedded_multireddit_all); } else { holder.textView.setText(postFilterUsage.nameOfUsage); } break; case PostFilterUsage.HISTORY_TYPE: holder.textView.setText(R.string.post_filter_usage_history); break; case PostFilterUsage.UPVOTED_TYPE: holder.textView.setText(R.string.post_filter_usage_upvoted); break; case PostFilterUsage.DOWNVOTED_TYPE: holder.textView.setText(R.string.post_filter_usage_downvoted); break; case PostFilterUsage.HIDDEN_TYPE: holder.textView.setText(R.string.post_filter_usage_hidden); break; case PostFilterUsage.SAVED_TYPE: holder.textView.setText(R.string.post_filter_usage_saved); break; } } } @Override public int getItemCount() { return postFilterUsageList == null || postFilterUsageList.isEmpty() ? 1 : (postFilterUsageList.size() > 5 ? 6 : postFilterUsageList.size()); } public void setPostFilterUsageList(List postFilterUsageList) { this.postFilterUsageList = postFilterUsageList; notifyDataSetChanged(); } class EntryViewHolder extends RecyclerView.ViewHolder { TextView textView; public EntryViewHolder(@NonNull ItemPostFilterUsageEmbeddedBinding binding) { super(binding.getRoot()); textView = binding.getRoot(); textView.setTextColor(baseActivity.customThemeWrapper.getSecondaryTextColor()); if (baseActivity.typeface != null) { textView.setTypeface(baseActivity.typeface); } textView.setOnClickListener(view -> { Toast.makeText(baseActivity, textView.getText(), Toast.LENGTH_SHORT).show(); }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/PostSectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.content.SharedPreferences; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuGroupTitleBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuItemBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class PostSectionRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MENU_GROUP_TITLE = 1; private static final int VIEW_TYPE_MENU_ITEM = 2; private static final int POST_SECTION_ITEMS = 4; private final BaseActivity baseActivity; private final int primaryTextColor; private final int secondaryTextColor; private final int primaryIconColor; private boolean collapsePostSection; private final boolean isLoggedIn; private final NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener; public PostSectionRecyclerViewAdapter(BaseActivity baseActivity, CustomThemeWrapper customThemeWrapper, SharedPreferences navigationDrawerSharedPreferences, boolean isLoggedIn, NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener) { this.baseActivity = baseActivity; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); primaryIconColor = customThemeWrapper.getPrimaryIconColor(); collapsePostSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.COLLAPSE_POST_SECTION, false); this.isLoggedIn = isLoggedIn; this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { return position == 0 ? VIEW_TYPE_MENU_GROUP_TITLE : VIEW_TYPE_MENU_ITEM; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MENU_GROUP_TITLE) { return new MenuGroupTitleViewHolder(ItemNavDrawerMenuGroupTitleBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new MenuItemViewHolder(ItemNavDrawerMenuItemBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MenuGroupTitleViewHolder) { ((MenuGroupTitleViewHolder) holder).binding.titleTextViewItemNavDrawerMenuGroupTitle.setText(R.string.label_post); if (collapsePostSection) { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24dp); } else { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24dp); } holder.itemView.setOnClickListener(view -> { if (collapsePostSection) { collapsePostSection = false; notifyItemRangeInserted(holder.getBindingAdapterPosition() + 1, POST_SECTION_ITEMS); } else { collapsePostSection = true; notifyItemRangeRemoved(holder.getBindingAdapterPosition() + 1, POST_SECTION_ITEMS); } notifyItemChanged(holder.getBindingAdapterPosition()); }); } else if (holder instanceof MenuItemViewHolder) { int stringId = 0; int drawableId = 0; if (isLoggedIn) { switch (position) { case 1: stringId = R.string.upvoted; drawableId = R.drawable.ic_arrow_upward_day_night_24dp; break; case 2: stringId = R.string.downvoted; drawableId = R.drawable.ic_arrow_downward_day_night_24dp; break; case 3: stringId = R.string.hidden; drawableId = R.drawable.ic_lock_day_night_24dp; break; case 4: stringId = R.string.account_saved_thing_activity_label; drawableId = R.drawable.ic_bookmarks_day_night_24dp; break; } } ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(stringId); ((MenuItemViewHolder) holder).binding.imageViewItemNavDrawerMenuItem.setImageDrawable(ContextCompat.getDrawable(baseActivity, drawableId)); int finalStringId = stringId; holder.itemView.setOnClickListener(view -> itemClickListener.onMenuClick(finalStringId)); } } @Override public int getItemCount() { return isLoggedIn ? (collapsePostSection ? 1: POST_SECTION_ITEMS + 1) : 0; } class MenuGroupTitleViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuGroupTitleBinding binding; MenuGroupTitleViewHolder(@NonNull ItemNavDrawerMenuGroupTitleBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.titleTextViewItemNavDrawerMenuGroupTitle.setTypeface(baseActivity.typeface); } binding.titleTextViewItemNavDrawerMenuGroupTitle.setTextColor(secondaryTextColor); binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setColorFilter(secondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); } } class MenuItemViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuItemBinding binding; MenuItemViewHolder(@NonNull ItemNavDrawerMenuItemBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.textViewItemNavDrawerMenuItem.setTypeface(baseActivity.typeface); } binding.textViewItemNavDrawerMenuItem.setTextColor(primaryTextColor); binding.imageViewItemNavDrawerMenuItem.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/PreferenceSectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuGroupTitleBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuItemBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class PreferenceSectionRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MENU_GROUP_TITLE = 1; private static final int VIEW_TYPE_MENU_ITEM = 2; private static final int PREFERENCES_SECTION_ITEMS = 3; private final BaseActivity baseActivity; private final Resources resources; private final int primaryTextColor; private final int secondaryTextColor; private final int primaryIconColor; private boolean isNSFWEnabled; private boolean collapsePreferencesSection; private final NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener; public PreferenceSectionRecyclerViewAdapter(BaseActivity baseActivity, CustomThemeWrapper customThemeWrapper, @NonNull String accountName, SharedPreferences nsfwAndSpoilerSharedPreferences, SharedPreferences navigationDrawerSharedPreferences, NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener) { this.baseActivity = baseActivity; resources = baseActivity.getResources(); primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); primaryIconColor = customThemeWrapper.getPrimaryIconColor(); isNSFWEnabled = nsfwAndSpoilerSharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.NSFW_BASE, false); collapsePreferencesSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.COLLAPSE_PREFERENCES_SECTION, false); this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { return position == 0 ? VIEW_TYPE_MENU_GROUP_TITLE : VIEW_TYPE_MENU_ITEM; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MENU_GROUP_TITLE) { return new MenuGroupTitleViewHolder(ItemNavDrawerMenuGroupTitleBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new MenuItemViewHolder(ItemNavDrawerMenuItemBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MenuGroupTitleViewHolder) { ((MenuGroupTitleViewHolder) holder).binding.titleTextViewItemNavDrawerMenuGroupTitle.setText(R.string.label_preferences); if (collapsePreferencesSection) { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24dp); } else { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24dp); } holder.itemView.setOnClickListener(view -> { if (collapsePreferencesSection) { collapsePreferencesSection = !collapsePreferencesSection; notifyItemRangeInserted(holder.getBindingAdapterPosition() + 1, PREFERENCES_SECTION_ITEMS); } else { collapsePreferencesSection = !collapsePreferencesSection; notifyItemRangeRemoved(holder.getBindingAdapterPosition() + 1, PREFERENCES_SECTION_ITEMS); } notifyItemChanged(holder.getBindingAdapterPosition()); }); } else if (holder instanceof MenuItemViewHolder) { int stringId = 0; int drawableId = 0; boolean setOnClickListener = true; switch (position) { case 1: if ((resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { stringId = R.string.dark_theme; drawableId = R.drawable.ic_dark_theme_24dp; } else { stringId = R.string.light_theme; drawableId = R.drawable.ic_light_theme_24dp; } break; case 2: setOnClickListener = false; if (isNSFWEnabled) { stringId = R.string.disable_nsfw; drawableId = R.drawable.ic_nsfw_off_day_night_24dp; } else { stringId = R.string.enable_nsfw; drawableId = R.drawable.ic_nsfw_on_day_night_24dp; } holder.itemView.setOnClickListener(view -> { if (isNSFWEnabled) { isNSFWEnabled = false; ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(R.string.enable_nsfw); ((MenuItemViewHolder) holder).binding.imageViewItemNavDrawerMenuItem.setImageDrawable(ContextCompat.getDrawable(baseActivity, R.drawable.ic_nsfw_on_day_night_24dp)); itemClickListener.onMenuClick(R.string.disable_nsfw); } else { isNSFWEnabled = true; ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(R.string.disable_nsfw); ((MenuItemViewHolder) holder).binding.imageViewItemNavDrawerMenuItem.setImageDrawable(ContextCompat.getDrawable(baseActivity, R.drawable.ic_nsfw_off_day_night_24dp)); itemClickListener.onMenuClick(R.string.enable_nsfw); } }); break; case 3: stringId = R.string.settings; drawableId = R.drawable.ic_settings_day_night_24dp; } if (stringId != 0) { ((MenuItemViewHolder) holder).binding.textViewItemNavDrawerMenuItem.setText(stringId); ((MenuItemViewHolder) holder).binding.imageViewItemNavDrawerMenuItem.setImageDrawable(ContextCompat.getDrawable(baseActivity, drawableId)); if (setOnClickListener) { int finalStringId = stringId; holder.itemView.setOnClickListener(view -> itemClickListener.onMenuClick(finalStringId)); } } } } @Override public int getItemCount() { return collapsePreferencesSection ? 1 : PREFERENCES_SECTION_ITEMS + 1; } public void setNSFWEnabled(boolean isNSFWEnabled) { this.isNSFWEnabled = isNSFWEnabled; notifyItemChanged(2); } class MenuGroupTitleViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuGroupTitleBinding binding; MenuGroupTitleViewHolder(@NonNull ItemNavDrawerMenuGroupTitleBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.titleTextViewItemNavDrawerMenuGroupTitle.setTypeface(baseActivity.typeface); } binding.titleTextViewItemNavDrawerMenuGroupTitle.setTextColor(secondaryTextColor); binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setColorFilter(secondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); } } class MenuItemViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuItemBinding binding; MenuItemViewHolder(@NonNull ItemNavDrawerMenuItemBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.textViewItemNavDrawerMenuItem.setTypeface(baseActivity.typeface); } binding.textViewItemNavDrawerMenuItem.setTextColor(primaryTextColor); binding.imageViewItemNavDrawerMenuItem.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/RedditSectionRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.content.SharedPreferences; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuGroupTitleBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuItemBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class RedditSectionRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MENU_GROUP_TITLE = 1; private static final int VIEW_TYPE_MENU_ITEM = 2; private static final int REDDIT_SECTION_ITEMS = 1; private final BaseActivity baseActivity; private final int primaryTextColor; private final int secondaryTextColor; private final int primaryIconColor; private boolean collapseRedditSection; private final NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener; public RedditSectionRecyclerViewAdapter(BaseActivity baseActivity, CustomThemeWrapper customThemeWrapper, SharedPreferences navigationDrawerSharedPreferences, NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener) { this.baseActivity = baseActivity; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); primaryIconColor = customThemeWrapper.getPrimaryIconColor(); collapseRedditSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.COLLAPSE_REDDIT_SECTION, false); this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { return position == 0 ? VIEW_TYPE_MENU_GROUP_TITLE : VIEW_TYPE_MENU_ITEM; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MENU_GROUP_TITLE) { return new MenuGroupTitleViewHolder(ItemNavDrawerMenuGroupTitleBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new MenuItemViewHolder(ItemNavDrawerMenuItemBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MenuGroupTitleViewHolder) { ((MenuGroupTitleViewHolder) holder).binding.titleTextViewItemNavDrawerMenuGroupTitle.setText(R.string.label_reddit); if (collapseRedditSection) { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24dp); } else { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24dp); } holder.itemView.setOnClickListener(view -> { if (collapseRedditSection) { collapseRedditSection = !collapseRedditSection; notifyItemRangeInserted(holder.getBindingAdapterPosition() + 1, REDDIT_SECTION_ITEMS); } else { collapseRedditSection = !collapseRedditSection; notifyItemRangeRemoved(holder.getBindingAdapterPosition() + 1, REDDIT_SECTION_ITEMS); } notifyItemChanged(holder.getBindingAdapterPosition()); }); } else if (holder instanceof MenuItemViewHolder) { /*int stringId = 0; int drawableId = 0; ((MenuItemViewHolder) holder).menuTextView.setText(stringId); ((MenuItemViewHolder) holder).imageView.setImageDrawable(ContextCompat.getDrawable(baseActivity, drawableId)); int finalStringId = stringId; holder.itemView.setOnClickListener(view -> itemClickListener.onMenuClick(finalStringId));*/ } } @Override public int getItemCount() { return 0; } class MenuGroupTitleViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuGroupTitleBinding binding; MenuGroupTitleViewHolder(@NonNull ItemNavDrawerMenuGroupTitleBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.titleTextViewItemNavDrawerMenuGroupTitle.setTypeface(baseActivity.typeface); } binding.titleTextViewItemNavDrawerMenuGroupTitle.setTextColor(secondaryTextColor); binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setColorFilter(secondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); } } class MenuItemViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuItemBinding binding; MenuItemViewHolder(@NonNull ItemNavDrawerMenuItemBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.textViewItemNavDrawerMenuItem.setTypeface(baseActivity.typeface); } binding.textViewItemNavDrawerMenuItem.setTextColor(primaryTextColor); binding.imageViewItemNavDrawerMenuItem.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/adapters/navigationdrawer/SubscribedSubredditsRecyclerViewAdapter.java ================================================ package ml.docilealligator.infinityforreddit.adapters.navigationdrawer; import android.content.SharedPreferences; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestManager; import java.util.ArrayList; import java.util.List; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerMenuGroupTitleBinding; import ml.docilealligator.infinityforreddit.databinding.ItemNavDrawerSubscribedThingBinding; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class SubscribedSubredditsRecyclerViewAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MENU_GROUP_TITLE = 1; private static final int VIEW_TYPE_SUBSCRIBED_SUBREDDIT = 2; private final BaseActivity baseActivity; private final RequestManager glide; private final int primaryTextColor; private final int secondaryTextColor; private boolean collapseSubscribedSubredditsSection; private final boolean hideSubscribedSubredditsSection; private ArrayList subscribedSubreddits = new ArrayList<>(); private final NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener; public SubscribedSubredditsRecyclerViewAdapter(BaseActivity baseActivity, RequestManager glide, CustomThemeWrapper customThemeWrapper, SharedPreferences navigationDrawerSharedPreferences, NavigationDrawerRecyclerViewMergedAdapter.ItemClickListener itemClickListener) { this.baseActivity = baseActivity; this.glide = glide; primaryTextColor = customThemeWrapper.getPrimaryTextColor(); secondaryTextColor = customThemeWrapper.getSecondaryTextColor(); collapseSubscribedSubredditsSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.COLLAPSE_SUBSCRIBED_SUBREDDITS_SECTION, false); hideSubscribedSubredditsSection = navigationDrawerSharedPreferences.getBoolean(SharedPreferencesUtils.HIDE_SUBSCRIBED_SUBREDDITS_SECTIONS, false); this.itemClickListener = itemClickListener; } @Override public int getItemViewType(int position) { return position == 0 ? VIEW_TYPE_MENU_GROUP_TITLE : VIEW_TYPE_SUBSCRIBED_SUBREDDIT; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MENU_GROUP_TITLE) { return new MenuGroupTitleViewHolder(ItemNavDrawerMenuGroupTitleBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } else { return new SubscribedThingViewHolder(ItemNavDrawerSubscribedThingBinding .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceof MenuGroupTitleViewHolder) { ((MenuGroupTitleViewHolder) holder).binding.titleTextViewItemNavDrawerMenuGroupTitle.setText(R.string.subscriptions); if (collapseSubscribedSubredditsSection) { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24dp); } else { ((MenuGroupTitleViewHolder) holder).binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24dp); } holder.itemView.setOnClickListener(view -> { if (collapseSubscribedSubredditsSection) { collapseSubscribedSubredditsSection = !collapseSubscribedSubredditsSection; notifyItemRangeInserted(holder.getBindingAdapterPosition() + 1, subscribedSubreddits.size()); } else { collapseSubscribedSubredditsSection = !collapseSubscribedSubredditsSection; notifyItemRangeRemoved(holder.getBindingAdapterPosition() + 1, subscribedSubreddits.size()); } notifyItemChanged(holder.getBindingAdapterPosition()); }); } else if (holder instanceof SubscribedThingViewHolder) { SubscribedSubredditData subreddit = subscribedSubreddits.get(position - 1); String subredditName = subreddit.getName(); String iconUrl = subreddit.getIconUrl(); ((SubscribedThingViewHolder) holder).binding.thingNameTextViewItemNavDrawerSubscribedThing.setText(subredditName); if (iconUrl != null && !iconUrl.equals("")) { glide.load(iconUrl) .transform(new RoundedCornersTransformation(72, 0)) .error(glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0))) .into(((SubscribedThingViewHolder) holder).binding.thingIconGifImageViewItemNavDrawerSubscribedThing); } else { glide.load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(((SubscribedThingViewHolder) holder).binding.thingIconGifImageViewItemNavDrawerSubscribedThing); } holder.itemView.setOnClickListener(view -> { itemClickListener.onSubscribedSubredditClick(subredditName); }); } } @Override public int getItemCount() { if (hideSubscribedSubredditsSection) { return 0; } return subscribedSubreddits.isEmpty() ? 0 : (collapseSubscribedSubredditsSection ? 1 : subscribedSubreddits.size() + 1); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); if (holder instanceof SubscribedThingViewHolder) { glide.clear(((SubscribedThingViewHolder) holder).binding.thingIconGifImageViewItemNavDrawerSubscribedThing); } } public void setSubscribedSubreddits(List subscribedSubreddits) { this.subscribedSubreddits = (ArrayList) subscribedSubreddits; notifyDataSetChanged(); } class MenuGroupTitleViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerMenuGroupTitleBinding binding; MenuGroupTitleViewHolder(@NonNull ItemNavDrawerMenuGroupTitleBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.titleTextViewItemNavDrawerMenuGroupTitle.setTypeface(baseActivity.typeface); } binding.titleTextViewItemNavDrawerMenuGroupTitle.setTextColor(secondaryTextColor); binding.collapseIndicatorImageViewItemNavDrawerMenuGroupTitle.setColorFilter(secondaryTextColor, android.graphics.PorterDuff.Mode.SRC_IN); } } class SubscribedThingViewHolder extends RecyclerView.ViewHolder { ItemNavDrawerSubscribedThingBinding binding; SubscribedThingViewHolder(@NonNull ItemNavDrawerSubscribedThingBinding binding) { super(binding.getRoot()); this.binding = binding; if (baseActivity.typeface != null) { binding.thingNameTextViewItemNavDrawerSubscribedThing.setTypeface(baseActivity.typeface); } binding.thingNameTextViewItemNavDrawerSubscribedThing.setTextColor(primaryTextColor); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/DownloadFile.java ================================================ package ml.docilealligator.infinityforreddit.apis; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Streaming; import retrofit2.http.Url; public interface DownloadFile { @Streaming @GET() Call downloadFile(@Url String fileUrl); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/ImgurAPI.java ================================================ package ml.docilealligator.infinityforreddit.apis; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.Path; public interface ImgurAPI { @GET("gallery/{id}") Call getGalleryImages(@Header(APIUtils.AUTHORIZATION_KEY) String clientId, @Path("id") String id); @GET("album/{id}") Call getAlbumImages(@Header(APIUtils.AUTHORIZATION_KEY) String clientId, @Path("id") String id); @GET("image/{id}") Call getImage(@Header(APIUtils.AUTHORIZATION_KEY) String clientId, @Path("id") String id); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/OhMyDlAPI.kt ================================================ package ml.docilealligator.infinityforreddit.apis import retrofit2.Call import retrofit2.http.Body import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.HeaderMap import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query interface OhMyDlAPI { @FormUrlEncoded @POST("/api/download") fun getRedgifsData( @FieldMap params: Map ): Call } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/PushshiftAPI.java ================================================ package ml.docilealligator.infinityforreddit.apis; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; public interface PushshiftAPI { @GET("reddit/comment/search/") Call getRemovedComment(@Query("ids") String commentId); @GET("reddit/submission/search/") Call getRemovedPost(@Query("ids") String postId); @GET("reddit/comment/search/") Call searchComments(@Query("link_id") String linkId, @Query("limit") int limit, @Query("sort") String sort, @Query(value = "fields", encoded = true) String fields, @Query("after") long after, @Query("before") long before, @Query("q") String query); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/RedditAPI.java ================================================ package ml.docilealligator.infinityforreddit.apis; import com.google.common.util.concurrent.ListenableFuture; import java.util.Map; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.APIUtils; import okhttp3.MultipartBody; import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.Response; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.Field; import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.HeaderMap; import retrofit2.http.Multipart; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Part; import retrofit2.http.PartMap; import retrofit2.http.Path; import retrofit2.http.Query; public interface RedditAPI { @FormUrlEncoded @POST("api/v1/access_token") Call getAccessToken(@HeaderMap Map headers, @FieldMap Map params); @GET("r/{subredditName}/about.json?raw_json=1") Call getSubredditData(@Path("subredditName") String subredditName); @GET("r/{subredditName}/about.json?raw_json=1") Call getSubredditDataOauth(@Path("subredditName") String subredditName, @HeaderMap Map headers); @GET("subreddits/mine/subscriber?raw_json=1") Call getSubscribedThing(@Query("after") String lastItem, @HeaderMap Map headers); @GET("api/v1/me?raw_json=1") Call getMyInfo(@HeaderMap Map headers); @FormUrlEncoded @POST("api/vote") Call voteThing(@HeaderMap Map headers, @FieldMap Map params); @GET("comments/{id}.json?raw_json=1") Call getPostOauth(@Path("id") String id, @HeaderMap Map headers); @GET("comments/{id}.json?raw_json=1") Call getPost(@Path("id") String id); @GET("user/{username}/about.json?raw_json=1") Call getUserData(@Path("username") String username); @GET("user/{username}/about.json?raw_json=1&limit=100") Call getUserDataOauth(@HeaderMap Map headers, @Path("username") String username); @GET("user/{username}/comments.json?raw_json=1&limit=100") Call getUserComments(@Path("username") String username, @Query("after") String after, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime); @GET("user/{username}/comments.json?raw_json=1&limit=100") Call getUserCommentsOauth(@HeaderMap Map headers, @Path("username") String username, @Query("after") String after, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime); @GET("user/{username}/{where}.json?&type=comments&raw_json=1&limit=100") Call getUserSavedCommentsOauth(@Path("username") String username, @Path("where") String where, @Query("after") String lastItem, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime, @HeaderMap Map headers); @FormUrlEncoded @POST("api/subscribe") Call subredditSubscription(@HeaderMap Map headers, @FieldMap Map params); @GET("/api/info.json?raw_json=1") Call getInfo(@Query("id") String id); @GET("/api/info.json?raw_json=1") Call getInfoOauth(@Query("id") String id, @HeaderMap Map headers); @GET("subreddits/search.json?raw_json=1") Call searchSubreddits(@Query("q") String subredditName, @Query("after") String after, @Query("sort") SortType.Type sort, @Query("include_over_18") int nsfw, @HeaderMap Map headers); @GET("search.json?raw_json=1&type=user") Call searchUsers(@Query("q") String profileName, @Query("after") String after, @Query("sort") SortType.Type sort, @Query("include_over_18") int nsfw); @FormUrlEncoded @POST("api/comment") Call sendCommentOrReplyToMessage(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("api/del") Call delete(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("api/submit") Call submit(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("api/media/asset.json?raw_json=1&gilding_detail=1") Call uploadImage(@HeaderMap Map headers, @FieldMap Map params); @GET("r/{subredditName}/api/link_flair.json?raw_json=1") Call getFlairs(@HeaderMap Map headers, @Path("subredditName") String subredditName); @GET("/r/{subredditName}/about/rules.json?raw_json=1") Call getRules(@Path("subredditName") String subredditName); @GET("/r/{subredditName}/about/rules.json?raw_json=1") Call getRulesOauth(@HeaderMap Map headers, @Path("subredditName") String subredditName); @GET("/comments/{id}/placeholder/{singleCommentId}.json?raw_json=1") Call getPostAndCommentsSingleThreadByIdOauth(@Path("id") String id, @Path("singleCommentId") String singleCommentId, @Query("sort") SortType.Type sortType, @Query("context") String contextNumber, @HeaderMap Map headers); @GET("/comments/{id}.json?raw_json=1") Call getPostAndCommentsByIdOauth(@Path("id") String id, @Query("sort") SortType.Type sortType, @HeaderMap Map headers); @GET("/comments/{id}/placeholder/{singleCommentId}.json?raw_json=1") Call getPostAndCommentsSingleThreadById(@Path("id") String id, @Path("singleCommentId") String singleCommentId, @Query("sort") SortType.Type sortType, @Query("context") String contextNumber); @GET("/comments/{id}.json?raw_json=1") Call getPostAndCommentsById(@Path("id") String id, @Query("sort") SortType.Type sortType); @Multipart @POST(".") Call uploadMediaToAWS(@PartMap() Map params, @Part() MultipartBody.Part file); @FormUrlEncoded @POST("/api/editusertext") Call editPostOrComment(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/marknsfw") Call markNSFW(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/unmarknsfw") Call unmarkNSFW(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/spoiler") Call markSpoiler(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/unspoiler") Call unmarkSpoiler(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("{subredditNamePrefixed}/api/selectflair") Call selectFlair(@Path("subredditNamePrefixed") String subredditName, @HeaderMap Map headers, @FieldMap Map params); @GET("/message/{where}.json?raw_json=1&limit=100") Call getMessages(@HeaderMap Map headers, @Path("where") String where, @Query("after") String after); @FormUrlEncoded @POST("/api/read_message") Call readMessage(@HeaderMap Map headers, @FieldMap Map ids); @FormUrlEncoded @POST("/api/save") Call save(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/unsave") Call unsave(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/hide") Call hide(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/unhide") Call unhide(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/favorite") Call favoriteThing(@HeaderMap Map headers, @FieldMap Map params); @GET("/api/multi/mine?expand_srs=true") Call getMyMultiReddits(@HeaderMap Map headers); @FormUrlEncoded @POST("/api/multi/favorite?raw_json=1&gilding_detail=1") Call favoriteMultiReddit(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/multi/multipath") Call createMultiReddit(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @PUT("/api/multi/multipath") Call updateMultiReddit(@HeaderMap Map headers, @FieldMap Map params); @DELETE("/api/multi/multipath") Call deleteMultiReddit(@HeaderMap Map headers, @Query("multipath") String multipath); @GET("/api/multi/multipath?expand_srs=true") Call getMultiRedditInfo(@HeaderMap Map headers, @Query("multipath") String multipath); @GET("/api/multi/multipath?expand_srs=true") ListenableFuture> getMultiRedditInfoListenableFuture(@HeaderMap Map headers, @Query("multipath") String multipath); @FormUrlEncoded @POST("/api/report") Call report(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/compose") Call composePrivateMessage(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("api/block_user") Call blockUser(@HeaderMap Map headers, @FieldMap Map params); @GET("r/{subredditName}/api/user_flair_v2.json?raw_json=1") Call getUserFlairs(@HeaderMap Map headers, @Path("subredditName") String subredditName); @FormUrlEncoded @POST("/r/{subredditName}/api/selectflair?raw_json=1") Call selectUserFlair(@HeaderMap Map headers, @FieldMap Map params, @Path("subredditName") String subredditName); @FormUrlEncoded @POST("api/v2/gold/gild") Call awardThing(@HeaderMap Map headers, @FieldMap Map params); @GET("/r/random/comments.json?limit=1&raw_json=1") Call getRandomPost(); @GET("/r/randnsfw/new.json?sort=new&t=all&limit=1&raw_json=1") Call getRandomNSFWPost(); @POST("/api/read_all_messages") Call readAllMessages(@HeaderMap Map headers); @FormUrlEncoded @PUT("/api/multi{multipath}/r/{subredditName}") Call addSubredditToMultiReddit(@HeaderMap Map headers, @FieldMap Map params, @Path(value = "multipath", encoded = true) String multipath, @Path("subredditName") String subredditName); @FormUrlEncoded @POST("/api/quarantine_option?raw_json=1") Call optInQuarantinedSubreddit(@HeaderMap Map headers, @FieldMap Map params); @GET("/api/subreddit_autocomplete_v2?typeahead_active=true&include_profiles=false&raw_json=1") Call subredditAutocomplete(@HeaderMap Map headers, @Query("query") String query, @Query("include_over_18") boolean nsfw); @POST("/api/submit_gallery_post.json?resubmit=true&raw_json=1") Call submitGalleryPost(@HeaderMap Map headers, @Body String body); @POST("/api/submit_poll_post.json?resubmit=true&raw_json=1&gilding_detail=1") Call submitPollPost(@HeaderMap Map headers, @Body String body); @GET("/api/trending_searches_v1.json?withAds=0&raw_json=1&gilding_detail=1") Call getTrendingSearches(); @GET("/api/trending_searches_v1.json?withAds=0&raw_json=1&gilding_detail=1") Call getTrendingSearchesOauth(@HeaderMap Map headers); @GET("/r/{subredditName}/wiki/{wikiPage}.json?raw_json=1") Call getWikiPage(@Path("subredditName") String subredditName, @Path("wikiPage") String wikiPage); @GET("{sortType}?raw_json=1&limit=100") ListenableFuture> getBestPostsListenableFuture(@Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @HeaderMap Map headers); @GET("r/{subredditName}/{sortType}.json?raw_json=1&always_show_media=1") ListenableFuture> getSubredditBestPostsOauthListenableFuture(@Path("subredditName") String subredditName, @Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @Query("limit") int limit, @HeaderMap Map headers); @GET("r/{subredditName}/{sortType}.json?raw_json=1&always_show_media=1") ListenableFuture> getSubredditBestPostsListenableFuture(@Path("subredditName") String subredditName, @Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @Query("limit") int limit); @GET("r/{subredditName}/{sortType}.json?raw_json=1&always_show_media=1") ListenableFuture> getAnonymousFrontPageOrMultiredditPostsListenableFuture(@Path("subredditName") String subredditName, @Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @Query("limit") int limit, @Header("User-Agent") String userAgent); @GET("user/{username}/{where}.json?type=links&raw_json=1&limit=100") ListenableFuture> getUserPostsOauthListenableFuture(@Header(APIUtils.AUTHORIZATION_KEY) String authorization, @Path("username") String username, @Path("where") String where, @Query("after") String lastItem, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime); @GET("user/{username}/{where}.json?type=links&raw_json=1") ListenableFuture> getUserPostsOauthListenableFuture(@Header(APIUtils.AUTHORIZATION_KEY) String authorization, @Path("username") String username, @Path("where") String where, @Query("after") String lastItem, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("limit") int limit); @GET("user/{username}/submitted.json?raw_json=1&limit=100") ListenableFuture> getUserPostsListenableFuture(@Path("username") String username, @Query("after") String lastItem, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime); @GET("user/{username}/submitted.json?raw_json=1") ListenableFuture> getUserPostsListenableFuture(@Path("username") String username, @Query("after") String lastItem, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("limit") int limit); @GET("search.json?include_over_18=1&raw_json=1&limit=100&type=link") ListenableFuture> searchPostsOauthListenableFuture(@Query("q") String query, @Query("after") String after, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("source") String source, @HeaderMap Map headers); @GET("search.json?include_over_18=1&raw_json=1&limit=100&type=link") ListenableFuture> searchPostsListenableFuture(@Query("q") String query, @Query("after") String after, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("source") String source); @GET("r/{subredditName}/search.json?include_over_18=1&raw_json=1&limit=100&type=link&restrict_sr=true") ListenableFuture> searchPostsInSpecificSubredditOauthListenableFuture(@Path("subredditName") String subredditName, @Query("q") String query, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("after") String after, @HeaderMap Map headers); @GET("r/{subredditName}/search.json?include_over_18=1&raw_json=1&limit=100&type=link&restrict_sr=true") ListenableFuture> searchPostsInSpecificSubredditListenableFuture(@Path("subredditName") String subredditName, @Query("q") String query, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("after") String after); @GET("{multipath}?raw_json=1&limit=100") ListenableFuture> getMultiRedditPostsListenableFuture(@Path(value = "multipath", encoded = true) String multiPath, @Path(value = "sortType", encoded = true) SortType.Type sortType, @Query("after") String after, @Query("t") SortType.Time sortTime); @GET("{multipath}?raw_json=1") ListenableFuture> getMultiRedditPostsListenableFuture(@Path(value = "multipath", encoded = true) String multiPath, @Path(value = "sortType", encoded = true) SortType.Type sortType, @Query("after") String after, @Query("t") SortType.Time sortTime, @Query("limit") int limit); @GET("{multipath}/{sortType}.json?raw_json=1&limit=100") ListenableFuture> getMultiRedditPostsOauthListenableFuture(@Path(value = "multipath", encoded = true) String multiPath, @Path(value = "sortType", encoded = true) SortType.Type sortType, @Query("after") String after, @Query("t") SortType.Time sortTime, @HeaderMap Map headers); @GET("{multipath}/{sortType}.json?raw_json=1") ListenableFuture> getMultiRedditPostsOauthListenableFuture(@Path(value = "multipath", encoded = true) String multiPath, @Path(value = "sortType", encoded = true) SortType.Type sortType, @Query("after") String after, @Query("t") SortType.Time sortTime, @HeaderMap Map headers, @Query("limit") int limit); @GET("{multipath}/search.json?raw_json=1&limit=100&type=link&restrict_sr=on&sr_detail=true&include_over_18=1&always_show_media=1") ListenableFuture> searchMultiRedditPostsListenableFuture(@Path(value = "multipath", encoded = true) String multiPath, @Query("q") String query, @Query("after") String after, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime); @GET("{multipath}/search.json?raw_json=1&limit=100&type=link&restrict_sr=on&sr_detail=true&include_over_18=1&always_show_media=1") ListenableFuture> searchMultiRedditPostsOauthListenableFuture(@Path(value = "multipath", encoded = true) String multiPath, @Query("q") String query, @Query("after") String after, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime, @HeaderMap Map headers); @GET("{sortType}?raw_json=1&limit=100") Call getBestPosts(@Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @HeaderMap Map headers); @GET("r/{subredditName}/{sortType}.json?raw_json=1&always_show_media=1") Call getSubredditBestPostsOauth(@Path("subredditName") String subredditName, @Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @Query("limit") int limit, @HeaderMap Map headers); @GET("r/{subredditName}/{sortType}.json?raw_json=1&always_show_media=1") Call getSubredditBestPosts(@Path("subredditName") String subredditName, @Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @Query("limit") int limit); @GET("r/{subredditName}/{sortType}.json?raw_json=1&always_show_media=1") Call getAnonymousFrontPageOrMultiredditPosts(@Path("subredditName") String subredditName, @Path("sortType") SortType.Type sortType, @Query("t") SortType.Time sortTime, @Query("after") String lastItem, @Query("limit") int limit, @Header("User-Agent") String userAgent); @GET("user/{username}/{where}.json?&type=links&raw_json=1&limit=100") Call getUserPostsOauth(@Path("username") String username, @Path("where") String where, @Query("after") String lastItem, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime, @HeaderMap Map headers); @GET("user/{username}/submitted.json?raw_json=1&limit=100") Call getUserPosts(@Path("username") String username, @Query("after") String lastItem, @Query("sort") SortType.Type sortType, @Query("t") SortType.Time sortTime); @GET("search.json?include_over_18=1&raw_json=1&limit=100&type=link") Call searchPostsOauth(@Query("q") String query, @Query("after") String after, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("source") String source, @HeaderMap Map headers); @GET("search.json?include_over_18=1&raw_json=1&limit=100&type=link") Call searchPosts(@Query("q") String query, @Query("after") String after, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("source") String source); @GET("r/{subredditName}/search.json?include_over_18=1&raw_json=1&limit=100&type=link&restrict_sr=true") Call searchPostsInSpecificSubredditOauth(@Path("subredditName") String subredditName, @Query("q") String query, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("after") String after, @HeaderMap Map headers); @GET("r/{subredditName}/search.json?include_over_18=1&raw_json=1&limit=100&type=link&restrict_sr=true") Call searchPostsInSpecificSubreddit(@Path("subredditName") String subredditName, @Query("q") String query, @Query("sort") SortType.Type sort, @Query("t") SortType.Time sortTime, @Query("after") String after); @GET("{multipath}?raw_json=1&limit=100") Call getMultiRedditPosts(@Path(value = "multipath", encoded = true) String multiPath, @Query("after") String after, @Query("t") SortType.Time sortTime); @GET("{multipath}.json?raw_json=1&limit=100") Call getMultiRedditPostsOauth(@Path(value = "multipath", encoded = true) String multiPath, @Query("after") String after, @Query("t") SortType.Time sortTime, @HeaderMap Map headers); @POST("r/{subredditName}/api/delete_sr_icon") Call deleteSrIcon(@HeaderMap Map headers, @Path("subredditName") String subredditName); @POST("r/{subredditName}/api/delete_sr_banner") Call deleteSrBanner(@HeaderMap Map headers, @Path("subredditName") String subredditName); @Multipart @POST("r/{subredditName}/api/upload_sr_img") Call uploadSrImg(@HeaderMap Map headers, @Path("subredditName") String subredditName, @PartMap Map params, @Part MultipartBody.Part file); @GET("r/{subredditName}/about/edit?raw_json=1") Call getSubredditSetting(@HeaderMap Map headers, @Path("subredditName") String subredditName); @FormUrlEncoded @POST("/api/site_admin") Call postSiteAdmin(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/morechildren.json?raw_json=1&api_type=json") Call moreChildren(@Field("link_id") String linkId, @Field("children") String children, @Field("sort") SortType.Type sort); @FormUrlEncoded @POST("/api/morechildren.json?raw_json=1&api_type=json") Call moreChildrenOauth(@Field("link_id") String linkId, @Field("children") String children, @Field("sort") SortType.Type sort, @HeaderMap Map headers); @FormUrlEncoded @POST("/api/sendreplies") Call toggleRepliesNotification(@HeaderMap Map headers, @FieldMap Map params); @GET("/api/user_data_by_account_ids.json") Call loadPartialUserData(@Query("ids") String commaSeparatedUserFullNames); @FormUrlEncoded @POST("/api/approve") Call approveThing(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/remove") Call removeThing(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/set_subreddit_sticky") Call toggleStickyPost(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/lock") Call lockThing(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/unlock") Call unLockThing(@HeaderMap Map headers, @FieldMap Map params); @FormUrlEncoded @POST("/api/distinguish") Call toggleDistinguishedThing(@HeaderMap Map headers, @FieldMap Map params); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/RedditAPIKt.kt ================================================ package ml.docilealligator.infinityforreddit.apis import retrofit2.Call import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.HeaderMap import retrofit2.http.POST import retrofit2.http.Query interface RedditAPIKt { @GET("/api/multi/multipath?expand_srs=true&raw_json=1") suspend fun getMultiRedditInfo( @HeaderMap headers: Map, @Query("multipath") multipath: String ): String @FormUrlEncoded @POST("/api/multi/copy?expand_srs=true") suspend fun copyMultiReddit( @HeaderMap headers: Map, @FieldMap params: Map ): String } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/RedgifsAPI.java ================================================ package ml.docilealligator.infinityforreddit.apis; import java.util.Map; import retrofit2.Call; import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.HeaderMap; import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.http.Query; public interface RedgifsAPI { @GET("/v2/gifs/{id}") Call getRedgifsData(@HeaderMap Map headers, @Path("id") String id, @Query("user-agent") String userAgent); @FormUrlEncoded @POST("/v2/oauth/client") Call getRedgifsAccessToken(@FieldMap Map params); @GET("/v2/auth/temporary") Call getRedgifsTemporaryToken(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/RevedditAPI.java ================================================ package ml.docilealligator.infinityforreddit.apis; import java.util.Map; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.HeaderMap; import retrofit2.http.Query; public interface RevedditAPI { @GET("/short/thread-comments/") Call getRemovedComments(@HeaderMap Map headers, @Query("link_id") String threadId, @Query("after") long after, @Query("root_comment_id") String rootCommentId, @Query("comment_id") String commentId, @Query("num_comments") int numComments, @Query("post_created_utc") long postCreatedUtc, @Query("focus_comment_removed") boolean focusCommentRemoved); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/ServerAPI.java ================================================ package ml.docilealligator.infinityforreddit.apis; import com.google.common.util.concurrent.ListenableFuture; import java.util.Map; import retrofit2.Call; import retrofit2.Response; import retrofit2.http.Field; import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.HeaderMap; import retrofit2.http.PATCH; import retrofit2.http.POST; import retrofit2.http.Query; public interface ServerAPI { @GET("/themes/") ListenableFuture> getCustomThemesListenableFuture(@Query("page") String page); @GET("/themes/theme") Call getCustomTheme(@Query("name") String themeName, @Query("username") String username); @FormUrlEncoded @PATCH("/themes/modify") Call modifyTheme(@HeaderMap Map headers, @Field("id") int id, @Field("name") String themeName, @Field("data") String customThemeJson); @FormUrlEncoded @POST("/themes/create") Call createTheme(@HeaderMap Map headers, @Field("name") String themeName, @Field("data") String customThemeJson); @FormUrlEncoded @POST("/redditUserAuth/refresh_access_token") Call refreshAccessToken(@Field("username") String username, @Field("refresh_token") String refreshToken); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/StreamableAPI.java ================================================ package ml.docilealligator.infinityforreddit.apis; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Path; public interface StreamableAPI { @GET("videos/{shortcode}") Call getStreamableData(@Path("shortcode") String shortCode); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/TitleSuggestion.java ================================================ package ml.docilealligator.infinityforreddit.apis; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Url; public interface TitleSuggestion { @GET() Call getHtml(@Url String url); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/apis/VReddIt.java ================================================ package ml.docilealligator.infinityforreddit.apis; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Url; public interface VReddIt { @GET() Call getRedirectUrl(@Url String vReddItUrl); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/AccountManagement.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.content.SharedPreferences; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.account.AccountDao; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class AccountManagement { public static void switchAccount(RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences currentAccountSharedPreferences, Executor executor, Handler handler, String newAccountName, SwitchAccountListener switchAccountListener) { executor.execute(() -> { redditDataRoomDatabase.accountDao().markAllAccountsNonCurrent(); redditDataRoomDatabase.accountDao().markAccountCurrent(newAccountName); Account account = redditDataRoomDatabase.accountDao().getCurrentAccount(); currentAccountSharedPreferences.edit() .putString(SharedPreferencesUtils.ACCESS_TOKEN, account.getAccessToken()) .putString(SharedPreferencesUtils.ACCOUNT_NAME, account.getAccountName()) .putString(SharedPreferencesUtils.ACCOUNT_IMAGE_URL, account.getProfileImageUrl()).apply(); currentAccountSharedPreferences.edit().remove(SharedPreferencesUtils.SUBSCRIBED_THINGS_SYNC_TIME).apply(); handler.post(() -> switchAccountListener.switched(account)); }); } public static void switchToAnonymousMode(RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences currentAccountSharedPreferences, Executor executor, Handler handler, boolean removeCurrentAccount, SwitchToAnonymousAccountAsyncTaskListener switchToAnonymousAccountAsyncTaskListener) { executor.execute(() -> { AccountDao accountDao = redditDataRoomDatabase.accountDao(); if (removeCurrentAccount) { accountDao.deleteCurrentAccount(); } accountDao.markAllAccountsNonCurrent(); String redgifsAccessToken = currentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, ""); currentAccountSharedPreferences.edit().clear().apply(); currentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, redgifsAccessToken).apply(); handler.post(switchToAnonymousAccountAsyncTaskListener::logoutSuccess); }); } public static void removeAccount(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, String accoutName) { executor.execute(() -> { redditDataRoomDatabase.accountDao().deleteAccount(accoutName); }); } public interface SwitchToAnonymousAccountAsyncTaskListener { void logoutSuccess(); } public interface SwitchAccountListener { void switched(Account account); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/AddSubredditOrUserToMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class AddSubredditOrUserToMultiReddit { public interface AddSubredditOrUserToMultiRedditListener { void success(); void failed(int code); } public static void addSubredditOrUserToMultiReddit(Retrofit oauthRetrofit, String accessToken, String multipath, String subredditName, AddSubredditOrUserToMultiRedditListener addSubredditOrUserToMultiRedditListener) { Map params = new HashMap<>(); params.put(APIUtils.MODEL_KEY, "{\"name\":\"" + subredditName + "\"}"); oauthRetrofit.create(RedditAPI.class).addSubredditToMultiReddit(APIUtils.getOAuthHeader(accessToken), params, multipath, subredditName) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { addSubredditOrUserToMultiRedditListener.success(); } else { addSubredditOrUserToMultiRedditListener.failed(response.code()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { addSubredditOrUserToMultiRedditListener.failed(-1); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/BackupSettings.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.os.Handler; import androidx.documentfile.provider.DocumentFile; import com.google.gson.Gson; import net.lingala.zip4j.ZipFile; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.model.enums.EncryptionMethod; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubreddit; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class BackupSettings { public static void backupSettings(Context context, Executor executor, Handler handler, ContentResolver contentResolver, Uri destinationDirUri, String password, RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences defaultSharedPreferences, SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences, SharedPreferences sortTypeSharedPreferences, SharedPreferences postLayoutSharedPreferences, SharedPreferences postDetailsSharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, SharedPreferences mainActivityTabsSharedPreferences, SharedPreferences proxySharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferences, SharedPreferences bottomAppBarSharedPreferences, SharedPreferences postHistorySharedPreferences, SharedPreferences navigationDrawerSharedPreferences, BackupSettingsListener backupSettingsListener) { executor.execute(() -> { File cacheDir = Utils.getCacheDir(context); if (cacheDir == null) { handler.post(() -> backupSettingsListener.failed(context.getText(R.string.restore_settings_failed_cannot_get_cache_dir).toString())); return; } String backupDir = cacheDir + "/Backup/" + BuildConfig.VERSION_NAME; File backupDirFile = new File(backupDir); if (new File(backupDir).exists()) { try { FileUtils.deleteDirectory(backupDirFile); } catch (IOException e) { e.printStackTrace(); } } backupDirFile.mkdirs(); File databaseDirFile = new File(backupDir + "/database"); databaseDirFile.mkdirs(); // Handle default shared preferences (potentially excluding client ID) Map defaultPrefsMap = defaultSharedPreferences.getAll(); Map filteredDefaultPrefsMap = new HashMap<>(defaultPrefsMap); // Create a mutable copy boolean res = saveMapToFile(filteredDefaultPrefsMap, backupDir, SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE); SharedPreferences defaultPrefsPrivate = context.getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); boolean resPrivate = saveMapToFile(defaultPrefsPrivate.getAll(), backupDir, SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE + "_private"); boolean res1 = saveMapToFile(lightThemeSharedPreferences.getAll(), backupDir, CustomThemeSharedPreferencesUtils.LIGHT_THEME_SHARED_PREFERENCES_FILE); boolean res2 = saveMapToFile(darkThemeSharedPreferences.getAll(), backupDir, CustomThemeSharedPreferencesUtils.DARK_THEME_SHARED_PREFERENCES_FILE); boolean res3 = saveMapToFile(amoledThemeSharedPreferences.getAll(), backupDir, CustomThemeSharedPreferencesUtils.AMOLED_THEME_SHARED_PREFERENCES_FILE); boolean res4 = saveMapToFile(sortTypeSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.SORT_TYPE_SHARED_PREFERENCES_FILE); boolean res5 = saveMapToFile(postLayoutSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.POST_LAYOUT_SHARED_PREFERENCES_FILE); boolean res6 = saveMapToFile(postDetailsSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.POST_DETAILS_SHARED_PREFERENCES_FILE); boolean res7 = saveMapToFile(postFeedScrolledPositionSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_SHARED_PREFERENCES_FILE); boolean res8 = saveMapToFile(mainActivityTabsSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.MAIN_PAGE_TABS_SHARED_PREFERENCES_FILE); boolean res9 = saveMapToFile(proxySharedPreferences.getAll(), backupDir, SharedPreferencesUtils.PROXY_SHARED_PREFERENCES_FILE); boolean res10 = saveMapToFile(nsfwAndSpoilerSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.NSFW_AND_SPOILER_SHARED_PREFERENCES_FILE); boolean res11 = saveMapToFile(bottomAppBarSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.BOTTOM_APP_BAR_SHARED_PREFERENCES_FILE); boolean res12 = saveMapToFile(postHistorySharedPreferences.getAll(), backupDir, SharedPreferencesUtils.POST_HISTORY_SHARED_PREFERENCES_FILE); boolean res13 = saveMapToFile(navigationDrawerSharedPreferences.getAll(), backupDir, SharedPreferencesUtils.NAVIGATION_DRAWER_SHARED_PREFERENCES_FILE); List anonymousSubscribedSubredditsData = redditDataRoomDatabase.subscribedSubredditDao().getAllSubscribedSubredditsList(Account.ANONYMOUS_ACCOUNT); String anonymousSubscribedSubredditsDataJson = new Gson().toJson(anonymousSubscribedSubredditsData); boolean res14 = saveDatabaseTableToFile(anonymousSubscribedSubredditsDataJson, databaseDirFile.getAbsolutePath(), "/anonymous_subscribed_subreddits.json"); List anonymousSubscribedUsersData = redditDataRoomDatabase.subscribedUserDao().getAllSubscribedUsersList(Account.ANONYMOUS_ACCOUNT); String anonymousSubscribedUsersDataJson = new Gson().toJson(anonymousSubscribedUsersData); boolean res15 = saveDatabaseTableToFile(anonymousSubscribedUsersDataJson, databaseDirFile.getAbsolutePath(), "/anonymous_subscribed_users.json"); List anonymousMultireddits = redditDataRoomDatabase.multiRedditDao().getAllMultiRedditsList(Account.ANONYMOUS_ACCOUNT); String anonymousMultiredditsJson = new Gson().toJson(anonymousMultireddits); boolean res16 = saveDatabaseTableToFile(anonymousMultiredditsJson, databaseDirFile.getAbsolutePath(), "/anonymous_multireddits.json"); List anonymousMultiredditSubreddits = redditDataRoomDatabase.anonymousMultiredditSubredditDao().getAllSubreddits(); String anonymousMultiredditSubredditsJson = new Gson().toJson(anonymousMultiredditSubreddits); boolean res17 = saveDatabaseTableToFile(anonymousMultiredditSubredditsJson, databaseDirFile.getAbsolutePath(), "/anonymous_multireddit_subreddits.json"); List customThemes = redditDataRoomDatabase.customThemeDao().getAllCustomThemesList(); String customThemesJson = new Gson().toJson(customThemes); boolean res18 = saveDatabaseTableToFile(customThemesJson, databaseDirFile.getAbsolutePath(), "/custom_themes.json"); List postFilters = redditDataRoomDatabase.postFilterDao().getAllPostFilters(); String postFiltersJson = new Gson().toJson(postFilters); boolean res19 = saveDatabaseTableToFile(postFiltersJson, databaseDirFile.getAbsolutePath(), "/post_filters.json"); List postFilterUsage = redditDataRoomDatabase.postFilterUsageDao().getAllPostFilterUsageForBackup(); String postFilterUsageJson = new Gson().toJson(postFilterUsage); boolean res20 = saveDatabaseTableToFile(postFilterUsageJson, databaseDirFile.getAbsolutePath(), "/post_filter_usage.json"); List commentFilters = redditDataRoomDatabase.commentFilterDao().getAllCommentFilters(); String commentFiltersJson = new Gson().toJson(commentFilters); boolean res21 = saveDatabaseTableToFile(commentFiltersJson, databaseDirFile.getAbsolutePath(), "/comment_filters.json"); List commentFilterUsage = redditDataRoomDatabase.commentFilterUsageDao().getAllCommentFilterUsageForBackup(); String commentFilterUsageJson = new Gson().toJson(commentFilterUsage); boolean res22 = saveDatabaseTableToFile(commentFilterUsageJson, databaseDirFile.getAbsolutePath(), "/comment_filter_usage.json"); List accounts = redditDataRoomDatabase.accountDao().getAllAccounts(); String accountsJson = new Gson().toJson(accounts); boolean res23 = saveDatabaseTableToFile(accountsJson, databaseDirFile.getAbsolutePath(), "/accounts.json"); boolean zipRes = zipAndMoveToDestinationDir(context, cacheDir, contentResolver, destinationDirUri, password); try { FileUtils.deleteDirectory(new File(cacheDir + "/Backup/")); } catch (IOException e) { e.printStackTrace(); } handler.post(() -> { boolean finalResult = res && res1 && res2 && res3 && res4 && res5 && res6 && res7 && res8 && res9 && res10 && res11 && res12 && res13 && res14 && res15 && res16 && res17 && res18 && res19 && res20 && res21 && res22 && res23 && zipRes && resPrivate; if (finalResult) { backupSettingsListener.success(); } else { if (!zipRes) { backupSettingsListener.failed(context.getText(R.string.create_zip_in_destination_directory_failed).toString()); } else { backupSettingsListener.failed(context.getText(R.string.backup_some_settings_failed).toString()); } } }); }); } private static boolean saveMapToFile(Map mapToSave, String backupDir, String fileName) { boolean result = false; ObjectOutputStream output = null; try { output = new ObjectOutputStream(new FileOutputStream(backupDir + "/" + fileName + ".txt")); output.writeObject(mapToSave); result = true; } catch (IOException e) { e.printStackTrace(); } finally { try { if (output != null) { output.flush(); } } catch (IOException ex) { ex.printStackTrace(); } } return result; } private static boolean saveDatabaseTableToFile(String dataJson, String backupDir, String fileName) { File anonymousSubscribedSubredditsFile = new File(backupDir + fileName); try { anonymousSubscribedSubredditsFile.createNewFile(); try (PrintWriter out = new PrintWriter(anonymousSubscribedSubredditsFile.getAbsolutePath())) { out.println(dataJson); } catch (IOException e) { e.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } return true; } private static boolean zipAndMoveToDestinationDir(Context context, File cacheDir, ContentResolver contentResolver, Uri destinationDirUri, String password) { OutputStream outputStream = null; boolean result = false; try { String time = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(System.currentTimeMillis())); String fileName = "Continuum_Settings_Backup_v" + BuildConfig.VERSION_NAME + "-" + BuildConfig.VERSION_CODE + "-" + time + ".zip"; String filePath = cacheDir + "/Backup/" + fileName; ZipFile zip = new ZipFile(filePath, password.toCharArray()); ZipParameters zipParameters = new ZipParameters(); zipParameters.setEncryptFiles(true); zipParameters.setEncryptionMethod(EncryptionMethod.AES); zip.addFolder(new File(cacheDir + "/Backup/" + BuildConfig.VERSION_NAME + "/"), zipParameters); DocumentFile dir = DocumentFile.fromTreeUri(context, destinationDirUri); if (dir == null) { return false; } DocumentFile checkForDuplicate = dir.findFile(fileName); if (checkForDuplicate != null) { checkForDuplicate.delete(); } DocumentFile destinationFile = dir.createFile("application/zip", fileName); if (destinationFile == null) { return false; } outputStream = contentResolver.openOutputStream(destinationFile.getUri()); if (outputStream == null) { return false; } byte[] fileReader = new byte[1024]; FileInputStream inputStream = new FileInputStream(filePath); while (true) { int read = inputStream.read(fileReader); if (read == -1) { break; } outputStream.write(fileReader, 0, read); } result = true; } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.flush(); } catch (IOException e) { e.printStackTrace(); } } } return result; } public interface BackupSettingsListener { void success(); void failed(String errorMessage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/ChangeThemeName.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class ChangeThemeName { public static void changeThemeName(Executor executor, RedditDataRoomDatabase redditDataRoomDatabase, String oldName, String newName) { executor.execute(() -> { redditDataRoomDatabase.customThemeDao().updateName(oldName, newName); }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/CheckIsFollowingUser.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import androidx.annotation.NonNull; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; public class CheckIsFollowingUser { public static void checkIsFollowingUser(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String username, @NonNull String accountName, CheckIsFollowingUserListener checkIsFollowingUserListener) { executor.execute(() -> { SubscribedUserData subscribedUserData = redditDataRoomDatabase.subscribedUserDao().getSubscribedUser(username, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : accountName); handler.post(() -> { if (subscribedUserData != null) { checkIsFollowingUserListener.isSubscribed(); } else { checkIsFollowingUserListener.isNotSubscribed(); } }); }); } public interface CheckIsFollowingUserListener { void isSubscribed(); void isNotSubscribed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/CheckIsSubscribedToSubreddit.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import androidx.annotation.NonNull; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; public class CheckIsSubscribedToSubreddit { public static void checkIsSubscribedToSubreddit(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String subredditName, @NonNull String accountName, CheckIsSubscribedToSubredditListener checkIsSubscribedToSubredditListener) { executor.execute(() -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (!redditDataRoomDatabase.accountDao().isAnonymousAccountInserted()) { redditDataRoomDatabase.accountDao().insert(Account.getAnonymousAccount()); } } SubscribedSubredditData subscribedSubredditData = redditDataRoomDatabase.subscribedSubredditDao().getSubscribedSubreddit(subredditName, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : accountName); handler.post(() -> { if (subscribedSubredditData != null) { checkIsSubscribedToSubredditListener.isSubscribed(); } else { checkIsSubscribedToSubredditListener.isNotSubscribed(); } }); }); } public interface CheckIsSubscribedToSubredditListener { void isSubscribed(); void isNotSubscribed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteAllPostLayouts.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.content.SharedPreferences; import android.os.Handler; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class DeleteAllPostLayouts { public static void deleteAllPostLayouts(Executor executor, Handler handler, SharedPreferences defaultSharedPreferences, SharedPreferences postLayoutSharedPreferences, DeleteAllPostLayoutsAsyncTaskListener deleteAllPostLayoutsAsyncTaskListener) { executor.execute(() -> { Map keys = defaultSharedPreferences.getAll(); SharedPreferences.Editor editor = defaultSharedPreferences.edit(); for (Map.Entry entry : keys.entrySet()) { String key = entry.getKey(); if (key.startsWith(SharedPreferencesUtils.POST_LAYOUT_SHARED_PREFERENCES_FILE) || key.startsWith(SharedPreferencesUtils.POST_LAYOUT_FRONT_PAGE_POST) || key.startsWith(SharedPreferencesUtils.POST_LAYOUT_POPULAR_POST_LEGACY) || key.startsWith(SharedPreferencesUtils.POST_LAYOUT_ALL_POST_LEGACY) || key.startsWith(SharedPreferencesUtils.POST_LAYOUT_SUBREDDIT_POST_BASE) || key.startsWith(SharedPreferencesUtils.POST_LAYOUT_MULTI_REDDIT_POST_BASE) || key.startsWith(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE) || key.startsWith(SharedPreferencesUtils.POST_LAYOUT_SEARCH_POST)) { editor.remove(key); } } editor.apply(); postLayoutSharedPreferences.edit().clear().apply(); handler.post(deleteAllPostLayoutsAsyncTaskListener::success); }); } public interface DeleteAllPostLayoutsAsyncTaskListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteAllReadPosts.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeleteAllReadPosts { public static void deleteAllReadPosts(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, DeleteAllReadPostsAsyncTaskListener deleteAllReadPostsAsyncTaskListener) { executor.execute(() -> { redditDataRoomDatabase.readPostDao().deleteAllReadPosts(); handler.post(deleteAllReadPostsAsyncTaskListener::success); }); } public interface DeleteAllReadPostsAsyncTaskListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteAllSortTypes.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.content.SharedPreferences; import android.os.Handler; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class DeleteAllSortTypes { public static void deleteAllSortTypes(Executor executor, Handler handler, SharedPreferences defaultSharedPreferences, SharedPreferences sortTypeSharedPreferences, DeleteAllSortTypesAsyncTaskListener deleteAllSortTypesAsyncTaskListener) { executor.execute(() -> { Map keys = defaultSharedPreferences.getAll(); SharedPreferences.Editor editor = defaultSharedPreferences.edit(); for (Map.Entry entry : keys.entrySet()) { String key = entry.getKey(); if (key.contains(SharedPreferencesUtils.SORT_TYPE_BEST_POST) || key.contains(SharedPreferencesUtils.SORT_TIME_BEST_POST) || key.contains(SharedPreferencesUtils.SORT_TYPE_ALL_POST_LEGACY) || key.contains(SharedPreferencesUtils.SORT_TIME_ALL_POST_LEGACY) || key.contains(SharedPreferencesUtils.SORT_TYPE_POPULAR_POST_LEGACY) || key.contains(SharedPreferencesUtils.SORT_TIME_POPULAR_POST_LEGACY) || key.contains(SharedPreferencesUtils.SORT_TYPE_SEARCH_POST) || key.contains(SharedPreferencesUtils.SORT_TIME_SEARCH_POST) || key.contains(SharedPreferencesUtils.SORT_TYPE_SUBREDDIT_POST_BASE) || key.contains(SharedPreferencesUtils.SORT_TIME_SUBREDDIT_POST_BASE) || key.contains(SharedPreferencesUtils.SORT_TYPE_MULTI_REDDIT_POST_BASE) || key.contains(SharedPreferencesUtils.SORT_TIME_MULTI_REDDIT_POST_BASE) || key.contains(SharedPreferencesUtils.SORT_TYPE_USER_POST_BASE) || key.contains(SharedPreferencesUtils.SORT_TIME_USER_POST_BASE) || key.contains(SharedPreferencesUtils.SORT_TYPE_USER_COMMENT) || key.contains(SharedPreferencesUtils.SORT_TIME_USER_COMMENT) || key.contains(SharedPreferencesUtils.SORT_TYPE_SEARCH_SUBREDDIT) || key.contains(SharedPreferencesUtils.SORT_TYPE_SEARCH_USER) || key.contains(SharedPreferencesUtils.SORT_TYPE_POST_COMMENT)) { editor.remove(key); } } editor.apply(); sortTypeSharedPreferences.edit().clear().apply(); handler.post(deleteAllSortTypesAsyncTaskListener::success); }); } public interface DeleteAllSortTypesAsyncTaskListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteAllSubreddits.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeleteAllSubreddits { public static void deleteAllSubreddits(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, DeleteAllSubredditsAsyncTaskListener deleteAllSubredditsAsyncTaskListener) { executor.execute(() -> { redditDataRoomDatabase.subredditDao().deleteAllSubreddits(); handler.post(deleteAllSubredditsAsyncTaskListener::success); }); } public interface DeleteAllSubredditsAsyncTaskListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteAllThemes.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.content.SharedPreferences; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeleteAllThemes { public static void deleteAllThemes(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences, DeleteAllThemesListener deleteAllThemesListener) { executor.execute(() -> { redditDataRoomDatabase.customThemeDao().deleteAllCustomThemes(); lightThemeSharedPreferences.edit().clear().apply(); darkThemeSharedPreferences.edit().clear().apply(); amoledThemeSharedPreferences.edit().clear().apply(); handler.post(deleteAllThemesListener::success); }); } public interface DeleteAllThemesListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteAllUsers.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeleteAllUsers { public static void deleteAllUsers(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, DeleteAllUsersListener deleteAllUsersListener) { executor.execute(() -> { redditDataRoomDatabase.userDao().deleteAllUsers(); handler.post(deleteAllUsersListener::success); }); } public interface DeleteAllUsersListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteMultiredditInDatabase.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import androidx.annotation.NonNull; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; public class DeleteMultiredditInDatabase { public static void deleteMultiredditInDatabase(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, @NonNull String accountName, String multipath, DeleteMultiredditInDatabaseListener deleteMultiredditInDatabaseListener) { executor.execute(() -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { redditDataRoomDatabase.multiRedditDao().anonymousDeleteMultiReddit(multipath); } else { redditDataRoomDatabase.multiRedditDao().deleteMultiReddit(multipath, accountName); } handler.post(deleteMultiredditInDatabaseListener::success); }); } public interface DeleteMultiredditInDatabaseListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/DeleteTheme.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; public class DeleteTheme { public static void deleteTheme(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String themeName, DeleteThemeListener deleteThemeListener) { executor.execute(() -> { CustomTheme customTheme = redditDataRoomDatabase.customThemeDao().getCustomTheme(themeName); if (customTheme != null) { boolean isLightTheme = customTheme.isLightTheme; boolean isDarkTheme = customTheme.isDarkTheme; boolean isAmoledTheme = customTheme.isAmoledTheme; redditDataRoomDatabase.customThemeDao().deleteCustomTheme(themeName); handler.post(() -> deleteThemeListener.success(isLightTheme, isDarkTheme, isAmoledTheme)); } else { handler.post(() -> deleteThemeListener.success(false, false, false)); } }); } public interface DeleteThemeListener { void success(boolean isLightTheme, boolean isDarkTheme, boolean isAmoledTheme); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/GetCustomTheme.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; public class GetCustomTheme { public static void getCustomTheme(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String customThemeName, GetCustomThemeListener getCustomThemeListener) { executor.execute(() -> { CustomTheme customTheme = redditDataRoomDatabase.customThemeDao().getCustomTheme(customThemeName); handler.post(() -> getCustomThemeListener.success(customTheme)); }); } public static void getCustomTheme(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, int themeType, GetCustomThemeListener getCustomThemeListener) { executor.execute(() -> { CustomTheme customTheme; switch (themeType) { case CustomThemeSharedPreferencesUtils.DARK: customTheme = redditDataRoomDatabase.customThemeDao().getDarkCustomTheme(); break; case CustomThemeSharedPreferencesUtils.AMOLED: customTheme = redditDataRoomDatabase.customThemeDao().getAmoledCustomTheme(); break; default: customTheme = redditDataRoomDatabase.customThemeDao().getLightCustomTheme(); } handler.post(() -> getCustomThemeListener.success(customTheme)); }); } public interface GetCustomThemeListener { void success(CustomTheme customTheme); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/InsertCustomTheme.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.content.SharedPreferences; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; public class InsertCustomTheme { public static void insertCustomTheme(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences, CustomTheme customTheme, boolean checkDuplicate, InsertCustomThemeListener insertCustomThemeListener) { executor.execute(() -> { CustomTheme previousTheme = redditDataRoomDatabase.customThemeDao().getCustomTheme(customTheme.name); if (checkDuplicate && previousTheme != null) { handler.post(insertCustomThemeListener::duplicate); } else { if (customTheme.isLightTheme) { redditDataRoomDatabase.customThemeDao().unsetLightTheme(); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(customTheme, lightThemeSharedPreferences); } else if (previousTheme != null && previousTheme.isLightTheme) { lightThemeSharedPreferences.edit().clear().apply(); } if (customTheme.isDarkTheme) { redditDataRoomDatabase.customThemeDao().unsetDarkTheme(); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(customTheme, darkThemeSharedPreferences); } else if (previousTheme != null && previousTheme.isDarkTheme) { darkThemeSharedPreferences.edit().clear().apply(); } if (customTheme.isAmoledTheme) { redditDataRoomDatabase.customThemeDao().unsetAmoledTheme(); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(customTheme, amoledThemeSharedPreferences); } else if (previousTheme != null && previousTheme.isAmoledTheme) { amoledThemeSharedPreferences.edit().clear().apply(); } redditDataRoomDatabase.customThemeDao().insert(customTheme); handler.post(insertCustomThemeListener::success); } }); } public interface InsertCustomThemeListener { void success(); default void duplicate() {} } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/InsertMultireddit.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubreddit; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.multireddit.MultiRedditDao; public class InsertMultireddit { public static void insertMultireddits(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, ArrayList multiReddits, @NonNull String accountName, InsertMultiRedditListener insertMultiRedditListener) { executor.execute(() -> { MultiRedditDao multiRedditDao = redditDataRoomDatabase.multiRedditDao(); List existingMultiReddits = multiRedditDao.getAllMultiRedditsList(accountName); Collections.sort(multiReddits, (multiReddit, t1) -> multiReddit.getName().compareToIgnoreCase(t1.getName())); List deletedMultiredditNames = new ArrayList<>(); compareTwoMultiRedditList(multiReddits, existingMultiReddits, deletedMultiredditNames); for (String deleted : deletedMultiredditNames) { multiRedditDao.deleteMultiReddit(deleted, accountName); } for (MultiReddit multiReddit : multiReddits) { multiRedditDao.insert(multiReddit); } handler.post(insertMultiRedditListener::success); }); } public static void insertMultireddit(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, MultiReddit multiReddit, InsertMultiRedditListener insertMultiRedditListener) { executor.execute(() -> { if (multiReddit.getOwner().equals(Account.ANONYMOUS_ACCOUNT)) { ArrayList allAnonymousMultiRedditSubreddits = (ArrayList) redditDataRoomDatabase.anonymousMultiredditSubredditDao().getAllAnonymousMultiRedditSubreddits(multiReddit.getPath()); redditDataRoomDatabase.multiRedditDao().insert(multiReddit); if (allAnonymousMultiRedditSubreddits != null) { redditDataRoomDatabase.anonymousMultiredditSubredditDao().insertAll(allAnonymousMultiRedditSubreddits); } } else { redditDataRoomDatabase.multiRedditDao().insert(multiReddit); } handler.post(insertMultiRedditListener::success); }); } private static void compareTwoMultiRedditList(List newMultiReddits, List oldMultiReddits, List deletedMultiReddits) { int newIndex = 0; for (int oldIndex = 0; oldIndex < oldMultiReddits.size(); oldIndex++) { if (newIndex >= newMultiReddits.size()) { for (; oldIndex < oldMultiReddits.size(); oldIndex++) { deletedMultiReddits.add(oldMultiReddits.get(oldIndex).getName()); } return; } MultiReddit old = oldMultiReddits.get(oldIndex); for (; newIndex < newMultiReddits.size(); newIndex++) { if (newMultiReddits.get(newIndex).getName().compareToIgnoreCase(old.getName()) == 0) { newIndex++; break; } if (newMultiReddits.get(newIndex).getName().compareToIgnoreCase(old.getName()) > 0) { deletedMultiReddits.add(old.getName()); break; } } } } public interface InsertMultiRedditListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/InsertSubredditData.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; public class InsertSubredditData { public static void insertSubredditData(Executor executor, Handler handler, RedditDataRoomDatabase db, SubredditData subredditData, InsertSubredditDataAsyncTaskListener insertSubredditDataAsyncTaskListener) { executor.execute(() -> { db.subredditDao().insert(subredditData); handler.post(insertSubredditDataAsyncTaskListener::insertSuccess); }); } public interface InsertSubredditDataAsyncTaskListener { void insertSuccess(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/InsertSubscribedThings.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.subreddit.SubredditDao; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditDao; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserDao; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; public class InsertSubscribedThings { public static void insertSubscribedThings(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, @NonNull String accountName, List subscribedSubredditDataList, List subscribedUserDataList, List subredditDataList, InsertSubscribedThingListener insertSubscribedThingListener) { executor.execute(() -> { if (!accountName.equals(Account.ANONYMOUS_ACCOUNT) && redditDataRoomDatabase.accountDao().getAccountData(accountName) == null) { handler.post(insertSubscribedThingListener::insertSuccess); return; } SubscribedSubredditDao subscribedSubredditDao = redditDataRoomDatabase.subscribedSubredditDao(); SubscribedUserDao subscribedUserDao = redditDataRoomDatabase.subscribedUserDao(); SubredditDao subredditDao = redditDataRoomDatabase.subredditDao(); if (subscribedSubredditDataList != null) { List existingSubscribedSubredditDataList = subscribedSubredditDao.getAllSubscribedSubredditsList(accountName); Collections.sort(subscribedSubredditDataList, (subscribedSubredditData, t1) -> subscribedSubredditData.getName().compareToIgnoreCase(t1.getName())); List unsubscribedSubreddits = new ArrayList<>(); compareTwoSubscribedSubredditList(subscribedSubredditDataList, existingSubscribedSubredditDataList, unsubscribedSubreddits); for (String unsubscribed : unsubscribedSubreddits) { subscribedSubredditDao.deleteSubscribedSubreddit(unsubscribed, accountName); } for (SubscribedSubredditData s : subscribedSubredditDataList) { subscribedSubredditDao.insert(s); } } if (subscribedUserDataList != null) { List existingSubscribedUserDataList = subscribedUserDao.getAllSubscribedUsersList(accountName); Collections.sort(subscribedUserDataList, (subscribedUserData, t1) -> subscribedUserData.getName().compareToIgnoreCase(t1.getName())); List unsubscribedUsers = new ArrayList<>(); compareTwoSubscribedUserList(subscribedUserDataList, existingSubscribedUserDataList, unsubscribedUsers); for (String unsubscribed : unsubscribedUsers) { subscribedUserDao.deleteSubscribedUser(unsubscribed, accountName); } for (SubscribedUserData s : subscribedUserDataList) { subscribedUserDao.insert(s); } } if (subredditDataList != null) { for (SubredditData s : subredditDataList) { subredditDao.insert(s); } } handler.post(insertSubscribedThingListener::insertSuccess); }); } public static void insertSubscribedThings(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, SubscribedSubredditData singleSubscribedSubredditData, InsertSubscribedThingListener insertSubscribedThingListener) { executor.execute(() -> { String accountName = singleSubscribedSubredditData.getUsername(); if (redditDataRoomDatabase.accountDao().getAccountData(accountName) == null) { handler.post(insertSubscribedThingListener::insertSuccess); return; } redditDataRoomDatabase.subscribedSubredditDao().insert(singleSubscribedSubredditData); handler.post(insertSubscribedThingListener::insertSuccess); }); } public static void insertSubscribedThings(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, SubscribedUserData singleSubscribedUserData, InsertSubscribedThingListener insertSubscribedThingListener) { executor.execute(() -> { String accountName = singleSubscribedUserData.getUsername(); if (redditDataRoomDatabase.accountDao().getAccountData(accountName) == null) { handler.post(insertSubscribedThingListener::insertSuccess); return; } redditDataRoomDatabase.subscribedUserDao().insert(singleSubscribedUserData); handler.post(insertSubscribedThingListener::insertSuccess); }); } private static void compareTwoSubscribedSubredditList(List newSubscribedSubreddits, List oldSubscribedSubreddits, List unsubscribedSubredditNames) { int newIndex = 0; for (int oldIndex = 0; oldIndex < oldSubscribedSubreddits.size(); oldIndex++) { if (newIndex >= newSubscribedSubreddits.size()) { for (; oldIndex < oldSubscribedSubreddits.size(); oldIndex++) { unsubscribedSubredditNames.add(oldSubscribedSubreddits.get(oldIndex).getName()); } return; } SubscribedSubredditData old = oldSubscribedSubreddits.get(oldIndex); for (; newIndex < newSubscribedSubreddits.size(); newIndex++) { if (newSubscribedSubreddits.get(newIndex).getName().compareToIgnoreCase(old.getName()) == 0) { newIndex++; break; } if (newSubscribedSubreddits.get(newIndex).getName().compareToIgnoreCase(old.getName()) > 0) { unsubscribedSubredditNames.add(old.getName()); break; } } } } private static void compareTwoSubscribedUserList(List newSubscribedUsers, List oldSubscribedUsers, List unsubscribedUserNames) { int newIndex = 0; for (int oldIndex = 0; oldIndex < oldSubscribedUsers.size(); oldIndex++) { if (newIndex >= newSubscribedUsers.size()) { for (; oldIndex < oldSubscribedUsers.size(); oldIndex++) { unsubscribedUserNames.add(oldSubscribedUsers.get(oldIndex).getName()); } return; } SubscribedUserData old = oldSubscribedUsers.get(oldIndex); for (; newIndex < newSubscribedUsers.size(); newIndex++) { if (newSubscribedUsers.get(newIndex).getName().compareToIgnoreCase(old.getName()) == 0) { newIndex++; break; } if (newSubscribedUsers.get(newIndex).getName().compareToIgnoreCase(old.getName()) > 0) { unsubscribedUserNames.add(old.getName()); break; } } } } public interface InsertSubscribedThingListener { void insertSuccess(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/InsertUserData.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.user.UserData; public class InsertUserData { public static void insertUserData(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, UserData userData, InsertUserDataListener insertUserDataListener) { executor.execute(() -> { if (redditDataRoomDatabase.userDao().getNUsers() > 10000) { redditDataRoomDatabase.userDao().deleteAllUsers(); } redditDataRoomDatabase.userDao().insert(userData); if (insertUserDataListener != null) { handler.post(insertUserDataListener::insertSuccess); } }); } public interface InsertUserDataListener { void insertSuccess(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/LoadSubredditIcon.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.subreddit.FetchSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditDao; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import retrofit2.Retrofit; public class LoadSubredditIcon { public static void loadSubredditIcon(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String subredditName, String accessToken, @NonNull String accountName, Retrofit oauthRetrofit, Retrofit retrofit, LoadSubredditIconListener loadSubredditIconListener) { executor.execute(() -> { SubredditDao subredditDao = redditDataRoomDatabase.subredditDao(); SubredditData subredditData = subredditDao.getSubredditData(subredditName); if (subredditData != null) { String iconImageUrl = subredditDao.getSubredditData(subredditName).getIconUrl(); handler.post(() -> loadSubredditIconListener.loadIconSuccess(iconImageUrl)); } else { handler.post(() -> FetchSubredditData.fetchSubredditData(executor, handler, accountName.equals(Account.ANONYMOUS_ACCOUNT) ? null : oauthRetrofit, retrofit, subredditName, accessToken, new FetchSubredditData.FetchSubredditDataListener() { @Override public void onFetchSubredditDataSuccess(SubredditData subredditData1, int nCurrentOnlineSubscribers) { ArrayList singleSubredditDataList = new ArrayList<>(); singleSubredditDataList.add(subredditData1); InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, accountName, null, null, singleSubredditDataList, () -> loadSubredditIconListener.loadIconSuccess(subredditData1.getIconUrl())); } @Override public void onFetchSubredditDataFail(boolean isQuarantined) { loadSubredditIconListener.loadIconSuccess(null); } })); } }); } public interface LoadSubredditIconListener { void loadIconSuccess(String iconImageUrl); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/LoadUserData.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import androidx.annotation.Nullable; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.user.FetchUserData; import ml.docilealligator.infinityforreddit.user.UserDao; import ml.docilealligator.infinityforreddit.user.UserData; import retrofit2.Retrofit; public class LoadUserData { public static void loadUserData(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String accessToken, String userName, @Nullable Retrofit oauthRetrofit, Retrofit retrofit, LoadUserDataAsyncTaskListener loadUserDataAsyncTaskListener) { executor.execute(() -> { UserDao userDao = redditDataRoomDatabase.userDao(); UserData userData = userDao.getUserData(userName); if (userData != null) { String iconImageUrl = userData.getIconUrl(); handler.post(() -> loadUserDataAsyncTaskListener.loadUserDataSuccess(iconImageUrl)); } else { handler.post(() -> FetchUserData.fetchUserData(executor, handler, redditDataRoomDatabase, oauthRetrofit, retrofit, accessToken, userName, new FetchUserData.FetchUserDataListener() { @Override public void onFetchUserDataSuccess(UserData userData, int inboxCount) { InsertUserData.insertUserData(executor, handler, redditDataRoomDatabase, userData, () -> loadUserDataAsyncTaskListener.loadUserDataSuccess(userData.getIconUrl())); } @Override public void onFetchUserDataFailed() { loadUserDataAsyncTaskListener.loadUserDataSuccess(null); } })); } }); } public interface LoadUserDataAsyncTaskListener { void loadUserDataSuccess(String iconImageUrl); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/ParseAndInsertNewAccount.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.account.AccountDao; public class ParseAndInsertNewAccount { public static void parseAndInsertNewAccount(Executor executor, Handler handler, String username, String accessToken, String refreshToken, String profileImageUrl, String bannerImageUrl, int karma, boolean isMod, String code, AccountDao accountDao, ParseAndInsertAccountListener parseAndInsertAccountListener) { executor.execute(() -> { Account account = new Account(username, accessToken, refreshToken, code, profileImageUrl, bannerImageUrl, karma, true, isMod); accountDao.markAllAccountsNonCurrent(); accountDao.insert(account); handler.post(parseAndInsertAccountListener::success); }); } public interface ParseAndInsertAccountListener { void success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/RestoreSettings.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.os.Handler; import androidx.annotation.Nullable; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import net.lingala.zip4j.ZipFile; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubreddit; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.AppRestartHelper; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class RestoreSettings { public static void restoreSettings(Context context, Executor executor, Handler handler, ContentResolver contentResolver, Uri zipFileUri, String password, RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences defaultSharedPreferences, SharedPreferences currentAccountSharedPreferences, SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences, SharedPreferences sortTypeSharedPreferences, SharedPreferences postLayoutSharedPreferences, SharedPreferences postDetailsSharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, SharedPreferences mainActivityTabsSharedPreferences, SharedPreferences proxySharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferencs, SharedPreferences bottomAppBarSharedPreferences, SharedPreferences postHistorySharedPreferences, SharedPreferences navigationDrawerSharedPreferences, RestoreSettingsListener restoreSettingsListener) { executor.execute(() -> { try { InputStream zipFileInputStream = contentResolver.openInputStream(zipFileUri); if (zipFileInputStream == null) { handler.post(() -> restoreSettingsListener.failed(context.getString(R.string.restore_settings_failed_cannot_get_file))); return; } File cacheDir = Utils.getCacheDir(context); if (cacheDir == null) { handler.post(() -> restoreSettingsListener.failed(context.getString(R.string.restore_settings_failed_cannot_get_cache_dir))); return; } String cachePath = cacheDir + "/Restore/"; if (new File(cachePath).exists()) { FileUtils.deleteDirectory(new File(cachePath)); } new File(cachePath).mkdir(); FileOutputStream zipCacheOutputStream = new FileOutputStream(new File(cachePath + "restore.zip")); byte[] fileReader = new byte[1024]; while (true) { int read = zipFileInputStream.read(fileReader); if (read == -1) { break; } zipCacheOutputStream.write(fileReader, 0, read); } new ZipFile(cachePath + "restore.zip", password.toCharArray()).extractAll(cachePath); new File(cachePath + "restore.zip").delete(); File[] files = new File(cachePath).listFiles(); if (files == null || files.length <= 0) { handler.post(() -> restoreSettingsListener.failedWithWrongPassword(context.getString(R.string.restore_settings_failed_file_corrupted))); } else { File restoreFilesDir = files[0]; File[] restoreFiles = restoreFilesDir.listFiles(); boolean result = true; if (restoreFiles != null) { SharedPreferences defaultPrefsPrivate = context.getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); for (File f : restoreFiles) { if (f.isFile()) { if (f.getName().equals(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE + "_private.txt")) { result = result & importSharedPreferencsFromFile(defaultPrefsPrivate, f.toString()); } else if (f.getName().equals(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE + ".txt")) { result = result & importSharedPreferencsFromFile(defaultSharedPreferences, f.toString()); } else if (f.getName().startsWith(CustomThemeSharedPreferencesUtils.LIGHT_THEME_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(lightThemeSharedPreferences, f.toString()); } else if (f.getName().startsWith(CustomThemeSharedPreferencesUtils.DARK_THEME_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(darkThemeSharedPreferences, f.toString()); } else if (f.getName().startsWith(CustomThemeSharedPreferencesUtils.AMOLED_THEME_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(amoledThemeSharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.SORT_TYPE_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(sortTypeSharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.POST_LAYOUT_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(postLayoutSharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.POST_DETAILS_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(postDetailsSharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(postFeedScrolledPositionSharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.MAIN_PAGE_TABS_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(mainActivityTabsSharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.PROXY_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(proxySharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.NSFW_AND_SPOILER_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(nsfwAndSpoilerSharedPreferencs, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.BOTTOM_APP_BAR_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(bottomAppBarSharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.POST_HISTORY_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(postHistorySharedPreferences, f.toString()); } else if (f.getName().startsWith(SharedPreferencesUtils.NAVIGATION_DRAWER_SHARED_PREFERENCES_FILE)) { result = result & importSharedPreferencsFromFile(navigationDrawerSharedPreferences, f.toString()); } } else if (f.isDirectory() && f.getName().equals("database")) { if (!redditDataRoomDatabase.accountDao().isAnonymousAccountInserted()) { redditDataRoomDatabase.accountDao().insert(Account.getAnonymousAccount()); } File anonymousSubscribedSubredditsFile = new File(f.getAbsolutePath() + "/anonymous_subscribed_subreddits.json"); File anonymousSubscribedUsersFile = new File(f.getAbsolutePath() + "/anonymous_subscribed_users.json"); File anonymousMultiredditsFile = new File(f.getAbsolutePath() + "/anonymous_multireddits.json"); File anonymousMultiredditSubredditsFile = new File(f.getAbsolutePath() + "/anonymous_multireddit_subreddits.json"); File customThemesFile = new File(f.getAbsolutePath() + "/custom_themes.json"); File postFiltersFile = new File(f.getAbsolutePath() + "/post_filters.json"); File postFilterUsageFile = new File(f.getAbsolutePath() + "/post_filter_usage.json"); File commentFiltersFile = new File(f.getAbsolutePath() + "/comment_filters.json"); File commentFilterUsageFile = new File(f.getAbsolutePath() + "/comment_filter_usage.json"); File accountsFile = new File(f.getAbsolutePath() + "/accounts.json"); if (anonymousSubscribedSubredditsFile.exists()) { List anonymousSubscribedSubreddits = getListFromFile(anonymousSubscribedSubredditsFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.subscribedSubredditDao().insertAll(anonymousSubscribedSubreddits); } if (anonymousSubscribedUsersFile.exists()) { List anonymousSubscribedUsers = getListFromFile(anonymousSubscribedUsersFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.subscribedUserDao().insertAll(anonymousSubscribedUsers); } if (anonymousMultiredditsFile.exists()) { List anonymousMultireddits = getListFromFile(anonymousMultiredditsFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.multiRedditDao().insertAll(anonymousMultireddits); if (anonymousMultiredditSubredditsFile.exists()) { List anonymousMultiredditSubreddits = getListFromFile(anonymousMultiredditSubredditsFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.anonymousMultiredditSubredditDao().insertAll(anonymousMultiredditSubreddits); } } if (customThemesFile.exists()) { List customThemes = getListFromFile(customThemesFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.customThemeDao().insertAll(customThemes); } if (postFiltersFile.exists()) { List postFilters = getListFromFile(postFiltersFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.postFilterDao().insertAll(postFilters); if (postFilterUsageFile.exists()) { List postFilterUsage = getListFromFile(postFilterUsageFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.postFilterUsageDao().insertAll(postFilterUsage); } } if (commentFiltersFile.exists()) { List commentFilters = getListFromFile(commentFiltersFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.commentFilterDao().insertAll(commentFilters); if (commentFilterUsageFile.exists()) { List commentFilterUsage = getListFromFile(commentFilterUsageFile, new TypeToken>() {}.getType()); redditDataRoomDatabase.commentFilterUsageDao().insertAll(commentFilterUsage); } } if (accountsFile.exists()) { List accounts = getListFromFile(accountsFile, new TypeToken>() {}.getType()); if (accounts != null) { // Clear existing accounts before inserting restored ones redditDataRoomDatabase.accountDao().deleteAllAccounts(); for (Account account : accounts) { redditDataRoomDatabase.accountDao().insert(account); } // Optionally, mark the first restored account as current, or restore the 'is_current_user' flag if it was backed up. // If accounts list is not empty, mark the first one as current if (!accounts.isEmpty()) { redditDataRoomDatabase.accountDao().markAccountCurrent(accounts.get(0).getAccountName()); // Also update the current account shared preferences for immediate effect Account firstAccount = accounts.get(0); currentAccountSharedPreferences.edit() .putString(SharedPreferencesUtils.ACCOUNT_NAME, firstAccount.getAccountName()) .putString(SharedPreferencesUtils.ACCESS_TOKEN, firstAccount.getAccessToken()) .putString(SharedPreferencesUtils.ACCOUNT_IMAGE_URL, firstAccount.getProfileImageUrl()) .apply(); } } } } } } else { handler.post(() -> restoreSettingsListener.failed(context.getString(R.string.restore_settings_failed_file_corrupted))); } FileUtils.deleteDirectory(new File(cachePath)); if (result) { handler.post(() -> { restoreSettingsListener.success(); try { Thread.sleep(2000); } catch (InterruptedException e) { // Restore the interrupted status Thread.currentThread().interrupt(); // Optionally log the interruption android.util.Log.w("RestoreSettings", "Sleep interrupted before app restart", e); } // Trigger restart after posting success message AppRestartHelper.triggerAppRestart(context); }); } else { handler.post(() -> restoreSettingsListener.failed(context.getString(R.string.restore_settings_partially_failed))); } } } catch (IOException e) { e.printStackTrace(); if (e instanceof net.lingala.zip4j.exception.ZipException && e.getMessage() != null && e.getMessage().contains("Wrong Password")) { handler.post(() -> restoreSettingsListener.failedWithWrongPassword(context.getString(R.string.restore_settings_failed_wrong_password))); } else { handler.post(() -> restoreSettingsListener.failed(context.getString(R.string.restore_settings_partially_failed))); } } }); } private static boolean importSharedPreferencsFromFile(SharedPreferences sharedPreferences, String uriString) { boolean result = false; ObjectInputStream input = null; try { input = new ObjectInputStream(new FileInputStream(uriString)); Object object = input.readObject(); if (object instanceof Map) { Map map = (Map) object; Set> entrySet = map.entrySet(); SharedPreferences.Editor editor = sharedPreferences.edit(); for (Map.Entry e : entrySet) { if (e.getValue() instanceof String) { editor.putString(e.getKey(), (String) e.getValue()); } else if (e.getValue() instanceof Integer) { editor.putInt(e.getKey(), (Integer) e.getValue()); } else if (e.getValue() instanceof Float) { editor.putFloat(e.getKey(), (Float) e.getValue()); } else if (e.getValue() instanceof Boolean) { editor.putBoolean(e.getKey(), (Boolean) e.getValue()); } else if (e.getValue() instanceof Long) { editor.putLong(e.getKey(), (Long) e.getValue()); } } editor.apply(); result = true; } } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { try { if (input != null) { input.close(); } } catch (IOException ex) { ex.printStackTrace(); } } return result; } @Nullable private static List getListFromFile(File file, Type dataType) { try (JsonReader reader = new JsonReader(new FileReader(file))) { Gson gson = new Gson(); return gson.fromJson(reader, dataType); } catch (IOException e) { e.printStackTrace(); } return null; } public interface RestoreSettingsListener { void success(); void failed(String errorMessage); void failedWithWrongPassword(String errorMessage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/SaveBitmapImageToFile.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.graphics.Bitmap; import android.os.Handler; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.concurrent.Executor; public class SaveBitmapImageToFile { public static void SaveBitmapImageToFile(Executor executor, Handler handler, Bitmap resource, String cacheDirPath, String fileName, SaveBitmapImageToFileListener saveBitmapImageToFileListener) { executor.execute(() -> { try { File imageFile = new File(cacheDirPath, fileName); OutputStream outputStream = new FileOutputStream(imageFile); resource.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); outputStream.flush(); outputStream.close(); handler.post(() -> saveBitmapImageToFileListener.saveSuccess(imageFile)); } catch (IOException e) { handler.post(saveBitmapImageToFileListener::saveFailed); } }); } public interface SaveBitmapImageToFileListener { void saveSuccess(File imageFile); void saveFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/SaveGIFToFile.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.os.Handler; import com.bumptech.glide.load.resource.gif.GifDrawable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.concurrent.Executor; public class SaveGIFToFile { public static void saveGifToFile(Executor executor, Handler handler, GifDrawable resource, String cacheDirPath, String fileName, SaveGIFToFileListener saveImageToFileListener) { executor.execute(() -> { try { File imageFile = new File(cacheDirPath, fileName); ByteBuffer byteBuffer = resource.getBuffer(); OutputStream outputStream = new FileOutputStream(imageFile); byte[] bytes = new byte[byteBuffer.capacity()]; ((ByteBuffer) byteBuffer.duplicate().clear()).get(bytes); outputStream.write(bytes, 0, bytes.length); outputStream.close(); handler.post(() -> saveImageToFileListener.saveSuccess(imageFile)); } catch (IOException e) { handler.post(saveImageToFileListener::saveFailed); } }); } public interface SaveGIFToFileListener { void saveSuccess(File imageFile); void saveFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/SetAsWallpaper.java ================================================ package ml.docilealligator.infinityforreddit.asynctasks; import android.app.WallpaperManager; import android.graphics.Bitmap; import android.graphics.Rect; import android.media.ThumbnailUtils; import android.os.Build; import android.os.Handler; import android.util.DisplayMetrics; import android.view.WindowManager; import java.io.IOException; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.WallpaperSetter; public class SetAsWallpaper { public static void setAsWallpaper(Executor executor, Handler handler, Bitmap bitmap, int setTo, WallpaperManager manager, WindowManager windowManager, WallpaperSetter.SetWallpaperListener setWallpaperListener) { executor.execute(new Runnable() { @Override public void run() { DisplayMetrics metrics = new DisplayMetrics(); Rect rect = null; Bitmap bitmapFinal = bitmap; if (windowManager != null) { windowManager.getDefaultDisplay().getMetrics(metrics); int height = metrics.heightPixels; int width = metrics.widthPixels; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { bitmapFinal = ThumbnailUtils.extractThumbnail(bitmapFinal, width, height); } float imageAR = (float) bitmapFinal.getWidth() / (float) bitmapFinal.getHeight(); float screenAR = (float) width / (float) height; if (imageAR > screenAR) { int desiredWidth = (int) (bitmapFinal.getHeight() * screenAR); rect = new Rect((bitmapFinal.getWidth() - desiredWidth) / 2, 0, bitmapFinal.getWidth(), bitmapFinal.getHeight()); } else { int desiredHeight = (int) (bitmapFinal.getWidth() / screenAR); rect = new Rect(0, (bitmapFinal.getHeight() - desiredHeight) / 2, bitmapFinal.getWidth(), (bitmapFinal.getHeight() + desiredHeight) / 2); } } try { switch (setTo) { case WallpaperSetter.HOME_SCREEN: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { manager.setBitmap(bitmapFinal, rect, true, WallpaperManager.FLAG_SYSTEM); } break; case WallpaperSetter.LOCK_SCREEN: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { manager.setBitmap(bitmapFinal, rect, true, WallpaperManager.FLAG_LOCK); } break; case WallpaperSetter.BOTH_SCREENS: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { manager.setBitmap(bitmapFinal, rect, true, WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK); } else { manager.setBitmap(bitmapFinal); } break; } handler.post(setWallpaperListener::success); } catch (IOException e) { handler.post(setWallpaperListener::failed); } } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/AccountChooserBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.account.AccountViewModel; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.AccountChooserRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentAccountChooserBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class AccountChooserBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject @Named("security") SharedPreferences sharedPreferences; @Inject Executor executor; BaseActivity activity; AccountChooserRecyclerViewAdapter adapter; AccountViewModel accountViewModel; public AccountChooserBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentAccountChooserBottomSheetBinding binding = FragmentAccountChooserBottomSheetBinding.inflate(inflater, container, false); ((Infinity) activity.getApplication()).getAppComponent().inject(this); Utils.hideKeyboard(activity); adapter = new AccountChooserRecyclerViewAdapter(activity, customThemeWrapper, Glide.with(this), account -> { if (activity instanceof AccountChooserListener) { ((AccountChooserListener) activity).onAccountSelected(account); } dismiss(); }); binding.recyclerViewAccountChooserBottomSheetFragment.setAdapter(adapter); if (sharedPreferences.getBoolean(SharedPreferencesUtils.REQUIRE_AUTHENTICATION_TO_GO_TO_ACCOUNT_SECTION_IN_NAVIGATION_DRAWER, false)) { BiometricManager biometricManager = BiometricManager.from(activity); if (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) { Executor executor = ContextCompat.getMainExecutor(activity); BiometricPrompt biometricPrompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); accountViewModel = new ViewModelProvider(AccountChooserBottomSheetFragment.this, new AccountViewModel.Factory(executor, redditDataRoomDatabase)).get(AccountViewModel.class); accountViewModel.getAllAccountsLiveData().observe(getViewLifecycleOwner(), accounts -> { adapter.changeAccountsDataset(accounts); }); } @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { super.onAuthenticationError(errorCode, errString); dismiss(); } }); BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.unlock)) .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) .build(); biometricPrompt.authenticate(promptInfo); } else { dismiss(); } } else { accountViewModel = new ViewModelProvider(this, new AccountViewModel.Factory(executor, redditDataRoomDatabase)).get(AccountViewModel.class); accountViewModel.getAllAccountsLiveData().observe(getViewLifecycleOwner(), accounts -> { adapter.changeAccountsDataset(accounts); }); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (BaseActivity) context; } public interface AccountChooserListener { void onAccountSelected(Account account); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/CommentFilterOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.CommentFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentCommentFilterOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class CommentFilterOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_COMMENT_FILTER = "ECF"; private CommentFilterPreferenceActivity activity; public CommentFilterOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentCommentFilterOptionsBottomSheetBinding binding = FragmentCommentFilterOptionsBottomSheetBinding.inflate(inflater, container, false); CommentFilter commentFilter = getArguments().getParcelable(EXTRA_COMMENT_FILTER); binding.editTextViewCommentFilterOptionsBottomSheetFragment.setOnClickListener(view -> { activity.editCommentFilter(commentFilter); dismiss(); }); binding.applyToTextViewCommentFilterOptionsBottomSheetFragment.setOnClickListener(view -> { activity.applyCommentFilterTo(commentFilter); dismiss(); }); binding.deleteTextViewCommentFilterOptionsBottomSheetFragment.setOnClickListener(view -> { activity.deleteCommentFilter(commentFilter); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (CommentFilterPreferenceActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/CommentFilterUsageOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.CommentFilterUsageListingActivity; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentCommentFilterUsageOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class CommentFilterUsageOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_COMMENT_FILTER_USAGE = "ECFU"; private CommentFilterUsageListingActivity activity; public CommentFilterUsageOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentCommentFilterUsageOptionsBottomSheetBinding binding = FragmentCommentFilterUsageOptionsBottomSheetBinding.inflate(inflater, container, false); CommentFilterUsage commentFilterUsage = getArguments().getParcelable(EXTRA_COMMENT_FILTER_USAGE); binding.editTextViewCommentFilterUsageOptionsBottomSheetFragment.setOnClickListener(view -> { activity.editCommentFilterUsage(commentFilterUsage); dismiss(); }); binding.deleteTextViewCommentFilterUsageOptionsBottomSheetFragment.setOnClickListener(view -> { activity.deleteCommentFilterUsage(commentFilterUsage); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (CommentFilterUsageListingActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/CommentModerationActionBottomSheetFragment.kt ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.content.res.AppCompatResources import ml.docilealligator.infinityforreddit.CommentModerationActionHandler import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.comment.Comment import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment import ml.docilealligator.infinityforreddit.databinding.FragmentCommentModerationActionBottomSheetBinding private const val EXTRA_COMMENT = "EP" private const val EXTRA_POSITION = "EPO" /** * A simple [Fragment] subclass. * Use the [CommentModerationActionBottomSheetFragment.newInstance] factory method to * create an instance of this fragment. */ class CommentModerationActionBottomSheetFragment : LandscapeExpandedRoundedBottomSheetDialogFragment() { private var comment: Comment? = null private var position: Int = -1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { comment = it.getParcelable(EXTRA_COMMENT) position = it.getInt(EXTRA_POSITION) } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // Inflate the layout for this fragment val binding = FragmentCommentModerationActionBottomSheetBinding.inflate( inflater, container, false ) comment?.let { comment -> if (comment.isApproved) { binding.approveTextViewCommentModerationActionBottomSheetFragment.visibility = View.GONE } else { binding.approveTextViewCommentModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as CommentModerationActionHandler).approveComment(comment, position) dismiss() } } if (comment.isRemoved) { binding.removeTextViewCommentModerationActionBottomSheetFragment.visibility = View.GONE binding.spamTextViewCommentModerationActionBottomSheetFragment.visibility = View.GONE } else { binding.removeTextViewCommentModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as CommentModerationActionHandler).removeComment(comment, position, false) dismiss() } binding.spamTextViewCommentModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as CommentModerationActionHandler).removeComment(comment, position, true) dismiss() } } activity?.let { binding.toggleLockTextViewCommentModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( AppCompatResources.getDrawable(it, if (comment.isLocked) R.drawable.ic_unlock_24dp else R.drawable.ic_lock_day_night_24dp), null, null, null ) } binding.toggleLockTextViewCommentModerationActionBottomSheetFragment.setText(if (comment.isLocked) R.string.unlock else R.string.lock) binding.toggleLockTextViewCommentModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as CommentModerationActionHandler).toggleLock(comment, position) dismiss() } } return binding.root } companion object { @JvmStatic fun newInstance(comment: Comment, position: Int) = CommentModerationActionBottomSheetFragment().apply { arguments = Bundle().apply { putParcelable(EXTRA_COMMENT, comment) putInt(EXTRA_POSITION, position) } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/CommentMoreBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; 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.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CommentActivity; import ml.docilealligator.infinityforreddit.activities.CommentFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.activities.EditCommentActivity; import ml.docilealligator.infinityforreddit.activities.ReportActivity; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentCommentMoreBottomSheetBinding; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.utils.ShareScreenshotUtilsKt; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class CommentMoreBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_COMMENT = "ECF"; public static final String EXTRA_EDIT_AND_DELETE_AVAILABLE = "EEADA"; public static final String EXTRA_POSITION = "EP"; public static final String EXTRA_SHOW_REPLY_AND_SAVE_OPTION = "ESSARO"; public static final String EXTRA_IS_NSFW = "EIN"; public static final String EXTRA_POST = "EPO"; public static final String EXTRA_THREAD_COMMENTS = "ETC"; private FragmentCommentMoreBottomSheetBinding binding; private BaseActivity activity; public CommentMoreBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentCommentMoreBottomSheetBinding.inflate(inflater, container, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } Bundle bundle = getArguments(); if (bundle == null) { dismiss(); return binding.getRoot(); } Comment comment = bundle.getParcelable(EXTRA_COMMENT); if (comment == null) { dismiss(); return binding.getRoot(); } boolean editAndDeleteAvailable = bundle.getBoolean(EXTRA_EDIT_AND_DELETE_AVAILABLE, false); boolean showReplyAndSaveOption = bundle.getBoolean(EXTRA_SHOW_REPLY_AND_SAVE_OPTION, false); if (!activity.accountName.equals(Account.ANONYMOUS_ACCOUNT) && !"".equals(activity.accessToken)) { if (editAndDeleteAvailable) { binding.editTextViewCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); binding.deleteTextViewCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); binding.editTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, EditCommentActivity.class); intent.putExtra(EditCommentActivity.EXTRA_FULLNAME, comment.getFullName()); intent.putExtra(EditCommentActivity.EXTRA_CONTENT, comment.getCommentMarkdown()); if (comment.getMediaMetadataMap() != null) { ArrayList mediaMetadataList = new ArrayList<>(comment.getMediaMetadataMap().values()); intent.putParcelableArrayListExtra(EditCommentActivity.EXTRA_MEDIA_METADATA_LIST, mediaMetadataList); } intent.putExtra(EditCommentActivity.EXTRA_POSITION, bundle.getInt(EXTRA_POSITION)); if (activity instanceof ViewPostDetailActivity) { activity.startActivityForResult(intent, ViewPostDetailActivity.EDIT_COMMENT_REQUEST_CODE); } else { activity.startActivityForResult(intent, ViewUserDetailActivity.EDIT_COMMENT_REQUEST_CODE); } dismiss(); }); binding.deleteTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { dismiss(); if (activity instanceof ViewPostDetailActivity) { ((ViewPostDetailActivity) activity).deleteComment(comment.getFullName(), bundle.getInt(EXTRA_POSITION)); } else if (activity instanceof ViewUserDetailActivity) { ((ViewUserDetailActivity) activity).deleteComment(comment.getFullName()); } }); } if (comment.getAuthor().equals(activity.accountName)) { binding.notificationViewCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); binding.notificationViewCommentMoreBottomSheetFragment.setText(comment.isSendReplies() ? R.string.disable_reply_notifications : R.string.enable_reply_notifications); binding.notificationViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { dismiss(); if (activity instanceof ViewPostDetailActivity) { ((ViewPostDetailActivity) activity).toggleReplyNotifications(comment, bundle.getInt(EXTRA_POSITION)); } else if (activity instanceof ViewUserDetailActivity) { ((ViewUserDetailActivity) activity).toggleReplyNotifications(comment, bundle.getInt(EXTRA_POSITION)); } }); } } if (showReplyAndSaveOption) { if (!comment.isLocked()) { binding.replyTextViewCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); binding.replyTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, CommentActivity.class); intent.putExtra(CommentActivity.EXTRA_PARENT_DEPTH_KEY, comment.getDepth() + 1); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_MARKDOWN_KEY, comment.getCommentMarkdown()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_KEY, comment.getCommentRawText()); intent.putExtra(CommentActivity.EXTRA_PARENT_FULLNAME_KEY, comment.getFullName()); intent.putExtra(CommentActivity.EXTRA_IS_REPLYING_KEY, true); intent.putExtra(CommentActivity.EXTRA_PARENT_POSITION_KEY, bundle.getInt(EXTRA_POSITION)); activity.startActivityForResult(intent, CommentActivity.WRITE_COMMENT_REQUEST_CODE); dismiss(); }); } binding.saveTextViewCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); if (comment.isSaved()) { binding.saveTextViewCommentMoreBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(activity, R.drawable.ic_bookmark_day_night_24dp), null, null, null); binding.saveTextViewCommentMoreBottomSheetFragment.setText(R.string.unsave_comment); } else { binding.saveTextViewCommentMoreBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(activity, R.drawable.ic_bookmark_border_day_night_24dp), null, null, null); binding.saveTextViewCommentMoreBottomSheetFragment.setText(R.string.save_comment); } binding.saveTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { if (activity instanceof ViewPostDetailActivity) { ((ViewPostDetailActivity) activity).saveComment(comment, bundle.getInt(EXTRA_POSITION)); } dismiss(); }); } binding.shareTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { dismiss(); try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, comment.getPermalink()); activity.startActivity(Intent.createChooser(intent, getString(R.string.share))); } catch (ActivityNotFoundException e) { Toast.makeText(activity, R.string.no_activity_found_for_share, Toast.LENGTH_SHORT).show(); } }); binding.shareTextViewCommentMoreBottomSheetFragment.setOnLongClickListener(view -> { dismiss(); activity.copyLink(comment.getPermalink()); return true; }); binding.shareAsImageTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { dismiss(); ShareScreenshotUtilsKt.shareCommentAsScreenshot(activity, comment); }); Post post = bundle.getParcelable(EXTRA_POST); ArrayList threadComments = bundle.getParcelableArrayList(EXTRA_THREAD_COMMENTS); if (post != null && threadComments != null && !threadComments.isEmpty()) { binding.shareAsImageWithThreadTextViewCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); binding.shareAsImageWithThreadTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { dismiss(); ShareScreenshotUtilsKt.sharePostWithCommentsAsScreenshot( activity, post, threadComments, activity.customThemeWrapper, activity.getResources().getConfiguration().locale, activity.getDefaultSharedPreferences().getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE), new SaveMemoryCenterInisdeDownsampleStrategy( Integer.parseInt(activity.getDefaultSharedPreferences() .getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"))) ); }); } binding.copyTextViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { dismiss(); CopyTextBottomSheetFragment.show(activity.getSupportFragmentManager(), comment.getCommentRawText(), comment.getCommentMarkdown()); }); binding.reportViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, ReportActivity.class); intent.putExtra(ReportActivity.EXTRA_SUBREDDIT_NAME, comment.getSubredditName()); intent.putExtra(ReportActivity.EXTRA_THING_FULLNAME, comment.getFullName()); activity.startActivity(intent); dismiss(); }); binding.addToCommentFilterViewCommentMoreBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, CommentFilterPreferenceActivity.class); intent.putExtra(CommentFilterPreferenceActivity.EXTRA_COMMENT, comment); activity.startActivity(intent); dismiss(); }); if (comment.getDepth() > 0 && activity instanceof ViewPostDetailActivity) { binding.jumpToParentCommentCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); binding.jumpToParentCommentCommentMoreBottomSheetFragment.setOnClickListener(view -> { if (activity instanceof ViewPostDetailActivity) { ((ViewPostDetailActivity) activity).scrollToParentComment(bundle.getInt(EXTRA_POSITION), comment.getDepth()); } dismiss(); }); } if (comment.isCanModComment()) { binding.modCommentMoreBottomSheetFragment.setVisibility(View.VISIBLE); binding.modCommentMoreBottomSheetFragment.setOnClickListener(view -> { CommentModerationActionBottomSheetFragment commentModerationActionBottomSheetFragment = CommentModerationActionBottomSheetFragment.newInstance(comment, bundle.getInt(EXTRA_POSITION)); Fragment parentFragment = getParentFragment(); if (parentFragment != null) { commentModerationActionBottomSheetFragment.show(parentFragment.getChildFragmentManager(), commentModerationActionBottomSheetFragment.getTag()); } else { commentModerationActionBottomSheetFragment.show(activity.getSupportFragmentManager(), commentModerationActionBottomSheetFragment.getTag()); } dismiss(); }); } if (comment.isApproved()) { binding.statusCommentMoreBottomSheetFragment.setText(getString(R.string.approved_status, comment.getApprovedBy())); } else if (comment.isRemoved()) { if (comment.isSpam()) { binding.statusCommentMoreBottomSheetFragment.setText(R.string.comment_spam_status); } else { binding.statusCommentMoreBottomSheetFragment.setText(R.string.comment_removed_status); } } else { binding.statusCommentMoreBottomSheetFragment.setVisibility(View.GONE); } if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/CopyTextBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentCopyTextBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class CopyTextBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_RAW_TEXT = "ERT"; public static final String EXTRA_MARKDOWN = "EM"; private BaseActivity baseActivity; private ViewRedditGalleryActivity viewRedditGalleryActivity; private String markdownText; public CopyTextBottomSheetFragment() { // Required empty public constructor } /** * Convenience method for creating the dialog, creating and setting arguments bundle * and displaying the dialog */ public static void show(@NonNull FragmentManager fragmentManager, @Nullable String rawText, @Nullable String markdown) { Bundle bundle = new Bundle(); bundle.putString(CopyTextBottomSheetFragment.EXTRA_RAW_TEXT, rawText); bundle.putString(CopyTextBottomSheetFragment.EXTRA_MARKDOWN, markdown); CopyTextBottomSheetFragment copyTextBottomSheetFragment = new CopyTextBottomSheetFragment(); copyTextBottomSheetFragment.setArguments(bundle); copyTextBottomSheetFragment.show(fragmentManager, null); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentCopyTextBottomSheetBinding binding = FragmentCopyTextBottomSheetBinding.inflate(inflater, container, false); String rawText = getArguments().getString(EXTRA_RAW_TEXT, null); markdownText = getArguments().getString(EXTRA_MARKDOWN, null); if (rawText != null) { binding.copyRawTextTextViewCopyTextBottomSheetFragment.setOnClickListener(view -> { showCopyDialog(rawText); dismiss(); }); binding.copyAllRawTextTextViewCopyTextBottomSheetFragment.setOnClickListener(view -> { copyText(rawText); dismiss(); }); } else { binding.copyRawTextTextViewCopyTextBottomSheetFragment.setVisibility(View.GONE); binding.copyAllRawTextTextViewCopyTextBottomSheetFragment.setVisibility(View.GONE); } if (markdownText != null) { binding.copyMarkdownTextViewCopyTextBottomSheetFragment.setOnClickListener(view -> { showCopyDialog(markdownText); dismiss(); }); binding.copyAllMarkdownTextViewCopyTextBottomSheetFragment.setOnClickListener(view -> { copyText(markdownText); dismiss(); }); } else { binding.copyMarkdownTextViewCopyTextBottomSheetFragment.setVisibility(View.GONE); binding.copyAllMarkdownTextViewCopyTextBottomSheetFragment.setVisibility(View.GONE); } if (baseActivity != null && baseActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), baseActivity.typeface); } else if (viewRedditGalleryActivity != null && viewRedditGalleryActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), viewRedditGalleryActivity.typeface); } return binding.getRoot(); } private void showCopyDialog(String text) { AppCompatActivity activity = baseActivity == null ? viewRedditGalleryActivity : baseActivity; LayoutInflater inflater = activity.getLayoutInflater(); View layout = inflater.inflate(R.layout.copy_text_material_dialog, null); TextView textView = layout.findViewById(R.id.text_view_copy_text_material_dialog); textView.setText(text); new MaterialAlertDialogBuilder(activity, R.style.CopyTextMaterialAlertDialogTheme) .setTitle(R.string.copy_text) .setView(layout) .setPositiveButton(R.string.copy_all, (dialogInterface, i) -> copyText(text)) .setNegativeButton(R.string.cancel, null) .show(); } private void copyText(String text) { AppCompatActivity activity = baseActivity == null ? viewRedditGalleryActivity : baseActivity; ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", text); clipboard.setPrimaryClip(clip); if (android.os.Build.VERSION.SDK_INT < 33) { Toast.makeText(activity, R.string.copy_success, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(activity, R.string.copy_link_failed, Toast.LENGTH_SHORT).show(); } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof BaseActivity) { baseActivity = (BaseActivity) context; } else { viewRedditGalleryActivity = (ViewRedditGalleryActivity) context; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/CreateThemeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentCreateThemeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class CreateThemeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private BaseActivity activity; public interface SelectBaseThemeBottomSheetFragmentListener { void importTheme(); } public CreateThemeBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentCreateThemeBottomSheetBinding binding = FragmentCreateThemeBottomSheetBinding.inflate(inflater, container, false); binding.importThemeTextViewCreateThemeBottomSheetFragment.setOnClickListener(view -> { ((SelectBaseThemeBottomSheetFragmentListener) activity).importTheme(); dismiss(); }); binding.lightThemeTextViewCreateThemeBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_CREATE_THEME, true); intent.putExtra(CustomizeThemeActivity.EXTRA_IS_PREDEFIINED_THEME, true); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_NAME, getString(R.string.theme_name_indigo)); startActivity(intent); dismiss(); }); binding.darkThemeTextViewCreateThemeBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_CREATE_THEME, true); intent.putExtra(CustomizeThemeActivity.EXTRA_IS_PREDEFIINED_THEME, true); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_NAME, getString(R.string.theme_name_indigo_dark)); startActivity(intent); dismiss(); }); binding.amoledThemeTextViewCreateThemeBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_CREATE_THEME, true); intent.putExtra(CustomizeThemeActivity.EXTRA_IS_PREDEFIINED_THEME, true); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_NAME, getString(R.string.theme_name_indigo_amoled)); startActivity(intent); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/CustomThemeOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customtheme.OnlineCustomThemeMetadata; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentCustomThemeOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class CustomThemeOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_THEME_NAME = "ETN"; public static final String EXTRA_ONLINE_CUSTOM_THEME_METADATA = "ECT"; public static final String EXTRA_INDEX_IN_THEME_LIST = "EIITL"; private String themeName; private OnlineCustomThemeMetadata onlineCustomThemeMetadata; private BaseActivity activity; public CustomThemeOptionsBottomSheetFragment() { // Required empty public constructor } public interface CustomThemeOptionsBottomSheetFragmentListener { void editTheme(String themeName, @Nullable OnlineCustomThemeMetadata onlineCustomThemeMetadata, int indexInThemeList); void changeName(String oldThemeName); void shareTheme(String themeName); void shareTheme(OnlineCustomThemeMetadata onlineCustomThemeMetadata); void delete(String themeName); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentCustomThemeOptionsBottomSheetBinding binding = FragmentCustomThemeOptionsBottomSheetBinding.inflate(inflater, container, false); themeName = getArguments().getString(EXTRA_THEME_NAME); onlineCustomThemeMetadata = getArguments().getParcelable(EXTRA_ONLINE_CUSTOM_THEME_METADATA); if (onlineCustomThemeMetadata != null && !onlineCustomThemeMetadata.username.equals(activity.accountName)) { binding.editThemeTextViewCustomThemeOptionsBottomSheetFragment.setVisibility(View.GONE); binding.changeThemeNameTextViewCustomThemeOptionsBottomSheetFragment.setVisibility(View.GONE); } else { binding.editThemeTextViewCustomThemeOptionsBottomSheetFragment.setOnClickListener(view -> { ((CustomThemeOptionsBottomSheetFragmentListener) activity).editTheme(themeName, onlineCustomThemeMetadata, getArguments().getInt(EXTRA_INDEX_IN_THEME_LIST, -1)); dismiss(); }); binding.changeThemeNameTextViewCustomThemeOptionsBottomSheetFragment.setOnClickListener(view -> { ((CustomThemeOptionsBottomSheetFragmentListener) activity).changeName(themeName); dismiss(); }); } binding.themeNameTextViewCustomThemeOptionsBottomSheetFragment.setText(themeName); binding.shareThemeTextViewCustomThemeOptionsBottomSheetFragment.setOnClickListener(view -> { if (onlineCustomThemeMetadata != null) { ((CustomThemeOptionsBottomSheetFragmentListener) activity).shareTheme(onlineCustomThemeMetadata); } else { ((CustomThemeOptionsBottomSheetFragmentListener) activity).shareTheme(themeName); } dismiss(); }); binding.deleteThemeTextViewCustomThemeOptionsBottomSheetFragment.setOnClickListener(view -> { ((CustomThemeOptionsBottomSheetFragmentListener) activity).delete(themeName); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/FABMoreOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentFabMoreOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class FABMoreOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_ANONYMOUS_MODE = "EAM"; public static final int FAB_OPTION_SUBMIT_POST = 0; public static final int FAB_OPTION_REFRESH = 1; public static final int FAB_OPTION_CHANGE_SORT_TYPE = 2; public static final int FAB_OPTION_CHANGE_POST_LAYOUT = 3; public static final int FAB_OPTION_SEARCH = 4; public static final int FAB_OPTION_GO_TO_SUBREDDIT = 5; public static final int FAB_OPTION_GO_TO_USER = 6; public static final int FAB_HIDE_READ_POSTS = 7; public static final int FAB_FILTER_POSTS = 8; public static final int FAB_GO_TO_TOP = 9; private FABOptionSelectionCallback activity; public FABMoreOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentFabMoreOptionsBottomSheetBinding binding = FragmentFabMoreOptionsBottomSheetBinding.inflate(inflater, container, false); if (getArguments() != null && getArguments().getBoolean(EXTRA_ANONYMOUS_MODE, false)) { binding.submitPostTextViewFabMoreOptionsBottomSheetFragment.setVisibility(View.GONE); binding.hideReadPostsTextViewFabMoreOptionsBottomSheetFragment.setVisibility(View.GONE); } else { binding.submitPostTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_SUBMIT_POST); dismiss(); }); binding.hideReadPostsTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_HIDE_READ_POSTS); dismiss(); }); } binding.refreshTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_REFRESH); dismiss(); }); binding.changeSortTypeTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_CHANGE_SORT_TYPE); dismiss(); }); binding.changePostLayoutTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_CHANGE_POST_LAYOUT); dismiss(); }); binding.searchTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_SEARCH); dismiss(); }); binding.goToSubredditTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_GO_TO_SUBREDDIT); dismiss(); }); binding.goToUserTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_GO_TO_USER); dismiss(); }); binding.filterPostsTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_FILTER_POSTS); dismiss(); }); binding.goToTopTextViewFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_GO_TO_TOP); dismiss(); }); Activity baseActivity = getActivity(); if (baseActivity instanceof BaseActivity) { if (((BaseActivity) baseActivity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((BaseActivity) baseActivity).typeface); } } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (FABOptionSelectionCallback) context; } public interface FABOptionSelectionCallback { void fabOptionSelected(int option); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/FilteredThingFABMoreOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentFilteredThingFabMoreOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class FilteredThingFABMoreOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final int FAB_OPTION_FILTER = 0; public static final int FAB_OPTION_HIDE_READ_POSTS = 1; private FABOptionSelectionCallback activity; public FilteredThingFABMoreOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentFilteredThingFabMoreOptionsBottomSheetBinding binding = FragmentFilteredThingFabMoreOptionsBottomSheetBinding.inflate(inflater, container, false); binding.filterTextViewFilteredThingFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_FILTER); dismiss(); }); binding.hideReadPostsTextViewFilteredThingFabMoreOptionsBottomSheetFragment.setOnClickListener(view -> { activity.fabOptionSelected(FAB_OPTION_HIDE_READ_POSTS); dismiss(); }); Activity baseActivity = getActivity(); if (baseActivity instanceof BaseActivity) { if (((BaseActivity) baseActivity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((BaseActivity) baseActivity).typeface); } } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (FABOptionSelectionCallback) context; } public interface FABOptionSelectionCallback { void fabOptionSelected(int option); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/FlairBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import com.google.android.material.bottomsheet.BottomSheetBehavior; import org.greenrobot.eventbus.EventBus; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.FlairBottomSheetRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentFlairBottomSheetBinding; import ml.docilealligator.infinityforreddit.events.FlairSelectedEvent; import ml.docilealligator.infinityforreddit.subreddit.FetchFlairs; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class FlairBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_VIEW_POST_DETAIL_FRAGMENT_ID = "EPFI"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String mSubredditName; private BaseActivity mActivity; private Handler mHandler; private FlairBottomSheetRecyclerViewAdapter mAdapter; private FragmentFlairBottomSheetBinding binding; public FlairBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentFlairBottomSheetBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); if (mActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mActivity.typeface); } long viewPostFragmentId = getArguments().getLong(EXTRA_VIEW_POST_DETAIL_FRAGMENT_ID, -1); mAdapter = new FlairBottomSheetRecyclerViewAdapter(mActivity, mCustomThemeWrapper, flair -> { if (viewPostFragmentId <= 0) { //PostXXXActivity ((FlairSelectionCallback) mActivity).flairSelected(flair); } else { EventBus.getDefault().post(new FlairSelectedEvent(viewPostFragmentId, flair)); } dismiss(); }); binding.recyclerViewBottomSheetFragment.setAdapter(mAdapter); mSubredditName = getArguments().getString(EXTRA_SUBREDDIT_NAME); mHandler = new Handler(Looper.getMainLooper()); fetchFlairs(); return binding.getRoot(); } private void fetchFlairs() { FetchFlairs.fetchFlairsInSubreddit(mExecutor, mHandler, mOauthRetrofit, mActivity.accessToken, mSubredditName, new FetchFlairs.FetchFlairsInSubredditListener() { @Override public void fetchSuccessful(List flairs) { binding.progressBarFlairBottomSheetFragment.setVisibility(View.GONE); if (flairs == null || flairs.isEmpty()) { binding.errorTextViewFlairBottomSheetFragment.setVisibility(View.VISIBLE); binding.errorTextViewFlairBottomSheetFragment.setText(R.string.no_flair); } else { binding.errorTextViewFlairBottomSheetFragment.setVisibility(View.GONE); mAdapter.changeDataset(flairs); } } @Override public void fetchFailed() { binding.progressBarFlairBottomSheetFragment.setVisibility(View.GONE); binding.errorTextViewFlairBottomSheetFragment.setVisibility(View.VISIBLE); binding.errorTextViewFlairBottomSheetFragment.setText(R.string.error_loading_flairs); binding.errorTextViewFlairBottomSheetFragment.setOnClickListener(view -> fetchFlairs()); } }); } @Override public void onStart() { super.onStart(); View parentView = (View) requireView().getParent(); BottomSheetBehavior.from(parentView).setState(BottomSheetBehavior.STATE_EXPANDED); BottomSheetBehavior.from(parentView).setSkipCollapsed(true); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } public interface FlairSelectionCallback { void flairSelected(Flair flair); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/GiphyGifInfoBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import com.giphy.sdk.ui.views.GiphyDialogFragment; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentGiphyGifInfoBottomSheetBinding; public class GiphyGifInfoBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private BaseActivity activity; public GiphyGifInfoBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentGiphyGifInfoBottomSheetBinding binding = FragmentGiphyGifInfoBottomSheetBinding.inflate(inflater, container, false); binding.selectGiphyGifButtonUploadedImagesBottomSheetFragment.setOnClickListener(view -> { GiphyDialogFragment.Companion.newInstance().show(activity.getSupportFragmentManager(), "giphy_dialog"); dismiss(); }); return binding.getRoot(); } public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/ImportantInfoBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.MainActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentImportantInfoBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class ImportantInfoBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private MainActivity mainActivity; public ImportantInfoBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentImportantInfoBottomSheetBinding binding = FragmentImportantInfoBottomSheetBinding.inflate(inflater, container, false); if (mainActivity != null && mainActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mainActivity.typeface); } binding.getRoot().setNestedScrollingEnabled(true); /*SpannableString message = new SpannableString(getString(R.string.reddit_api_info, "https://www.reddit.com/r/reddit/comments/145bram/addressing_the_community_about_changes_to_our_api", "https://www.reddit.com/r/Infinity_For_Reddit/comments/147bhsg/the_future_of_infinity")); Linkify.addLinks(message, Linkify.WEB_URLS); binding.messageTextViewRedditApiInfoBottomSheetFragment.setText(message); binding.messageTextViewRedditApiInfoBottomSheetFragment.setMovementMethod(BetterLinkMovementMethod.newInstance().setOnLinkClickListener((textView, url) -> { Intent intent = new Intent(mainActivity, LinkResolverActivity.class); intent.setData(Uri.parse(url)); startActivity(intent); return true; }));*/ binding.messageTextViewRedditApiInfoBottomSheetFragment.setLinkTextColor(getResources().getColor(R.color.colorAccent)); binding.doNotShowThisAgainTextView.setOnClickListener(view -> { binding.doNotShowThisAgainCheckBox.toggle(); }); binding.continueButtonRedditApiInfoBottomSheetFragment.setOnClickListener(view -> { if (binding.doNotShowThisAgainCheckBox.isChecked()) { mainActivity.doNotShowRedditAPIInfoAgain(); } dismiss(); }); return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mainActivity = (MainActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/KarmaInfoBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentKarmaInfoBottomSheetBinding; public class KarmaInfoBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_POST_KARMA = "EPK"; public static final String EXTRA_COMMENT_KARMA = "ECK"; public static final String EXTRA_AWARDER_KARMA = "EARK"; public static final String EXTRA_AWARDEE_KARMA = "EAEK"; public static KarmaInfoBottomSheetFragment newInstance(int postKarma, int commentKarma, int awarderKarma, int awardeeKarma) { KarmaInfoBottomSheetFragment fragment = new KarmaInfoBottomSheetFragment(); Bundle args = new Bundle(); args.putInt(EXTRA_POST_KARMA, postKarma); args.putInt(EXTRA_COMMENT_KARMA, commentKarma); args.putInt(EXTRA_AWARDER_KARMA, awarderKarma); args.putInt(EXTRA_AWARDEE_KARMA, awardeeKarma); fragment.setArguments(args); return fragment; } public KarmaInfoBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentKarmaInfoBottomSheetBinding binding = FragmentKarmaInfoBottomSheetBinding.inflate(inflater, container, false); int postKarma = getArguments().getInt(EXTRA_POST_KARMA, 0); int commentKarma = getArguments().getInt(EXTRA_COMMENT_KARMA, 0); int awarderKarma = getArguments().getInt(EXTRA_AWARDER_KARMA, 0); int awardeeKarma = getArguments().getInt(EXTRA_AWARDEE_KARMA, 0); binding.postKarmaKarmaInfoBottomSheetFragment.setText(Integer.toString(postKarma)); binding.commentKarmaKarmaInfoBottomSheetFragment.setText(Integer.toString(commentKarma)); binding.awarderKarmaKarmaInfoBottomSheetFragment.setText(Integer.toString(awarderKarma)); binding.awardeeKarmaKarmaInfoBottomSheetFragment.setText(Integer.toString(awardeeKarma)); return binding.getRoot(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/MultiRedditOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.EditMultiRedditActivity; import ml.docilealligator.infinityforreddit.activities.SubscribedThingListingActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentMultiRedditOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class MultiRedditOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_MULTI_REDDIT = "EMR"; private SubscribedThingListingActivity subscribedThingListingActivity; public MultiRedditOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentMultiRedditOptionsBottomSheetBinding binding = FragmentMultiRedditOptionsBottomSheetBinding.inflate(inflater, container, false); MultiReddit multiReddit = getArguments().getParcelable(EXTRA_MULTI_REDDIT); binding.copyMultiRedditPathTextViewMultiRedditOptionsBottomSheetFragment.setOnClickListener(view -> { if (multiReddit != null) { ClipboardManager clipboard = (ClipboardManager) subscribedThingListingActivity.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", multiReddit.getPath()); clipboard.setPrimaryClip(clip); Toast.makeText(subscribedThingListingActivity, multiReddit.getPath(), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(subscribedThingListingActivity, R.string.copy_multi_reddit_path_failed, Toast.LENGTH_SHORT).show(); } } dismiss(); }); binding.editMultiRedditTextViewMultiRedditOptionsBottomSheetFragment.setOnClickListener(view -> { if (multiReddit != null) { Intent editIntent = new Intent(subscribedThingListingActivity, EditMultiRedditActivity.class); editIntent.putExtra(EditMultiRedditActivity.EXTRA_MULTI_PATH, multiReddit.getPath()); startActivity(editIntent); } dismiss(); }); binding.deleteMultiRedditTextViewMultiRedditOptionsBottomSheetFragment.setOnClickListener(view -> { subscribedThingListingActivity.deleteMultiReddit(multiReddit); dismiss(); }); if (subscribedThingListingActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), subscribedThingListingActivity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); subscribedThingListingActivity = (SubscribedThingListingActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/NewCommentFilterUsageBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.CommentFilterUsageListingActivity; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilterUsage; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentNewCommentFilterUsageBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class NewCommentFilterUsageBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private CommentFilterUsageListingActivity activity; public NewCommentFilterUsageBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentNewCommentFilterUsageBottomSheetBinding binding = FragmentNewCommentFilterUsageBottomSheetBinding.inflate(inflater, container, false); binding.subredditTextViewNewCommentFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newCommentFilterUsage(CommentFilterUsage.SUBREDDIT_TYPE); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (CommentFilterUsageListingActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/NewPostFilterUsageBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.PostFilterUsageListingActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentNewPostFilterUsageBottomSheetBinding; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.utils.Utils; public class NewPostFilterUsageBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private PostFilterUsageListingActivity activity; public NewPostFilterUsageBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentNewPostFilterUsageBottomSheetBinding binding = FragmentNewPostFilterUsageBottomSheetBinding.inflate(inflater, container, false); binding.homeTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.HOME_TYPE); dismiss(); }); binding.subredditTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.SUBREDDIT_TYPE); dismiss(); }); binding.userTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.USER_TYPE); dismiss(); }); binding.multiredditTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.MULTIREDDIT_TYPE); dismiss(); }); binding.searchTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.SEARCH_TYPE); dismiss(); }); binding.historyTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.HISTORY_TYPE); dismiss(); }); binding.upvotedTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.UPVOTED_TYPE); dismiss(); }); binding.downvotedTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.DOWNVOTED_TYPE); dismiss(); }); binding.hiddenTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.HIDDEN_TYPE); dismiss(); }); binding.savedTextViewNewPostFilterUsageBottomSheetFragment.setOnClickListener(view -> { activity.newPostFilterUsage(PostFilterUsage.SAVED_TYPE); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (PostFilterUsageListingActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PlaybackSpeedBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.ViewImgurMediaActivity; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentPlaybackSpeedBinding; import ml.docilealligator.infinityforreddit.fragments.ViewImgurVideoFragment; import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryVideoFragment; import ml.docilealligator.infinityforreddit.utils.Utils; public class PlaybackSpeedBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_PLAYBACK_SPEED = "EPS"; private Activity activity; public PlaybackSpeedBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentPlaybackSpeedBinding binding = FragmentPlaybackSpeedBinding.inflate(inflater, container, false); int playbackSpeed = getArguments().getInt(EXTRA_PLAYBACK_SPEED, ViewVideoActivity.PLAYBACK_SPEED_NORMAL); switch (playbackSpeed) { case ViewVideoActivity.PLAYBACK_SPEED_25: binding.playbackSpeed025TextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; case ViewVideoActivity.PLAYBACK_SPEED_50: binding.playbackSpeed050TextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; case ViewVideoActivity.PLAYBACK_SPEED_75: binding.playbackSpeed075TextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; case ViewVideoActivity.PLAYBACK_SPEED_NORMAL: binding.playbackSpeedNormalTextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; case ViewVideoActivity.PLAYBACK_SPEED_125: binding.playbackSpeed125TextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; case ViewVideoActivity.PLAYBACK_SPEED_150: binding.playbackSpeed150TextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; case ViewVideoActivity.PLAYBACK_SPEED_175: binding.playbackSpeed175TextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; case ViewVideoActivity.PLAYBACK_SPEED_200: binding.playbackSpeed200TextViewPlaybackSpeedBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_playback_speed_day_night_24dp, 0); break; } binding.playbackSpeed025TextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_25); dismiss(); }); binding.playbackSpeed050TextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_50); dismiss(); }); binding.playbackSpeed075TextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_75); dismiss(); }); binding.playbackSpeedNormalTextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_NORMAL); dismiss(); }); binding.playbackSpeed125TextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_125); dismiss(); }); binding.playbackSpeed150TextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_150); dismiss(); }); binding.playbackSpeed175TextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_175); dismiss(); }); binding.playbackSpeed200TextViewPlaybackSpeedBottomSheetFragment.setOnClickListener(view -> { setPlaybackSpeed(ViewVideoActivity.PLAYBACK_SPEED_200); dismiss(); }); if (activity instanceof ViewVideoActivity) { if (((ViewVideoActivity) activity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((ViewVideoActivity) activity).typeface); } } else if (activity instanceof ViewImgurMediaActivity) { if (((ViewImgurMediaActivity) activity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((ViewImgurMediaActivity) activity).typeface); } } else if (activity instanceof ViewRedditGalleryActivity) { if (((ViewRedditGalleryActivity) activity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((ViewRedditGalleryActivity) activity).typeface); } } return binding.getRoot(); } private void setPlaybackSpeed(int playbackSpeed) { if (activity instanceof ViewVideoActivity) { ((ViewVideoActivity) activity).setPlaybackSpeed(playbackSpeed); } else { Fragment parentFragment = getParentFragment(); if (parentFragment instanceof ViewImgurVideoFragment) { ((ViewImgurVideoFragment) parentFragment).setPlaybackSpeed(playbackSpeed); } else if (parentFragment instanceof ViewRedditGalleryVideoFragment) { ((ViewRedditGalleryVideoFragment) parentFragment).setPlaybackSpeed(playbackSpeed); } } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (Activity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PostCommentSortTypeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentPostCommentSortTypeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class PostCommentSortTypeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_CURRENT_SORT_TYPE = "ECST"; private BaseActivity activity; public PostCommentSortTypeBottomSheetFragment() { // Required empty public constructor } public static PostCommentSortTypeBottomSheetFragment getNewInstance(SortType.Type currentSortType) { PostCommentSortTypeBottomSheetFragment fragment = new PostCommentSortTypeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putSerializable(EXTRA_CURRENT_SORT_TYPE, currentSortType); fragment.setArguments(bundle); return fragment; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentPostCommentSortTypeBottomSheetBinding binding = FragmentPostCommentSortTypeBottomSheetBinding.inflate(inflater, container, false); SortType.Type currentSortType = (SortType.Type) getArguments().getSerializable(EXTRA_CURRENT_SORT_TYPE); if (currentSortType != null) { if (currentSortType.equals(SortType.Type.BEST) || currentSortType.equals(SortType.Type.CONFIDENCE)) { binding.bestTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.bestTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.TOP)) { binding.topTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.topTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.NEW)) { binding.newTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.newTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.CONTROVERSIAL)) { binding.controversialTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.controversialTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.OLD)) { binding.oldTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.oldTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.RANDOM)) { binding.randomTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.randomTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.QA)) { binding.qaTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.qaTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.LIVE)) { binding.liveTypeTextViewPostCommentSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.liveTypeTextViewPostCommentSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } binding.bestTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.CONFIDENCE)); dismiss(); }); binding.topTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.TOP)); dismiss(); }); binding.newTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.NEW)); dismiss(); }); binding.controversialTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.CONTROVERSIAL)); dismiss(); }); binding.oldTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.OLD)); dismiss(); }); binding.randomTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.RANDOM)); dismiss(); }); binding.qaTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.QA)); dismiss(); }); binding.liveTypeTextViewPostCommentSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.LIVE)); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PostFilterOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.PostFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentPostFilterOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.utils.Utils; public class PostFilterOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_POST_FILTER = "EPF"; private PostFilterPreferenceActivity activity; public PostFilterOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentPostFilterOptionsBottomSheetBinding binding = FragmentPostFilterOptionsBottomSheetBinding.inflate(inflater, container, false); PostFilter postFilter = getArguments().getParcelable(EXTRA_POST_FILTER); binding.editTextViewPostFilterOptionsBottomSheetFragment.setOnClickListener(view -> { activity.editPostFilter(postFilter); dismiss(); }); binding.applyToTextViewPostFilterOptionsBottomSheetFragment.setOnClickListener(view -> { activity.applyPostFilterTo(postFilter); dismiss(); }); binding.deleteTextViewPostFilterOptionsBottomSheetFragment.setOnClickListener(view -> { activity.deletePostFilter(postFilter); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (PostFilterPreferenceActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PostFilterUsageOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.PostFilterUsageListingActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentPostFilterUsageOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.utils.Utils; public class PostFilterUsageOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_POST_FILTER_USAGE = "EPFU"; private PostFilterUsageListingActivity activity; public PostFilterUsageOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentPostFilterUsageOptionsBottomSheetBinding binding = FragmentPostFilterUsageOptionsBottomSheetBinding.inflate(inflater, container, false); PostFilterUsage postFilterUsage = getArguments().getParcelable(EXTRA_POST_FILTER_USAGE); if (postFilterUsage.usage == PostFilterUsage.HOME_TYPE || postFilterUsage.usage == PostFilterUsage.SEARCH_TYPE || postFilterUsage.usage == PostFilterUsage.HISTORY_TYPE || postFilterUsage.usage == PostFilterUsage.UPVOTED_TYPE || postFilterUsage.usage == PostFilterUsage.DOWNVOTED_TYPE || postFilterUsage.usage == PostFilterUsage.HIDDEN_TYPE || postFilterUsage.usage == PostFilterUsage.SAVED_TYPE) { binding.editTextViewPostFilterUsageOptionsBottomSheetFragment.setVisibility(View.GONE); } else { binding.editTextViewPostFilterUsageOptionsBottomSheetFragment.setOnClickListener(view -> { activity.editPostFilterUsage(postFilterUsage); dismiss(); }); } binding.deleteTextViewPostFilterUsageOptionsBottomSheetFragment.setOnClickListener(view -> { activity.deletePostFilterUsage(postFilterUsage); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (PostFilterUsageListingActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PostLayoutBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentPostLayoutBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class PostLayoutBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private FragmentPostLayoutBottomSheetBinding binding; private BaseActivity activity; public PostLayoutBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentPostLayoutBottomSheetBinding.inflate(inflater, container, false); binding.cardLayoutTextViewPostLayoutBottomSheetFragment.setOnClickListener(view -> { ((PostLayoutSelectionCallback) activity).postLayoutSelected(SharedPreferencesUtils.POST_LAYOUT_CARD); dismiss(); }); binding.compactLayoutTextViewPostLayoutBottomSheetFragment.setOnClickListener(view -> { ((PostLayoutSelectionCallback) activity).postLayoutSelected(SharedPreferencesUtils.POST_LAYOUT_COMPACT); dismiss(); }); binding.compactLayout2TextViewPostLayoutBottomSheetFragment.setOnClickListener(view -> { ((PostLayoutSelectionCallback) activity).postLayoutSelected(SharedPreferencesUtils.POST_LAYOUT_COMPACT_2); dismiss(); }); binding.galleryLayoutTextViewPostLayoutBottomSheetFragment.setOnClickListener(view -> { ((PostLayoutSelectionCallback) activity).postLayoutSelected(SharedPreferencesUtils.POST_LAYOUT_GALLERY); dismiss(); }); binding.cardLayout2TextViewPostLayoutBottomSheetFragment.setOnClickListener(view -> { ((PostLayoutSelectionCallback) activity).postLayoutSelected(SharedPreferencesUtils.POST_LAYOUT_CARD_2); dismiss(); }); binding.cardLayout3TextViewPostLayoutBottomSheetFragment.setOnClickListener(view -> { ((PostLayoutSelectionCallback) activity).postLayoutSelected(SharedPreferencesUtils.POST_LAYOUT_CARD_3); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } public interface PostLayoutSelectionCallback { void postLayoutSelected(int postLayout); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PostModerationActionBottomSheetFragment.kt ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.Fragment import ml.docilealligator.infinityforreddit.PostModerationActionHandler import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment import ml.docilealligator.infinityforreddit.databinding.FragmentModerationActionBottomSheetBinding import ml.docilealligator.infinityforreddit.post.Post private const val EXTRA_POST = "EP" private const val EXTRA_POSITION = "EPO" /** * A simple [Fragment] subclass. * Use the [PostModerationActionBottomSheetFragment.newInstance] factory method to * create an instance of this fragment. */ class PostModerationActionBottomSheetFragment : LandscapeExpandedRoundedBottomSheetDialogFragment() { private var post: Post? = null private var position: Int = -1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { post = it.getParcelable(EXTRA_POST) position = it.getInt(EXTRA_POSITION, -1) } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val binding: FragmentModerationActionBottomSheetBinding = FragmentModerationActionBottomSheetBinding.inflate(inflater, container, false) post?.let { post -> if (parentFragment is PostModerationActionHandler) { if (post.isApproved) { binding.approveTextViewModerationActionBottomSheetFragment.visibility = View.GONE } else { binding.approveTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).approvePost(post, position) dismiss() } } if (post.isRemoved) { binding.removeTextViewModerationActionBottomSheetFragment.visibility = View.GONE binding.spamTextViewModerationActionBottomSheetFragment.visibility = View.GONE } else { binding.removeTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).removePost(post, position, false) dismiss() } binding.spamTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).removePost(post, position, true) dismiss() } } activity?.let { binding.toggleStickyTextViewModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( AppCompatResources.getDrawable(it, if (post.isStickied) R.drawable.ic_unstick_post_24dp else R.drawable.ic_stick_post_24dp), null, null, null ) binding.toggleLockTextViewModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( AppCompatResources.getDrawable(it, if (post.isLocked) R.drawable.ic_unlock_24dp else R.drawable.ic_lock_day_night_24dp), null, null, null ) binding.toggleNsfwTextViewModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( AppCompatResources.getDrawable(it, if (post.isNSFW) R.drawable.ic_unmark_nsfw_24dp else R.drawable.ic_mark_nsfw_24dp), null, null, null ) binding.toggleSpoilerTextViewModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( AppCompatResources.getDrawable(it, if (post.isSpoiler) R.drawable.ic_unmark_spoiler_24dp else R.drawable.ic_spoiler_24dp), null, null, null ) binding.toggleModTextViewModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( AppCompatResources.getDrawable(it, if (post.isModerator) R.drawable.ic_undistinguish_as_mod_24dp else R.drawable.ic_distinguish_as_mod_24dp), null, null, null ) } binding.toggleStickyTextViewModerationActionBottomSheetFragment.setText(if (post.isStickied) R.string.unset_sticky_post else R.string.set_sticky_post) binding.toggleStickyTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).toggleSticky(post, position) dismiss() } binding.toggleLockTextViewModerationActionBottomSheetFragment.setText(if (post.isLocked) R.string.unlock else R.string.lock) binding.toggleLockTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).toggleLock(post, position) dismiss() } binding.toggleNsfwTextViewModerationActionBottomSheetFragment.setText(if (post.isNSFW) R.string.action_unmark_nsfw else R.string.action_mark_nsfw) binding.toggleNsfwTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).toggleNSFW(post, position) dismiss() } binding.toggleSpoilerTextViewModerationActionBottomSheetFragment.setText(if (post.isSpoiler) R.string.action_unmark_spoiler else R.string.action_mark_spoiler) binding.toggleSpoilerTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).toggleSpoiler(post, position) dismiss() } binding.toggleModTextViewModerationActionBottomSheetFragment.setText(if (post.isModerator) R.string.undistinguish_as_mod else R.string.distinguish_as_mod) binding.toggleModTextViewModerationActionBottomSheetFragment.setOnClickListener { (parentFragment as PostModerationActionHandler).toggleMod(post, position) dismiss() } } else { dismiss() } } return binding.root } companion object { @JvmStatic fun newInstance(post: Post, position: Int) = PostModerationActionBottomSheetFragment().apply { arguments = Bundle().apply { putParcelable(EXTRA_POST, post) putInt(EXTRA_POSITION, position) } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PostOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.Manifest; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.PersistableBundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import org.greenrobot.eventbus.EventBus; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.PostModerationActionHandler; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CommentActivity; import ml.docilealligator.infinityforreddit.activities.PostFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.activities.ReportActivity; import ml.docilealligator.infinityforreddit.activities.SubmitCrosspostActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentPostOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostList; import ml.docilealligator.infinityforreddit.post.HidePost; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.services.DownloadRedditVideoService; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; import java.util.ArrayList; /** * A simple {@link Fragment} subclass. * Use the {@link PostOptionsBottomSheetFragment#newInstance} factory method to * create an instance of this fragment. */ public class PostOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private static final String EXTRA_POST = "EP"; private static final String EXTRA_POST_LIST_POSITION = "EPLP"; private static final String EXTRA_GALLERY_INDEX = "EGI"; private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0; private BaseActivity mBaseActivity; private Post mPost; private FragmentPostOptionsBottomSheetBinding binding; private boolean isDownloading = false; private boolean isDownloadingGallery = false; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; public PostOptionsBottomSheetFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param post Post * @return A new instance of fragment PostOptionsBottomSheetFragment. */ public static PostOptionsBottomSheetFragment newInstance(Post post, int postListPosition, int galleryIndex) { PostOptionsBottomSheetFragment fragment = new PostOptionsBottomSheetFragment(); Bundle args = new Bundle(); args.putParcelable(EXTRA_POST, post); args.putInt(EXTRA_POST_LIST_POSITION, postListPosition); args.putInt(EXTRA_GALLERY_INDEX, galleryIndex); fragment.setArguments(args); return fragment; } public static PostOptionsBottomSheetFragment newInstance(Post post, int postListPosition) { PostOptionsBottomSheetFragment fragment = new PostOptionsBottomSheetFragment(); Bundle args = new Bundle(); args.putParcelable(EXTRA_POST, post); args.putInt(EXTRA_POST_LIST_POSITION, postListPosition); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mPost = getArguments().getParcelable(EXTRA_POST); } else { dismiss(); } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((Infinity) mBaseActivity.getApplication()).getAppComponent().inject(this); // Inflate the layout for this fragment binding = FragmentPostOptionsBottomSheetBinding.inflate(inflater, container, false); if (mPost != null) { switch (mPost.getPostType()) { case Post.IMAGE_TYPE: case Post.GALLERY_TYPE: binding.downloadTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE); binding.downloadTextViewPostOptionsBottomSheetFragment.setText(R.string.download_image); break; case Post.GIF_TYPE: binding.downloadTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE); binding.downloadTextViewPostOptionsBottomSheetFragment.setText(R.string.download_gif); break; case Post.VIDEO_TYPE: binding.downloadTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE); binding.downloadTextViewPostOptionsBottomSheetFragment.setText(R.string.download_video); break; } if (binding.downloadTextViewPostOptionsBottomSheetFragment.getVisibility() == View.VISIBLE) { binding.downloadTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { if (isDownloading) { return; } isDownloading = true; requestPermissionAndDownload(); }); } if (mPost.getPostType() == Post.GALLERY_TYPE) { binding.downloadAllTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE); binding.downloadAllTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { if (isDownloadingGallery) { return; } isDownloadingGallery = true; requestPermissionAndDownloadGallery(); }); } binding.shareTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { Bundle bundle = new Bundle(); bundle.putString(ShareBottomSheetFragment.EXTRA_POST_LINK, mPost.getPermalink()); if (mPost.getPostType() != Post.TEXT_TYPE) { bundle.putInt(ShareBottomSheetFragment.EXTRA_MEDIA_TYPE, mPost.getPostType()); switch (mPost.getPostType()) { case Post.IMAGE_TYPE: case Post.GIF_TYPE: case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: bundle.putString(ShareBottomSheetFragment.EXTRA_MEDIA_LINK, mPost.getUrl()); break; case Post.VIDEO_TYPE: bundle.putString(ShareBottomSheetFragment.EXTRA_MEDIA_LINK, mPost.getVideoDownloadUrl()); break; } } bundle.putParcelable(ShareBottomSheetFragment.EXTRA_POST, mPost); ShareBottomSheetFragment shareBottomSheetFragment = new ShareBottomSheetFragment(); shareBottomSheetFragment.setArguments(bundle); Fragment parentFragment = getParentFragment(); if (parentFragment != null) { shareBottomSheetFragment.show(parentFragment.getChildFragmentManager(), shareBottomSheetFragment.getTag()); } else { shareBottomSheetFragment.show(mBaseActivity.getSupportFragmentManager(), shareBottomSheetFragment.getTag()); } dismiss(); }); binding.addToPostFilterTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(mBaseActivity, PostFilterPreferenceActivity.class); intent.putExtra(PostFilterPreferenceActivity.EXTRA_POST, mPost); startActivity(intent); dismiss(); }); if (mBaseActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.commentTextViewPostOptionsBottomSheetFragment.setVisibility(View.GONE); binding.hidePostTextViewPostOptionsBottomSheetFragment.setVisibility(View.GONE); binding.crosspostTextViewPostOptionsBottomSheetFragment.setVisibility(View.GONE); binding.reportTextViewPostOptionsBottomSheetFragment.setVisibility(View.GONE); } else { if (mPost.isLocked() || mPost.isArchived()) { binding.commentTextViewPostOptionsBottomSheetFragment.setVisibility(View.GONE); } else { binding.commentTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(mBaseActivity, CommentActivity.class); intent.putExtra(CommentActivity.EXTRA_PARENT_FULLNAME_KEY, mPost.getFullName()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_TITLE_KEY, mPost.getTitle()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_MARKDOWN_KEY, mPost.getSelfText()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_KEY, mPost.getSelfTextPlain()); intent.putExtra(CommentActivity.EXTRA_SUBREDDIT_NAME_KEY, mPost.getSubredditName()); intent.putExtra(CommentActivity.EXTRA_IS_REPLYING_KEY, false); intent.putExtra(CommentActivity.EXTRA_PARENT_DEPTH_KEY, 0); mBaseActivity.startActivity(intent); dismiss(); }); } if (mPost.isHidden()) { binding.hidePostTextViewPostOptionsBottomSheetFragment.setText(R.string.action_unhide_post); } else { binding.hidePostTextViewPostOptionsBottomSheetFragment.setText(R.string.action_hide_post); } binding.hidePostTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { if (mPost.isHidden()) { HidePost.unhidePost(mOauthRetrofit, mBaseActivity.accessToken, mPost.getFullName(), new HidePost.HidePostListener() { @Override public void success() { mPost.setHidden(false); Toast.makeText(mBaseActivity, R.string.post_unhide_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, getArguments().getInt(EXTRA_POST_LIST_POSITION, 0))); dismiss(); } @Override public void failed() { mPost.setHidden(true); Toast.makeText(mBaseActivity, R.string.post_unhide_failed, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, getArguments().getInt(EXTRA_POST_LIST_POSITION, 0))); dismiss(); } }); } else { HidePost.hidePost(mOauthRetrofit, mBaseActivity.accessToken, mPost.getFullName(), new HidePost.HidePostListener() { @Override public void success() { mPost.setHidden(true); Toast.makeText(mBaseActivity, R.string.post_hide_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, getArguments().getInt(EXTRA_POST_LIST_POSITION, 0))); dismiss(); } @Override public void failed() { mPost.setHidden(false); Toast.makeText(mBaseActivity, R.string.post_hide_failed, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, getArguments().getInt(EXTRA_POST_LIST_POSITION, 0))); dismiss(); } }); } }); binding.crosspostTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { Intent submitCrosspostIntent = new Intent(mBaseActivity, SubmitCrosspostActivity.class); submitCrosspostIntent.putExtra(SubmitCrosspostActivity.EXTRA_POST, mPost); startActivity(submitCrosspostIntent); dismiss(); }); if (mPost.getAuthor().equals(mBaseActivity.accountName)) { binding.notificationTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE); binding.notificationTextViewPostOptionsBottomSheetFragment.setText(mPost.isSendReplies() ? R.string.disable_reply_notifications : R.string.enable_reply_notifications); binding.notificationTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { if (getParentFragment() instanceof PostModerationActionHandler) { ((PostModerationActionHandler) getParentFragment()).toggleNotification(mPost, getArguments().getInt(EXTRA_POST_LIST_POSITION, 0)); } dismiss(); }); } binding.reportTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(mBaseActivity, ReportActivity.class); intent.putExtra(ReportActivity.EXTRA_SUBREDDIT_NAME, mPost.getSubredditName()); intent.putExtra(ReportActivity.EXTRA_THING_FULLNAME, mPost.getFullName()); startActivity(intent); dismiss(); }); if (mPost.isCanModPost()) { binding.modTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE); binding.modTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> { PostModerationActionBottomSheetFragment postModerationActionBottomSheetFragment = PostModerationActionBottomSheetFragment.newInstance(mPost, getArguments().getInt(EXTRA_POST_LIST_POSITION, 0)); Fragment parentFragment = getParentFragment(); if (parentFragment != null) { postModerationActionBottomSheetFragment.show(parentFragment.getChildFragmentManager(), postModerationActionBottomSheetFragment.getTag()); } else { postModerationActionBottomSheetFragment.show(mBaseActivity.getSupportFragmentManager(), postModerationActionBottomSheetFragment.getTag()); } dismiss(); }); } } if (mPost.isApproved()) { binding.statusTextViewPostOptionsBottomSheetFragment.setText(getString(R.string.approved_status, mPost.getApprovedBy())); } else if (mPost.isRemoved()) { if (mPost.isSpam()) { binding.statusTextViewPostOptionsBottomSheetFragment.setText(R.string.post_spam_status); } else { binding.statusTextViewPostOptionsBottomSheetFragment.setText(R.string.post_removed_status); } } else { binding.statusTextViewPostOptionsBottomSheetFragment.setVisibility(View.GONE); } } if (mBaseActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mBaseActivity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mBaseActivity = (BaseActivity) context; } private void requestPermissionAndDownload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(mBaseActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted download(); } } else { download(); } } private void requestPermissionAndDownloadGallery() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(mBaseActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted downloadGallery(); } } else { downloadGallery(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(mBaseActivity, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); isDownloading = false; isDownloadingGallery = false; } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (isDownloading) { download(); } else if (isDownloadingGallery) { downloadGallery(); } } } } private void download() { isDownloading = false; // Check if download location is set SharedPreferences sharedPreferences = mSharedPreferences; String downloadLocation; boolean isNsfw = mPost.isNSFW(); int mediaType; switch (mPost.getPostType()) { case Post.VIDEO_TYPE: mediaType = DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO; break; case Post.GIF_TYPE: mediaType = DownloadMediaService.EXTRA_MEDIA_TYPE_GIF; break; default: mediaType = DownloadMediaService.EXTRA_MEDIA_TYPE_IMAGE; break; } if (isNsfw && sharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); } else { if (mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO) { downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); } else if (mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_GIF) { downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); } else { downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); } } if (downloadLocation == null || downloadLocation.isEmpty()) { Toast.makeText(mBaseActivity, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); dismiss(); return; } Toast.makeText(mBaseActivity, R.string.download_started, Toast.LENGTH_SHORT).show(); if (mPost.getPostType() == Post.VIDEO_TYPE) { if (!mPost.isRedgifs() && !mPost.isStreamable() && !mPost.isImgur()) { PersistableBundle extras = new PersistableBundle(); extras.putString(DownloadRedditVideoService.EXTRA_VIDEO_URL, mPost.getVideoDownloadUrl()); extras.putString(DownloadRedditVideoService.EXTRA_POST_ID, mPost.getId()); extras.putString(DownloadRedditVideoService.EXTRA_SUBREDDIT, mPost.getSubredditName()); extras.putInt(DownloadRedditVideoService.EXTRA_IS_NSFW, mPost.isNSFW() ? 1 : 0); String title = (mPost != null) ? mPost.getTitle() : "reddit_video"; // Get title or use default String sanitizedTitle = title.replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("[\\s_]+", "_").replaceAll("^_+|_+$", ""); if (sanitizedTitle.length() > 100) sanitizedTitle = sanitizedTitle.substring(0, 100).replaceAll("_+$", ""); if (sanitizedTitle.isEmpty()) sanitizedTitle = "reddit_video_" + System.currentTimeMillis(); String finalFileName = sanitizedTitle + ".mp4"; extras.putString(DownloadRedditVideoService.EXTRA_FILE_NAME, finalFileName); //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadRedditVideoService.constructJobInfo(mBaseActivity, 5000000, extras); ((JobScheduler) mBaseActivity.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); dismiss(); return; } } JobInfo jobInfo = DownloadMediaService.constructJobInfo(mBaseActivity, 5000000, mPost, getArguments().getInt(EXTRA_GALLERY_INDEX, 0)); ((JobScheduler) mBaseActivity.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); dismiss(); } private void downloadGallery() { isDownloadingGallery = false; // Check if download locations are set for all media types SharedPreferences sharedPreferences = mSharedPreferences; String imageDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String gifDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); String videoDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); String nsfwDownloadLocation = ""; boolean needsNsfwLocation = mPost.isNSFW() && sharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false); if (needsNsfwLocation) { nsfwDownloadLocation = sharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); if (nsfwDownloadLocation == null || nsfwDownloadLocation.isEmpty()) { Toast.makeText(mBaseActivity, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); dismiss(); return; } } else { // Check for required download locations based on the gallery content boolean hasImage = false; boolean hasGif = false; boolean hasVideo = false; ArrayList gallery = mPost.getGallery(); for (Post.Gallery galleryItem : gallery) { if (galleryItem.mediaType == Post.Gallery.TYPE_VIDEO) { hasVideo = true; } else if (galleryItem.mediaType == Post.Gallery.TYPE_GIF) { hasGif = true; } else { hasImage = true; } } if ((hasImage && (imageDownloadLocation == null || imageDownloadLocation.isEmpty())) || (hasGif && (gifDownloadLocation == null || gifDownloadLocation.isEmpty())) || (hasVideo && (videoDownloadLocation == null || videoDownloadLocation.isEmpty()))) { Toast.makeText(mBaseActivity, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); dismiss(); return; } } Toast.makeText(mBaseActivity, R.string.download_started, Toast.LENGTH_SHORT).show(); JobInfo jobInfo = DownloadMediaService.constructGalleryDownloadAllMediaJobInfo(mBaseActivity, 5000000, mPost); ((JobScheduler) mBaseActivity.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); dismiss(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/PostTypeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentPostTypeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class PostTypeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final int TYPE_TEXT = 0; public static final int TYPE_LINK = 1; public static final int TYPE_IMAGE = 2; public static final int TYPE_VIDEO = 3; public static final int TYPE_GALLERY = 4; public static final int TYPE_POLL = 5; private BaseActivity activity; public PostTypeBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentPostTypeBottomSheetBinding binding = FragmentPostTypeBottomSheetBinding.inflate(inflater, container, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } binding.textTypeLinearLayoutPostTypeBottomSheetFragment.setOnClickListener(view -> { ((PostTypeSelectionCallback) activity).postTypeSelected(TYPE_TEXT); dismiss(); }); binding.linkTypeLinearLayoutPostTypeBottomSheetFragment.setOnClickListener(view -> { ((PostTypeSelectionCallback) activity).postTypeSelected(TYPE_LINK); dismiss(); }); binding.imageTypeLinearLayoutPostTypeBottomSheetFragment.setOnClickListener(view -> { ((PostTypeSelectionCallback) activity).postTypeSelected(TYPE_IMAGE); dismiss(); }); binding.videoTypeLinearLayoutPostTypeBottomSheetFragment.setOnClickListener(view -> { ((PostTypeSelectionCallback) activity).postTypeSelected(TYPE_VIDEO); dismiss(); }); binding.galleryTypeLinearLayoutPostTypeBottomSheetFragment.setOnClickListener(view -> { ((PostTypeSelectionCallback) activity).postTypeSelected(TYPE_GALLERY); dismiss(); }); binding.pollTypeLinearLayoutPostTypeBottomSheetFragment.setOnClickListener(view -> { ((PostTypeSelectionCallback) activity).postTypeSelected(TYPE_POLL); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } public interface PostTypeSelectionCallback { void postTypeSelected(int postType); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SearchPostSortTypeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSearchPostSortTypeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class SearchPostSortTypeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_CURRENT_SORT_TYPE = "ECST"; private BaseActivity activity; public SearchPostSortTypeBottomSheetFragment() { // Required empty public constructor } public static SearchPostSortTypeBottomSheetFragment getNewInstance(SortType currentSortType) { SearchPostSortTypeBottomSheetFragment fragment = new SearchPostSortTypeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(EXTRA_CURRENT_SORT_TYPE, currentSortType.getType().fullName); fragment.setArguments(bundle); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentSearchPostSortTypeBottomSheetBinding binding = FragmentSearchPostSortTypeBottomSheetBinding.inflate(inflater, container, false); String currentSortType = getArguments().getString(EXTRA_CURRENT_SORT_TYPE); if (currentSortType.equals(SortType.Type.RELEVANCE.fullName)) { binding.relevanceTypeTextViewSearchSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.relevanceTypeTextViewSearchSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.HOT.fullName)) { binding.hotTypeTextViewSearchSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.hotTypeTextViewSearchSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.TOP.fullName)) { binding.topTypeTextViewSearchSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.topTypeTextViewSearchSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.NEW.fullName)) { binding.newTypeTextViewSearchSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.newTypeTextViewSearchSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.RISING.fullName)) { binding.commentsTypeTextViewSearchSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.commentsTypeTextViewSearchSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } binding.relevanceTypeTextViewSearchSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.RELEVANCE.name()); dismiss(); }); binding.hotTypeTextViewSearchSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.HOT.name()); dismiss(); }); binding.topTypeTextViewSearchSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.TOP.name()); dismiss(); }); binding.newTypeTextViewSearchSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.NEW)); dismiss(); }); binding.commentsTypeTextViewSearchSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.COMMENTS.name()); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SearchUserAndSubredditSortTypeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSearchUserAndSubredditSortTypeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class SearchUserAndSubredditSortTypeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_FRAGMENT_POSITION = "EFP"; public static final String EXTRA_CURRENT_SORT_TYPE = "ECST"; private BaseActivity activity; public SearchUserAndSubredditSortTypeBottomSheetFragment() { // Required empty public constructor } public static SearchUserAndSubredditSortTypeBottomSheetFragment getNewInstance(int fragmentPosition, SortType currentSortType) { SearchUserAndSubredditSortTypeBottomSheetFragment fragment = new SearchUserAndSubredditSortTypeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putInt(EXTRA_FRAGMENT_POSITION, fragmentPosition); bundle.putString(EXTRA_CURRENT_SORT_TYPE, currentSortType.getType().fullName); fragment.setArguments(bundle); return fragment; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentSearchUserAndSubredditSortTypeBottomSheetBinding binding = FragmentSearchUserAndSubredditSortTypeBottomSheetBinding.inflate(inflater, container, false); String currentSortType = getArguments().getString(EXTRA_CURRENT_SORT_TYPE); if (currentSortType.equals(SortType.Type.RELEVANCE.fullName)) { binding.relevanceTypeTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.relevanceTypeTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.ACTIVITY.fullName)) { binding.activityTypeTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.activityTypeTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } int position = getArguments() != null ? getArguments().getInt(EXTRA_FRAGMENT_POSITION) : -1; if(position < 0) { dismiss(); return binding.getRoot(); } binding.relevanceTypeTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).searchUserAndSubredditSortTypeSelected(new SortType(SortType.Type.RELEVANCE), position); dismiss(); }); binding.activityTypeTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).searchUserAndSubredditSortTypeSelected(new SortType(SortType.Type.ACTIVITY), position); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SelectOrCaptureImageBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.PostGalleryActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSelectOrCaptureImageBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class SelectOrCaptureImageBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private PostGalleryActivity mActivity; public SelectOrCaptureImageBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentSelectOrCaptureImageBottomSheetBinding binding = FragmentSelectOrCaptureImageBottomSheetBinding.inflate(inflater, container, false); binding.selectImageTextViewSelectOrCaptureImageBottomSheetFragment.setOnClickListener(view -> { mActivity.selectImage(); dismiss(); }); binding.captureImageTextViewSelectOrCaptureImageBottomSheetFragment.setOnClickListener(view -> { mActivity.captureImage(); dismiss(); }); if (mActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mActivity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (PostGalleryActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SelectSubredditsOrUsersOptionsBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.SelectedSubredditsAndUsersActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSelectSubredditsOrUsersOptionsBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class SelectSubredditsOrUsersOptionsBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { private SelectedSubredditsAndUsersActivity activity; public SelectSubredditsOrUsersOptionsBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentSelectSubredditsOrUsersOptionsBottomSheetBinding binding = FragmentSelectSubredditsOrUsersOptionsBottomSheetBinding.inflate(inflater, container, false); binding.selectSubredditsTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.setOnClickListener(view -> { activity.selectSubreddits(); dismiss(); }); binding.selectUsersTextViewSearchUserAndSubredditSortTypeBottomSheetFragment.setOnClickListener(view -> { activity.selectUsers(); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (SelectedSubredditsAndUsersActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SetAsWallpaperBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.app.Activity; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; import ml.docilealligator.infinityforreddit.activities.ViewImgurMediaActivity; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSetAsWallpaperBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class SetAsWallpaperBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_VIEW_PAGER_POSITION = "EVPP"; private Activity mActivity; public SetAsWallpaperBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentSetAsWallpaperBottomSheetBinding binding = FragmentSetAsWallpaperBottomSheetBinding.inflate(inflater, container, false); Bundle bundle = getArguments(); int viewPagerPosition = bundle == null ? -1 : bundle.getInt(EXTRA_VIEW_PAGER_POSITION); binding.bothTextViewSetAsWallpaperBottomSheetFragment.setOnClickListener(view -> { if (mActivity instanceof SetAsWallpaperCallback) { ((SetAsWallpaperCallback) mActivity).setToBoth(viewPagerPosition); } dismiss(); }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { binding.homeScreenTextViewSetAsWallpaperBottomSheetFragment.setVisibility(View.VISIBLE); binding.lockScreenTextViewSetAsWallpaperBottomSheetFragment.setVisibility(View.VISIBLE); binding.homeScreenTextViewSetAsWallpaperBottomSheetFragment.setOnClickListener(view -> { if (mActivity instanceof SetAsWallpaperCallback) { ((SetAsWallpaperCallback) mActivity).setToHomeScreen(viewPagerPosition); } dismiss(); }); binding.lockScreenTextViewSetAsWallpaperBottomSheetFragment.setOnClickListener(view -> { if (mActivity instanceof SetAsWallpaperCallback) { ((SetAsWallpaperCallback) mActivity).setToLockScreen(viewPagerPosition); } dismiss(); }); } if (mActivity instanceof ViewVideoActivity) { if (((ViewVideoActivity) mActivity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((ViewVideoActivity) mActivity).typeface); } } else if (mActivity instanceof ViewImgurMediaActivity) { if (((ViewImgurMediaActivity) mActivity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((ViewImgurMediaActivity) mActivity).typeface); } } else if (mActivity instanceof ViewRedditGalleryActivity) { if (((ViewRedditGalleryActivity) mActivity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((ViewRedditGalleryActivity) mActivity).typeface); } } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.mActivity = (Activity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SetRedditGalleryItemCaptionAndUrlBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.reflect.Field; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.PostGalleryActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSetRedditGalleryItemCaptionAndUrlBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class SetRedditGalleryItemCaptionAndUrlBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_POSITION = "EP"; public static final String EXTRA_CAPTION = "EC"; public static final String EXTRA_URL = "EU"; private PostGalleryActivity mActivity; public SetRedditGalleryItemCaptionAndUrlBottomSheetFragment() { } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { FragmentSetRedditGalleryItemCaptionAndUrlBottomSheetBinding binding = FragmentSetRedditGalleryItemCaptionAndUrlBottomSheetBinding.inflate(inflater, container, false); int primaryTextColor = mActivity.getResources().getColor(R.color.primaryTextColor); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { binding.captionTextInputLayoutSetRedditGalleryItemCaptionAndUrlBottomSheetFragment.setCursorColor(ColorStateList.valueOf(primaryTextColor)); binding.urlTextInputLayoutSetRedditGalleryItemCaptionAndUrlBottomSheetFragment.setCursorColor(ColorStateList.valueOf(primaryTextColor)); } else { setCursorDrawableColor(binding.captionTextInputEditTextSetRedditGalleryItemCaptionAndUrlBottomSheetFragment, primaryTextColor); setCursorDrawableColor(binding.urlTextInputEditTextSetRedditGalleryItemCaptionAndUrlBottomSheetFragment, primaryTextColor); } int position = getArguments().getInt(EXTRA_POSITION, -1); String caption = getArguments().getString(EXTRA_CAPTION, ""); String url = getArguments().getString(EXTRA_URL, ""); binding.captionTextInputEditTextSetRedditGalleryItemCaptionAndUrlBottomSheetFragment.setText(caption); binding.urlTextInputEditTextSetRedditGalleryItemCaptionAndUrlBottomSheetFragment.setText(url); binding.okButtonSetRedditGalleryItemCaptionAndUrlBottomSheetFragment.setOnClickListener(view -> { mActivity.setCaptionAndUrl(position, binding.captionTextInputEditTextSetRedditGalleryItemCaptionAndUrlBottomSheetFragment.getText().toString(), binding.urlTextInputEditTextSetRedditGalleryItemCaptionAndUrlBottomSheetFragment.getText().toString()); dismiss(); }); if (mActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mActivity.typeface); } return binding.getRoot(); } private void setCursorDrawableColor(EditText editText, int color) { try { @SuppressLint("SoonBlockedPrivateApi") Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); fCursorDrawableRes.setAccessible(true); int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); Field fEditor = TextView.class.getDeclaredField("mEditor"); fEditor.setAccessible(true); Object editor = fEditor.get(editText); Class clazz = editor.getClass(); Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); fCursorDrawable.setAccessible(true); Drawable[] drawables = new Drawable[2]; drawables[0] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[1] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); fCursorDrawable.set(editor, drawables); } catch (Throwable ignored) { } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (PostGalleryActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/ShareBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.comment.FetchComment; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentShareLinkBottomSheetBinding; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.ShareScreenshotUtilsKt; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; /** * A simple {@link Fragment} subclass. */ public class ShareBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_POST_LINK = "EPL"; public static final String EXTRA_MEDIA_LINK = "EML"; public static final String EXTRA_MEDIA_TYPE = "EMT"; public static final String EXTRA_POST = "EP"; public static final String EXTRA_COMMENTS = "EC"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject Executor mExecutor; private BaseActivity activity; public ShareBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((Infinity) activity.getApplication()).getAppComponent().inject(this); // Inflate the layout for this fragment FragmentShareLinkBottomSheetBinding binding = FragmentShareLinkBottomSheetBinding.inflate(inflater, container, false); String postLink = getArguments().getString(EXTRA_POST_LINK); String mediaLink = getArguments().containsKey(EXTRA_MEDIA_LINK) ? getArguments().getString(EXTRA_MEDIA_LINK) : null; Post post = getArguments().getParcelable(EXTRA_POST); ArrayList comments = getArguments().getParcelableArrayList(EXTRA_COMMENTS); binding.postLinkTextViewShareLinkBottomSheetFragment.setText(postLink); if (mediaLink != null) { binding.mediaLinkTextViewShareLinkBottomSheetFragment.setVisibility(View.VISIBLE); binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setVisibility(View.VISIBLE); binding.copyMediaLinkTextViewShareLinkBottomSheetFragment.setVisibility(View.VISIBLE); binding.mediaLinkTextViewShareLinkBottomSheetFragment.setText(mediaLink); int mediaType = getArguments().getInt(EXTRA_MEDIA_TYPE); switch (mediaType) { case Post.IMAGE_TYPE: binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.share_image_link); binding.copyMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.copy_image_link); binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( activity.getDrawable(R.drawable.ic_image_day_night_24dp), null, null, null); break; case Post.GIF_TYPE: binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.share_gif_link); binding.copyMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.copy_gif_link); binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( activity.getDrawable(R.drawable.ic_image_day_night_24dp), null, null, null); break; case Post.VIDEO_TYPE: binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.share_video_link); binding.copyMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.copy_video_link); binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds( activity.getDrawable(R.drawable.ic_video_day_night_24dp), null, null, null); break; case Post.LINK_TYPE: case Post.NO_PREVIEW_LINK_TYPE: binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.share_link); binding.copyMediaLinkTextViewShareLinkBottomSheetFragment.setText(R.string.copy_link); break; } binding.shareMediaLinkTextViewShareLinkBottomSheetFragment.setOnClickListener(view -> { shareLink(mediaLink); dismiss(); }); binding.copyMediaLinkTextViewShareLinkBottomSheetFragment.setOnClickListener(view -> { copyLink(mediaLink); dismiss(); }); } binding.sharePostLinkTextViewShareLinkBottomSheetFragment.setOnClickListener(view -> { shareLink(postLink); dismiss(); }); binding.copyPostLinkTextViewShareLinkBottomSheetFragment.setOnClickListener(view -> { copyLink(postLink); dismiss(); }); if (post != null) { binding.shareAsImageTextViewShareLinkBottomSheetFragment.setVisibility(View.VISIBLE); binding.shareAsImageTextViewShareLinkBottomSheetFragment.setOnClickListener(view -> { ShareScreenshotUtilsKt.sharePostAsScreenshot( activity, post, activity.customThemeWrapper, activity.getResources().getConfiguration().locale, activity.getDefaultSharedPreferences().getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE), new SaveMemoryCenterInisdeDownsampleStrategy( Integer.parseInt(activity.getDefaultSharedPreferences() .getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"))) ); dismiss(); }); binding.shareWithCommentsTextViewShareLinkBottomSheetFragment.setVisibility(View.VISIBLE); binding.shareWithCommentsTextViewShareLinkBottomSheetFragment.setOnClickListener(view -> { if (comments != null && !comments.isEmpty()) { shareWithComments(post, comments); } else { Toast.makeText(activity, R.string.loading, Toast.LENGTH_SHORT).show(); dismiss(); Retrofit retrofit = activity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit; FetchComment.fetchComments(mExecutor, new Handler(Looper.getMainLooper()), retrofit, activity.accessToken, activity.accountName, post.getId(), null, SortType.Type.BEST, null, false, new CommentFilter(), new FetchComment.FetchCommentListener() { @Override public void onFetchCommentSuccess(ArrayList expandedComments, String parentId, ArrayList children) { shareWithComments(post, expandedComments); } @Override public void onFetchCommentFailed() { Toast.makeText(activity, R.string.error_loading_comments_for_share, Toast.LENGTH_SHORT).show(); } }); } }); } if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } private void shareWithComments(Post post, ArrayList comments) { ShareScreenshotUtilsKt.sharePostWithCommentsAsScreenshot( activity, post, comments, activity.customThemeWrapper, activity.getResources().getConfiguration().locale, activity.getDefaultSharedPreferences().getString(SharedPreferencesUtils.TIME_FORMAT_KEY, SharedPreferencesUtils.TIME_FORMAT_DEFAULT_VALUE), new SaveMemoryCenterInisdeDownsampleStrategy( Integer.parseInt(activity.getDefaultSharedPreferences() .getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"))) ); } private void shareLink(String link) { try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, link); activity.startActivity(Intent.createChooser(intent, getString(R.string.share))); } catch (ActivityNotFoundException e) { Toast.makeText(activity, R.string.no_activity_found_for_share, Toast.LENGTH_SHORT).show(); } } private void copyLink(String link) { activity.copyLink(link); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SortTimeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSortTimeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class SortTimeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_SORT_TYPE = "EST"; private BaseActivity activity; public SortTimeBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentSortTimeBottomSheetBinding binding = FragmentSortTimeBottomSheetBinding.inflate(inflater, container, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } String sortType = getArguments() != null ? getArguments().getString(EXTRA_SORT_TYPE) : null; if (sortType == null) { dismiss(); return binding.getRoot(); } binding.hourTextViewSortTimeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity) .sortTypeSelected(new SortType(SortType.Type.valueOf(sortType), SortType.Time.HOUR)); dismiss(); }); binding.dayTextViewSortTimeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity) .sortTypeSelected(new SortType(SortType.Type.valueOf(sortType), SortType.Time.DAY)); dismiss(); }); binding.weekTextViewSortTimeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity) .sortTypeSelected(new SortType(SortType.Type.valueOf(sortType), SortType.Time.WEEK)); dismiss(); }); binding.monthTextViewSortTimeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity) .sortTypeSelected(new SortType(SortType.Type.valueOf(sortType), SortType.Time.MONTH)); dismiss(); }); binding.yearTextViewSortTimeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity) .sortTypeSelected(new SortType(SortType.Type.valueOf(sortType), SortType.Time.YEAR)); dismiss(); }); binding.allTimeTextViewSortTimeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity) .sortTypeSelected(new SortType(SortType.Type.valueOf(sortType), SortType.Time.ALL)); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/SortTypeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentSortTypeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class SortTypeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_NO_BEST_TYPE = "ENBT"; public static final String EXTRA_CURRENT_SORT_TYPE = "ECST"; private BaseActivity activity; public SortTypeBottomSheetFragment() { // Required empty public constructor } public static SortTypeBottomSheetFragment getNewInstance(boolean isNoBestType, SortType currentSortType) { SortTypeBottomSheetFragment fragment = new SortTypeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(EXTRA_NO_BEST_TYPE, isNoBestType); bundle.putString(EXTRA_CURRENT_SORT_TYPE, currentSortType.getType().fullName); fragment.setArguments(bundle); return fragment; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentSortTypeBottomSheetBinding binding = FragmentSortTypeBottomSheetBinding.inflate(inflater, container, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } if (getArguments().getBoolean(EXTRA_NO_BEST_TYPE)) { binding.bestTypeTextViewSortTypeBottomSheetFragment.setVisibility(View.GONE); } else { binding.bestTypeTextViewSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.BEST)); dismiss(); }); } String currentSortType = getArguments().getString(EXTRA_CURRENT_SORT_TYPE); if (currentSortType.equals(SortType.Type.BEST.fullName)) { binding.bestTypeTextViewSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.bestTypeTextViewSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.HOT.fullName)) { binding.hotTypeTextViewSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.hotTypeTextViewSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.NEW.fullName)) { binding.newTypeTextViewSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.newTypeTextViewSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.RISING.fullName)) { binding.risingTypeTextViewSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.risingTypeTextViewSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.TOP.fullName)) { binding.topTypeTextViewSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.topTypeTextViewSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.CONTROVERSIAL.fullName)) { binding.controversialTypeTextViewSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.controversialTypeTextViewSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } binding.hotTypeTextViewSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.HOT)); dismiss(); }); binding.newTypeTextViewSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.NEW)); dismiss(); }); binding.risingTypeTextViewSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.RISING)); dismiss(); }); binding.topTypeTextViewSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.TOP.name()); dismiss(); }); binding.controversialTypeTextViewSortTypeBottomSheetFragment.setOnClickListener(view -> { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.CONTROVERSIAL.name()); dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/UploadedImagesBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.activities.UploadImageEnabledActivity; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.UploadedImagesRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentUploadedImagesBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class UploadedImagesBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_UPLOADED_IMAGES = "EUI"; private UploadedImagesRecyclerViewAdapter adapter; private UploadImageEnabledActivity activity; public UploadedImagesBottomSheetFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentUploadedImagesBottomSheetBinding binding = FragmentUploadedImagesBottomSheetBinding.inflate(inflater, container, false); binding.getRoot().setNestedScrollingEnabled(true); binding.uploadButtonUploadedImagesBottomSheetFragment.setOnClickListener(view -> { activity.uploadImage(); dismiss(); }); binding.captureButtonUploadedImagesBottomSheetFragment.setOnClickListener(view -> { activity.captureImage(); dismiss(); }); adapter = new UploadedImagesRecyclerViewAdapter(getActivity(), getArguments().getParcelableArrayList(EXTRA_UPLOADED_IMAGES), uploadedImage -> { activity.insertImageUrl(uploadedImage); dismiss(); }); binding.recyclerViewUploadedImagesBottomSheet.setAdapter(adapter); Activity baseActivity = getActivity(); if (baseActivity instanceof BaseActivity) { if (((BaseActivity) activity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((BaseActivity) activity).typeface); } } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (UploadImageEnabledActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/UrlMenuBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentUrlMenuBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class UrlMenuBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_URL = "EU"; private Activity activity; private String url; public UrlMenuBottomSheetFragment() { // Required empty public constructor } @NonNull public static UrlMenuBottomSheetFragment newInstance(String url) { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(UrlMenuBottomSheetFragment.EXTRA_URL, url); urlMenuBottomSheetFragment.setArguments(bundle); return urlMenuBottomSheetFragment; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentUrlMenuBottomSheetBinding binding = FragmentUrlMenuBottomSheetBinding.inflate(inflater, container, false); url = getArguments().getString(EXTRA_URL); Uri uri = Uri.parse(url); if (uri.getScheme() == null && uri.getHost() == null) { url = "https://www.reddit.com" + url; } binding.linkTextViewUrlMenuBottomSheetFragment.setText(url); binding.openLinkTextViewUrlMenuBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(activity, LinkResolverActivity.class); intent.setData(Uri.parse(url)); activity.startActivity(intent); dismiss(); }); binding.copyLinkTextViewUrlMenuBottomSheetFragment.setOnClickListener(view -> { ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("simple text", url); clipboard.setPrimaryClip(clip); if (android.os.Build.VERSION.SDK_INT < 33) { Toast.makeText(activity, R.string.copy_success, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(activity, R.string.copy_link_failed, Toast.LENGTH_SHORT).show(); } dismiss(); }); binding.shareLinkTextViewUrlMenuBottomSheetFragment.setOnClickListener(view -> { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, url); try { Intent shareIntent = Intent.createChooser(intent, null); startActivity(shareIntent); } catch (ActivityNotFoundException e) { e.printStackTrace(); Toast.makeText(activity, R.string.no_app, Toast.LENGTH_SHORT).show(); } dismiss(); }); if (activity instanceof BaseActivity) { if (((BaseActivity) activity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((BaseActivity) activity).typeface); } } else if (activity instanceof ViewRedditGalleryActivity) { if (((ViewRedditGalleryActivity) activity).typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), ((ViewRedditGalleryActivity) activity).typeface); } } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (Activity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/bottomsheetfragments/UserThingSortTypeBottomSheetFragment.java ================================================ package ml.docilealligator.infinityforreddit.bottomsheetfragments; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.SortTypeSelectionCallback; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentUserThingSortTypeBottomSheetBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class UserThingSortTypeBottomSheetFragment extends LandscapeExpandedRoundedBottomSheetDialogFragment { public static final String EXTRA_CURRENT_SORT_TYPE = "ECST"; private BaseActivity activity; public UserThingSortTypeBottomSheetFragment() { // Required empty public constructor } public static UserThingSortTypeBottomSheetFragment getNewInstance(SortType currentSortType) { UserThingSortTypeBottomSheetFragment fragment = new UserThingSortTypeBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(EXTRA_CURRENT_SORT_TYPE, currentSortType.getType().fullName); fragment.setArguments(bundle); return fragment; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentUserThingSortTypeBottomSheetBinding binding = FragmentUserThingSortTypeBottomSheetBinding.inflate(inflater, container, false); String currentSortType = getArguments().getString(EXTRA_CURRENT_SORT_TYPE); if (currentSortType.equals(SortType.Type.NEW.fullName)) { binding.newTypeTextViewUserThingSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.newTypeTextViewUserThingSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.HOT.fullName)) { binding.hotTypeTextViewUserThingSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.hotTypeTextViewUserThingSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.TOP.fullName)) { binding.topTypeTextViewUserThingSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.topTypeTextViewUserThingSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } else if (currentSortType.equals(SortType.Type.CONTROVERSIAL.fullName)) { binding.controversialTypeTextViewUserThingSortTypeBottomSheetFragment.setCompoundDrawablesRelativeWithIntrinsicBounds(binding.controversialTypeTextViewUserThingSortTypeBottomSheetFragment.getCompoundDrawablesRelative()[0], null, AppCompatResources.getDrawable(activity, R.drawable.ic_check_circle_day_night_24dp), null); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { binding.getRoot().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } binding.newTypeTextViewUserThingSortTypeBottomSheetFragment.setOnClickListener(view -> { if (activity != null) { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.NEW)); } dismiss(); }); binding.hotTypeTextViewUserThingSortTypeBottomSheetFragment.setOnClickListener(view -> { if (activity != null) { ((SortTypeSelectionCallback) activity).sortTypeSelected(new SortType(SortType.Type.HOT)); } dismiss(); }); binding.topTypeTextViewUserThingSortTypeBottomSheetFragment.setOnClickListener(view -> { if (activity != null) { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.TOP.name()); } dismiss(); }); binding.controversialTypeTextViewUserThingSortTypeBottomSheetFragment.setOnClickListener(view -> { if (activity != null) { ((SortTypeSelectionCallback) activity).sortTypeSelected(SortType.Type.CONTROVERSIAL.name()); } dismiss(); }); if (activity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), activity.typeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.activity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/broadcastreceivers/DownloadedMediaDeleteActionBroadcastReceiver.java ================================================ package ml.docilealligator.infinityforreddit.broadcastreceivers; import static android.content.Context.NOTIFICATION_SERVICE; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.Uri; import androidx.documentfile.provider.DocumentFile; import java.io.File; public class DownloadedMediaDeleteActionBroadcastReceiver extends BroadcastReceiver { public static final String EXTRA_NOTIFICATION_ID = "ENI"; @Override public void onReceive(Context context, Intent intent) { Uri mediaUri = intent.getData(); if (mediaUri != null) { try { context.getContentResolver().delete(mediaUri, null, null); } catch (Exception e) { DocumentFile file = DocumentFile.fromSingleUri(context, mediaUri); if (file != null) { if (!file.delete()) { new File(mediaUri.toString()).delete(); } } } } NotificationManager manager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); manager.cancel(intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/broadcastreceivers/NetworkWifiStatusReceiver.java ================================================ package ml.docilealligator.infinityforreddit.broadcastreceivers; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class NetworkWifiStatusReceiver extends BroadcastReceiver { private final NetworkWifiStatusReceiverListener networkWifiStatusReceiverListener; public interface NetworkWifiStatusReceiverListener { void networkStatusChange(); } public NetworkWifiStatusReceiver(NetworkWifiStatusReceiverListener listener) { networkWifiStatusReceiverListener = listener; } @Override public void onReceive(Context context, Intent intent) { networkWifiStatusReceiverListener.networkStatusChange(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/broadcastreceivers/WallpaperChangeReceiver.java ================================================ package ml.docilealligator.infinityforreddit.broadcastreceivers; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import androidx.work.ExistingWorkPolicy; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import ml.docilealligator.infinityforreddit.worker.MaterialYouWorker; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class WallpaperChangeReceiver extends BroadcastReceiver { private final SharedPreferences sharedPreferences; public WallpaperChangeReceiver(SharedPreferences sharedPreferences) { this.sharedPreferences = sharedPreferences; } @Override public void onReceive(Context context, Intent intent) { if (sharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_MATERIAL_YOU, false)) { OneTimeWorkRequest materialYouRequest = OneTimeWorkRequest.from(MaterialYouWorker.class); WorkManager.getInstance(context).enqueueUniqueWork(MaterialYouWorker.UNIQUE_WORKER_NAME, ExistingWorkPolicy.REPLACE, materialYouRequest); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/Comment.java ================================================ package ml.docilealligator.infinityforreddit.comment; import android.os.Parcel; import android.os.Parcelable; import java.util.ArrayList; import java.util.Map; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.utils.APIUtils; public class Comment implements Parcelable { public static final int VOTE_TYPE_NO_VOTE = 0; public static final int VOTE_TYPE_UPVOTE = 1; public static final int VOTE_TYPE_DOWNVOTE = -1; public static final int NOT_PLACEHOLDER = 0; public static final int PLACEHOLDER_LOAD_MORE_COMMENTS = 1; public static final int PLACEHOLDER_CONTINUE_THREAD = 2; public static final Creator CREATOR = new Creator<>() { @Override public Comment createFromParcel(Parcel in) { return new Comment(in); } @Override public Comment[] newArray(int size) { return new Comment[size]; } }; private String id; private String fullName; private String author; private String authorFullName; private String authorFlair; private String authorFlairHTML; private String authorIconUrl; private String linkAuthor; private long commentTimeMillis; private String commentMarkdown; private String commentRawText; private String linkId; private String subredditName; private String parentId; private int score; private int voteType; private boolean isSubmitter; private String distinguished; private String permalink; private int depth; private int childCount; private boolean collapsed; private boolean hasReply; private boolean scoreHidden; private boolean saved; private boolean sendReplies; private boolean locked; private boolean canModComment; private boolean approved; private long approvedAtUTC; private String approvedBy; private boolean removed; private boolean spam; private boolean isExpanded; private boolean hasExpandedBefore; private boolean isFilteredOut; private ArrayList children; private ArrayList moreChildrenIds; private int placeholderType; private boolean isLoadingMoreChildren; private boolean loadMoreChildrenFailed; private long editedTimeMillis; private Map mediaMetadataMap; public Comment(String id, String fullName, String author, String authorFullName, String authorFlair, String authorFlairHTML, String linkAuthor, long commentTimeMillis, String commentMarkdown, String commentRawText, String linkId, String subredditName, String parentId, int score, int voteType, boolean isSubmitter, String distinguished, String permalink, int depth, boolean collapsed, boolean hasReply, boolean scoreHidden, boolean saved, boolean sendReplies, boolean locked, boolean canModComment, boolean approved, long approvedAtUTC, String approvedBy, boolean removed, boolean spam, long edited, Map mediaMetadataMap) { this.id = id; this.fullName = fullName; this.author = author; this.authorFullName = authorFullName; this.authorFlair = authorFlair; this.authorFlairHTML = authorFlairHTML; this.linkAuthor = linkAuthor; this.commentTimeMillis = commentTimeMillis; this.commentMarkdown = commentMarkdown; this.commentRawText = commentRawText; this.linkId = linkId; this.subredditName = subredditName; this.parentId = parentId; this.score = score; this.voteType = voteType; this.isSubmitter = isSubmitter; this.distinguished = distinguished; this.permalink = APIUtils.API_BASE_URI + permalink; this.depth = depth; this.collapsed = collapsed; this.hasReply = hasReply; this.scoreHidden = scoreHidden; this.saved = saved; this.sendReplies = sendReplies; this.locked = locked; this.canModComment = canModComment; this.approved = approved; this.approvedAtUTC = approvedAtUTC; this.approvedBy = approvedBy; this.removed = removed; this.spam = spam; this.isExpanded = false; this.hasExpandedBefore = false; this.editedTimeMillis = edited; this.mediaMetadataMap = mediaMetadataMap; placeholderType = NOT_PLACEHOLDER; } public Comment(String parentFullName, int depth, int placeholderType) { if (placeholderType == PLACEHOLDER_LOAD_MORE_COMMENTS) { this.fullName = parentFullName; } else { this.fullName = parentFullName; this.parentId = parentFullName.substring(3); } this.depth = depth; this.placeholderType = placeholderType; isLoadingMoreChildren = false; loadMoreChildrenFailed = false; } public Comment(String parentFullName) { } protected Comment(Parcel in) { id = in.readString(); fullName = in.readString(); author = in.readString(); authorFullName = in.readString(); authorFlair = in.readString(); authorFlairHTML = in.readString(); authorIconUrl = in.readString(); linkAuthor = in.readString(); commentTimeMillis = in.readLong(); commentMarkdown = in.readString(); commentRawText = in.readString(); linkId = in.readString(); subredditName = in.readString(); parentId = in.readString(); score = in.readInt(); voteType = in.readInt(); isSubmitter = in.readByte() != 0; distinguished = in.readString(); permalink = in.readString(); depth = in.readInt(); childCount = in.readInt(); collapsed = in.readByte() != 0; hasReply = in.readByte() != 0; scoreHidden = in.readByte() != 0; saved = in.readByte() != 0; sendReplies = in.readByte() != 0; locked = in.readByte() != 0; canModComment = in.readByte() != 0; approved = in.readByte() != 0; approvedAtUTC = in.readLong(); approvedBy = in.readString(); removed = in.readByte() != 0; spam = in.readByte() != 0; isExpanded = in.readByte() != 0; hasExpandedBefore = in.readByte() != 0; editedTimeMillis = in.readLong(); isFilteredOut = in.readByte() != 0; children = new ArrayList<>(); in.readTypedList(children, Comment.CREATOR); moreChildrenIds = new ArrayList<>(); in.readStringList(moreChildrenIds); placeholderType = in.readInt(); isLoadingMoreChildren = in.readByte() != 0; loadMoreChildrenFailed = in.readByte() != 0; mediaMetadataMap = (Map) in.readValue(getClass().getClassLoader()); } public String getId() { return id; } public String getFullName() { return fullName; } public String getAuthor() { return author; } public boolean isAuthorDeleted() { return author != null && author.equals("[deleted]"); } public void setAuthor(String author) { this.author = author; } public String getAuthorFullName() { return authorFullName; } public String getAuthorFlair() { return authorFlair; } public String getAuthorFlairHTML() { return authorFlairHTML; } public String getAuthorIconUrl() { return authorIconUrl; } public void setAuthorIconUrl(String authorIconUrl) { this.authorIconUrl = authorIconUrl; } public String getLinkAuthor() { return linkAuthor; } public long getCommentTimeMillis() { return commentTimeMillis; } public String getCommentMarkdown() { return commentMarkdown; } public void setCommentMarkdown(String commentMarkdown) { this.commentMarkdown = commentMarkdown; } public String getCommentRawText() { return commentRawText; } public void setCommentRawText(String commentRawText) { this.commentRawText = commentRawText; } public String getLinkId() { return linkId; } public String getSubredditName() { return subredditName; } public String getParentId() { return parentId; } public void setParentId(String parentId) { this.parentId = parentId; } public int getScore() { return score; } public void setScore(int score) { this.score = score; } public boolean isSubmitter() { return isSubmitter; } public void setSubmittedByAuthor(boolean isSubmittedByAuthor) { this.isSubmitter = isSubmittedByAuthor; } public boolean isModerator() { return distinguished != null && distinguished.equals("moderator"); } public boolean isAdmin() { return distinguished != null && distinguished.equals("admin"); } public String getPermalink() { return permalink; } public int getDepth() { return depth; } public int getChildCount() { return childCount; } public void setChildCount(int childCount) { this.childCount = childCount; } public boolean isCollapsed() { return collapsed; } public boolean hasReply() { return hasReply; } public void setHasReply(boolean hasReply) { this.hasReply = hasReply; } public boolean isScoreHidden() { return scoreHidden; } public boolean isSaved() { return saved; } public void setSaved(boolean saved) { this.saved = saved; } public boolean isSendReplies() { return sendReplies; } public void toggleSendReplies() { sendReplies = !sendReplies; } public boolean isLocked() { return locked; } public void setLocked(boolean locked) { this.locked = locked; } public boolean isCanModComment() { return canModComment; } public boolean isApproved() { return approved; } public void setApproved(boolean approved) { this.approved = approved; } public long getApprovedAtUTC() { return approvedAtUTC; } public void setApprovedAtUTC(long approvedAtUTC) { this.approvedAtUTC = approvedAtUTC; } public String getApprovedBy() { return approvedBy; } public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; } public boolean isRemoved() { return removed; } public void setRemoved(boolean removed, boolean spam) { this.removed = removed; this.spam = spam; } public boolean isSpam() { return spam; } public boolean isExpanded() { return isExpanded; } public void setExpanded(boolean isExpanded) { this.isExpanded = isExpanded; if (isExpanded && !hasExpandedBefore) { hasExpandedBefore = true; } } public boolean hasExpandedBefore() { return hasExpandedBefore; } public boolean isFilteredOut() { return isFilteredOut; } public void setIsFilteredOut(boolean isFilteredOut) { this.isFilteredOut = isFilteredOut; } public int getVoteType() { return voteType; } public void setVoteType(int voteType) { this.voteType = voteType; } public ArrayList getChildren() { return children; } public void addChildren(ArrayList moreChildren) { if (children == null || children.size() == 0) { children = moreChildren; } else { if (children.size() > 1 && children.get(children.size() - 1).placeholderType == PLACEHOLDER_LOAD_MORE_COMMENTS) { children.addAll(children.size() - 2, moreChildren); } else { children.addAll(moreChildren); } } childCount += moreChildren == null ? 0 : moreChildren.size(); assertChildrenDepth(); } public void addChild(Comment comment) { addChild(comment, 0); childCount++; assertChildrenDepth(); } public void addChild(Comment comment, int position) { if (children == null) { children = new ArrayList<>(); } children.add(position, comment); assertChildrenDepth(); } private void assertChildrenDepth() { if (BuildConfig.DEBUG) { for (Comment child: children) { if (child.depth != depth + 1) { throw new IllegalStateException("Child depth is not one more than parent depth"); } } } } public ArrayList getMoreChildrenIds() { return moreChildrenIds; } public void setMoreChildrenIds(ArrayList moreChildrenIds) { this.moreChildrenIds = moreChildrenIds; } public boolean hasMoreChildrenIds() { return moreChildrenIds != null; } public void removeMoreChildrenIds() { moreChildrenIds.clear(); } public int getPlaceholderType() { return placeholderType; } public boolean isLoadingMoreChildren() { return isLoadingMoreChildren; } public void setLoadingMoreChildren(boolean isLoadingMoreChildren) { this.isLoadingMoreChildren = isLoadingMoreChildren; } public boolean isLoadMoreChildrenFailed() { return loadMoreChildrenFailed; } public void setLoadMoreChildrenFailed(boolean loadMoreChildrenFailed) { this.loadMoreChildrenFailed = loadMoreChildrenFailed; } public boolean isEdited() { return editedTimeMillis != 0; } public long getEditedTimeMillis() { return editedTimeMillis; } public Map getMediaMetadataMap() { return mediaMetadataMap; } public void setMediaMetadataMap(Map mediaMetadataMap) { this.mediaMetadataMap = mediaMetadataMap; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(id); parcel.writeString(fullName); parcel.writeString(author); parcel.writeString(authorFullName); parcel.writeString(authorFlair); parcel.writeString(authorFlairHTML); parcel.writeString(authorIconUrl); parcel.writeString(linkAuthor); parcel.writeLong(commentTimeMillis); parcel.writeString(commentMarkdown); parcel.writeString(commentRawText); parcel.writeString(linkId); parcel.writeString(subredditName); parcel.writeString(parentId); parcel.writeInt(score); parcel.writeInt(voteType); parcel.writeByte((byte) (isSubmitter ? 1 : 0)); parcel.writeString(distinguished); parcel.writeString(permalink); parcel.writeInt(depth); parcel.writeInt(childCount); parcel.writeByte((byte) (collapsed ? 1 : 0)); parcel.writeByte((byte) (hasReply ? 1 : 0)); parcel.writeByte((byte) (scoreHidden ? 1 : 0)); parcel.writeByte((byte) (saved ? 1 : 0)); parcel.writeByte((byte) (sendReplies ? 1 : 0)); parcel.writeByte((byte) (locked ? 1 : 0)); parcel.writeByte((byte) (canModComment ? 1 : 0)); parcel.writeByte((byte) (approved ? 1 : 0)); parcel.writeLong(approvedAtUTC); parcel.writeString(approvedBy); parcel.writeByte((byte) (removed ? 1 : 0)); parcel.writeByte((byte) (spam ? 1 : 0)); parcel.writeByte((byte) (isExpanded ? 1 : 0)); parcel.writeByte((byte) (hasExpandedBefore ? 1 : 0)); parcel.writeLong(editedTimeMillis); parcel.writeByte((byte) (isFilteredOut ? 1 : 0)); parcel.writeTypedList(children); parcel.writeStringList(moreChildrenIds); parcel.writeInt(placeholderType); parcel.writeByte((byte) (isLoadingMoreChildren ? 1 : 0)); parcel.writeByte((byte) (loadMoreChildrenFailed ? 1 : 0)); parcel.writeValue(mediaMetadataMap); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/CommentDataSource.java ================================================ package ml.docilealligator.infinityforreddit.comment; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.MutableLiveData; import androidx.paging.PageKeyedDataSource; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class CommentDataSource extends PageKeyedDataSource { private final Executor executor; private final Handler handler; private final Retrofit retrofit; @Nullable private final String accessToken; @NonNull private final String accountName; private final String username; private final SortType sortType; private final boolean areSavedComments; private final MutableLiveData paginationNetworkStateLiveData; private final MutableLiveData initialLoadStateLiveData; private final MutableLiveData hasPostLiveData; private LoadParams params; private LoadCallback callback; CommentDataSource(Executor executor, Handler handler, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, String username, SortType sortType, boolean areSavedComments) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.username = username; this.sortType = sortType; this.areSavedComments = areSavedComments; paginationNetworkStateLiveData = new MutableLiveData<>(); initialLoadStateLiveData = new MutableLiveData<>(); hasPostLiveData = new MutableLiveData<>(); } MutableLiveData getPaginationNetworkStateLiveData() { return paginationNetworkStateLiveData; } MutableLiveData getInitialLoadStateLiveData() { return initialLoadStateLiveData; } MutableLiveData hasPostLiveData() { return hasPostLiveData; } void retryLoadingMore() { loadAfter(params, callback); } @Override public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) { initialLoadStateLiveData.postValue(NetworkState.LOADING); RedditAPI api = retrofit.create(RedditAPI.class); Call commentsCall; if (areSavedComments) { commentsCall = api.getUserSavedCommentsOauth(username, PostPagingSource.USER_WHERE_SAVED, null, sortType.getType(), sortType.getTime(), APIUtils.getOAuthHeader(accessToken)); } else { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { commentsCall = api.getUserComments(username, null, sortType.getType(), sortType.getTime()); } else { commentsCall = api.getUserCommentsOauth(APIUtils.getOAuthHeader(accessToken), username, null, sortType.getType(), sortType.getTime()); } } commentsCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { parseComments(response.body(), new ParseCommentsAsyncTaskListener() { @Override public void parseSuccessful(ArrayList comments, String after) { handler.post(() -> { if (comments.isEmpty()) { hasPostLiveData.postValue(false); } else { hasPostLiveData.postValue(true); } if (after == null || after.isEmpty() || after.equals("null")) { callback.onResult(comments, null, null); } else { callback.onResult(comments, null, after); } initialLoadStateLiveData.postValue(NetworkState.LOADED); }); } @Override public void parseFailed() { handler.post(() -> { initialLoadStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error parsing data")); }); } }); }); } else { initialLoadStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error parsing data")); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { initialLoadStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error parsing data")); } }); } @Override public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) { } @Override public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) { this.params = params; this.callback = callback; paginationNetworkStateLiveData.postValue(NetworkState.LOADING); RedditAPI api = retrofit.create(RedditAPI.class); Call commentsCall; if (areSavedComments) { commentsCall = api.getUserSavedCommentsOauth(username, PostPagingSource.USER_WHERE_SAVED, params.key, sortType.getType(), sortType.getTime(), APIUtils.getOAuthHeader(accessToken)); } else { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { commentsCall = api.getUserComments(username, params.key, sortType.getType(), sortType.getTime()); } else { commentsCall = api.getUserCommentsOauth(APIUtils.getOAuthHeader(accessToken), username, params.key, sortType.getType(), sortType.getTime()); } } commentsCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { parseComments(response.body(), new ParseCommentsAsyncTaskListener() { @Override public void parseSuccessful(ArrayList comments, String after) { handler.post(() -> { if (after == null || after.isEmpty() || after.equals("null")) { callback.onResult(comments, null); } else { callback.onResult(comments, after); } paginationNetworkStateLiveData.postValue(NetworkState.LOADED); }); } @Override public void parseFailed() { handler.post(() -> paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error parsing data"))); } }); }); } else { paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetching data")); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetching data")); } }); } @WorkerThread private static void parseComments(String response, ParseCommentsAsyncTaskListener parseCommentsAsyncTaskListener) { try { JSONObject data = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY); JSONArray commentsJSONArray = data.getJSONArray(JSONUtils.CHILDREN_KEY); String after = data.getString(JSONUtils.AFTER_KEY); ArrayList comments = new ArrayList<>(); for (int i = 0; i < commentsJSONArray.length(); i++) { try { JSONObject commentJSON = commentsJSONArray.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); comments.add(ParseComment.parseSingleComment(commentJSON, 0)); } catch (JSONException e) { e.printStackTrace(); } } parseCommentsAsyncTaskListener.parseSuccessful(comments, after); } catch (JSONException e) { e.printStackTrace(); parseCommentsAsyncTaskListener.parseFailed(); } } interface ParseCommentsAsyncTaskListener { void parseSuccessful(ArrayList comments, String after); void parseFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/CommentDataSourceFactory.java ================================================ package ml.docilealligator.infinityforreddit.comment; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.MutableLiveData; import androidx.paging.DataSource; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Retrofit; class CommentDataSourceFactory extends DataSource.Factory { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final String accessToken; private final String accountName; private final String username; private SortType sortType; private final boolean areSavedComments; private CommentDataSource commentDataSource; private final MutableLiveData commentDataSourceLiveData; CommentDataSourceFactory(Executor executor, Handler handler, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, String username, SortType sortType, boolean areSavedComments) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.username = username; this.sortType = sortType; this.areSavedComments = areSavedComments; commentDataSourceLiveData = new MutableLiveData<>(); } @NonNull @Override public DataSource create() { commentDataSource = new CommentDataSource(executor, handler, retrofit, accessToken, accountName, username, sortType, areSavedComments); commentDataSourceLiveData.postValue(commentDataSource); return commentDataSource; } public MutableLiveData getCommentDataSourceLiveData() { return commentDataSourceLiveData; } CommentDataSource getCommentDataSource() { return commentDataSource; } void changeSortType(SortType sortType) { this.sortType = sortType; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/CommentDraft.kt ================================================ package ml.docilealligator.infinityforreddit.comment import androidx.room.ColumnInfo import androidx.room.Entity @Entity( tableName = "comment_draft", primaryKeys = ["full_name", "draft_type"] ) data class CommentDraft( @ColumnInfo(name = "full_name") var parentFullName: String, var content: String, @ColumnInfo(name = "last_updated") var lastUpdated: Long, @ColumnInfo(name = "draft_type") var draftType: DraftType ) enum class DraftType { REPLY, EDIT } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/CommentDraftDao.kt ================================================ package ml.docilealligator.infinityforreddit.comment import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @Dao interface CommentDraftDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(commentDraft: CommentDraft) @Delete suspend fun delete(commentDraft: CommentDraft) @Query("SELECT * FROM comment_draft WHERE full_name = :fullName AND draft_type = :draftType") fun getCommentDraftLiveData(fullName: String, draftType: DraftType): LiveData } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/CommentViewModel.java ================================================ package ml.docilealligator.infinityforreddit.comment; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.SingleLiveEvent; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.moderation.CommentModerationEvent; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class CommentViewModel extends ViewModel { private final Retrofit retrofit; private final String accessToken; private final String accountName; private final CommentDataSourceFactory commentDataSourceFactory; private final LiveData paginationNetworkState; private final LiveData initialLoadingState; private final LiveData hasCommentLiveData; private final LiveData> comments; private final MutableLiveData sortTypeLiveData; public final SingleLiveEvent commentModerationEventLiveData = new SingleLiveEvent<>(); public CommentViewModel(Executor executor, Handler handler, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, String username, SortType sortType, boolean areSavedComments) { this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; commentDataSourceFactory = new CommentDataSourceFactory(executor, handler, retrofit, accessToken, accountName, username, sortType, areSavedComments); initialLoadingState = Transformations.switchMap(commentDataSourceFactory.getCommentDataSourceLiveData(), CommentDataSource::getInitialLoadStateLiveData); paginationNetworkState = Transformations.switchMap(commentDataSourceFactory.getCommentDataSourceLiveData(), CommentDataSource::getPaginationNetworkStateLiveData); hasCommentLiveData = Transformations.switchMap(commentDataSourceFactory.getCommentDataSourceLiveData(), CommentDataSource::hasPostLiveData); sortTypeLiveData = new MutableLiveData<>(sortType); PagedList.Config pagedListConfig = (new PagedList.Config.Builder()) .setEnablePlaceholders(false) .setPageSize(100) .setPrefetchDistance(10) .setInitialLoadSizeHint(10) .build(); comments = Transformations.switchMap(sortTypeLiveData, sort -> { commentDataSourceFactory.changeSortType(sortTypeLiveData.getValue()); return (new LivePagedListBuilder(commentDataSourceFactory, pagedListConfig)).build(); }); } public LiveData> getComments() { return comments; } public LiveData getPaginationNetworkState() { return paginationNetworkState; } public LiveData getInitialLoadingState() { return initialLoadingState; } public LiveData hasComment() { return hasCommentLiveData; } public void refresh() { commentDataSourceFactory.getCommentDataSource().invalidate(); } public void retryLoadingMore() { commentDataSourceFactory.getCommentDataSource().retryLoadingMore(); } public void changeSortType(SortType sortType) { sortTypeLiveData.postValue(sortType); } public void approveComment(Comment comment, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, comment.getFullName()); retrofit.create(RedditAPI.class) .approveThing(APIUtils.getOAuthHeader(accessToken), params) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { List snapshot = comments.getValue(); if (snapshot != null) { if (position < snapshot.size() && position >= 0) { Comment moddedComment = snapshot.get(position); if (moddedComment != null) { moddedComment.setApproved(true); moddedComment.setApprovedAtUTC(System.currentTimeMillis()); moddedComment.setApprovedBy(accountName); moddedComment.setRemoved(false, false); } } } commentModerationEventLiveData.postValue( new CommentModerationEvent.Approved(comment, position) ); } else { commentModerationEventLiveData.postValue( new CommentModerationEvent.ApproveFailed(comment, position) ); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { commentModerationEventLiveData.postValue( new CommentModerationEvent.ApproveFailed(comment, position) ); } }); } public void removeComment(Comment comment, int position, boolean isSpam) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, comment.getFullName()); params.put(APIUtils.SPAM_KEY, Boolean.toString(isSpam)); retrofit.create(RedditAPI.class) .removeThing(APIUtils.getOAuthHeader(accessToken), params) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { List snapshot = comments.getValue(); if (snapshot != null) { if (position < snapshot.size() && position >= 0) { Comment moddedComment = snapshot.get(position); if (moddedComment != null) { moddedComment.setRemoved(true, isSpam); moddedComment.setApproved(false); moddedComment.setApprovedAtUTC(0); moddedComment.setApprovedBy(null); } } } commentModerationEventLiveData.postValue( isSpam ? new CommentModerationEvent.MarkedAsSpam(comment, position) : new CommentModerationEvent.Removed(comment, position) ); } else { commentModerationEventLiveData.postValue( isSpam ? new CommentModerationEvent.MarkAsSpamFailed(comment, position) : new CommentModerationEvent.RemoveFailed(comment, position) ); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { commentModerationEventLiveData.postValue( isSpam ? new CommentModerationEvent.MarkAsSpamFailed(comment, position) : new CommentModerationEvent.RemoveFailed(comment, position) ); } }); } public void toggleLock(Comment comment, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, comment.getFullName()); Call call; if (comment.isLocked()) { call = retrofit.create(RedditAPI.class) .unLockThing(APIUtils.getOAuthHeader(accessToken), params); } else { call = retrofit.create(RedditAPI.class) .lockThing(APIUtils.getOAuthHeader(accessToken), params); } call.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { commentModerationEventLiveData.postValue( comment.isLocked() ? new CommentModerationEvent.Unlocked(comment, position) : new CommentModerationEvent.Locked(comment, position) ); List snapshot = comments.getValue(); if (snapshot != null) { if (position < snapshot.size() && position >= 0) { Comment moddedComment = snapshot.get(position); if (moddedComment != null) { moddedComment.setLocked(!moddedComment.isLocked()); } } } } else { commentModerationEventLiveData.postValue( comment.isLocked() ? new CommentModerationEvent.UnlockFailed(comment, position) : new CommentModerationEvent.LockFailed(comment, position) ); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { commentModerationEventLiveData.postValue( comment.isLocked() ? new CommentModerationEvent.UnlockFailed(comment, position) : new CommentModerationEvent.LockFailed(comment, position) ); } }); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final String accessToken; private final String accountName; private final String username; private final SortType sortType; private final boolean areSavedComments; public Factory(Executor executor, Handler handler, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, String username, SortType sortType, boolean areSavedComments) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.username = username; this.sortType = sortType; this.areSavedComments = areSavedComments; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new CommentViewModel(executor, handler, retrofit, accessToken, accountName, username, sortType, areSavedComments); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/FetchComment.java ================================================ package ml.docilealligator.infinityforreddit.comment; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchComment { public static void fetchComments(Executor executor, Handler handler, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, String article, String commentId, SortType.Type sortType, String contextNumber, boolean expandChildren, CommentFilter commentFilter, FetchCommentListener fetchCommentListener) { RedditAPI api = retrofit.create(RedditAPI.class); Call comments; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (commentId == null) { comments = api.getPostAndCommentsById(article, sortType); } else { comments = api.getPostAndCommentsSingleThreadById(article, commentId, sortType, contextNumber); } } else { if (commentId == null) { comments = api.getPostAndCommentsByIdOauth(article, sortType, APIUtils.getOAuthHeader(accessToken)); } else { comments = api.getPostAndCommentsSingleThreadByIdOauth(article, commentId, sortType, contextNumber, APIUtils.getOAuthHeader(accessToken)); } } comments.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ParseComment.parseComment(executor, handler, response.body(), expandChildren, commentFilter, new ParseComment.ParseCommentListener() { @Override public void onParseCommentSuccess(ArrayList topLevelComments, ArrayList expandedComments, String parentId, ArrayList moreChildrenIds) { fetchCommentListener.onFetchCommentSuccess(expandedComments, parentId, moreChildrenIds); } @Override public void onParseCommentFailed() { fetchCommentListener.onFetchCommentFailed(); } }); } else { fetchCommentListener.onFetchCommentFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchCommentListener.onFetchCommentFailed(); } }); } public static void fetchMoreComment(Executor executor, Handler handler, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, ArrayList allChildren, boolean expandChildren, String postFullName, SortType.Type sortType, FetchMoreCommentListener fetchMoreCommentListener) { if (allChildren == null) { return; } String childrenIds = String.join(",", allChildren); if (childrenIds.isEmpty()) { return; } RedditAPI api = retrofit.create(RedditAPI.class); Call moreComments; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { moreComments = api.moreChildren(postFullName, childrenIds, sortType); } else { moreComments = api.moreChildrenOauth(postFullName, childrenIds, sortType, APIUtils.getOAuthHeader(accessToken)); } moreComments.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ParseComment.parseMoreComment(executor, handler, response.body(), expandChildren, new ParseComment.ParseCommentListener() { @Override public void onParseCommentSuccess(ArrayList topLevelComments, ArrayList expandedComments, String parentId, ArrayList moreChildrenIds) { fetchMoreCommentListener.onFetchMoreCommentSuccess( topLevelComments, expandedComments, moreChildrenIds); } @Override public void onParseCommentFailed() { fetchMoreCommentListener.onFetchMoreCommentFailed(); } }); } else { fetchMoreCommentListener.onFetchMoreCommentFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchMoreCommentListener.onFetchMoreCommentFailed(); } }); } public interface FetchCommentListener { void onFetchCommentSuccess(ArrayList expandedComments, String parentId, ArrayList children); void onFetchCommentFailed(); } public interface FetchMoreCommentListener { void onFetchMoreCommentSuccess(ArrayList topLevelComments, ArrayList expandedComments, ArrayList moreChildrenIds); void onFetchMoreCommentFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/ParseComment.java ================================================ package ml.docilealligator.infinityforreddit.comment; import static ml.docilealligator.infinityforreddit.comment.Comment.VOTE_TYPE_DOWNVOTE; import static ml.docilealligator.infinityforreddit.comment.Comment.VOTE_TYPE_NO_VOTE; import static ml.docilealligator.infinityforreddit.comment.Comment.VOTE_TYPE_UPVOTE; import android.os.Handler; import android.text.Html; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ParseComment { public static void parseComment(Executor executor, Handler handler, String response, boolean expandChildren, CommentFilter commentFilter, ParseCommentListener parseCommentListener) { executor.execute(() -> { try { JSONArray childrenArray = new JSONArray(response); String parentId = childrenArray.getJSONObject(0).getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY) .getJSONObject(0).getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.NAME_KEY); childrenArray = childrenArray.getJSONObject(1).getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); ArrayList expandedNewComments = new ArrayList<>(); ArrayList moreChildrenIds = new ArrayList<>(); ArrayList newComments = new ArrayList<>(); parseCommentRecursion(childrenArray, newComments, moreChildrenIds, 0, commentFilter); expandChildren(newComments, expandedNewComments, expandChildren); ArrayList commentData; if (expandChildren) { commentData = expandedNewComments; } else { commentData = newComments; } handler.post(() -> parseCommentListener.onParseCommentSuccess(newComments, commentData, parentId, moreChildrenIds)); } catch (JSONException e) { e.printStackTrace(); handler.post(parseCommentListener::onParseCommentFailed); } }); } static void parseMoreComment(Executor executor, Handler handler, String response, boolean expandChildren, ParseCommentListener parseCommentListener) { executor.execute(() -> { try { JSONArray childrenArray = new JSONObject(response).getJSONObject(JSONUtils.JSON_KEY) .getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.THINGS_KEY); ArrayList newComments = new ArrayList<>(); ArrayList expandedNewComments = new ArrayList<>(); ArrayList moreChildrenIds = new ArrayList<>(); // api response is a flat list of comments tree // process it in order and rebuild the tree for (int i = 0; i < childrenArray.length(); i++) { JSONObject child = childrenArray.getJSONObject(i); JSONObject childData = child.getJSONObject(JSONUtils.DATA_KEY); if (child.getString(JSONUtils.KIND_KEY).equals(JSONUtils.KIND_VALUE_MORE)) { String parentFullName = childData.getString(JSONUtils.PARENT_ID_KEY); JSONArray childrenIds = childData.getJSONArray(JSONUtils.CHILDREN_KEY); if (childrenIds.length() != 0) { ArrayList localMoreChildrenIds = new ArrayList<>(childrenIds.length()); for (int j = 0; j < childrenIds.length(); j++) { localMoreChildrenIds.add(childrenIds.getString(j)); } Comment parentComment = findCommentByFullName(newComments, parentFullName); if (parentComment != null) { parentComment.setHasReply(true); parentComment.setMoreChildrenIds(localMoreChildrenIds); parentComment.addChildren(new ArrayList<>()); // ensure children list is not null } else { // assume that it is parent of this call moreChildrenIds.addAll(localMoreChildrenIds); } } else { Comment continueThreadPlaceholder = new Comment( parentFullName, childData.getInt(JSONUtils.DEPTH_KEY), Comment.PLACEHOLDER_CONTINUE_THREAD ); Comment parentComment = findCommentByFullName(newComments, parentFullName); if (parentComment != null) { parentComment.setHasReply(true); parentComment.addChild(continueThreadPlaceholder, parentComment.getChildCount()); parentComment.setChildCount(parentComment.getChildCount() + 1); } else { // assume that it is parent of this call newComments.add(continueThreadPlaceholder); } } } else { try { Comment comment = parseSingleComment(childData, 0); String parentFullName = comment.getParentId(); Comment parentComment = findCommentByFullName(newComments, parentFullName); if (parentComment != null) { parentComment.setHasReply(true); parentComment.addChild(comment, parentComment.getChildCount()); parentComment.setChildCount(parentComment.getChildCount() + 1); } else { // assume that it is parent of this call newComments.add(comment); } } catch (JSONException e) { // Well we need to catch and ignore the exception to not show "error loading comments" to users e.printStackTrace(); } } } updateChildrenCount(newComments); expandChildren(newComments, expandedNewComments, expandChildren); ArrayList commentData; if (expandChildren) { commentData = expandedNewComments; } else { commentData = newComments; } handler.post(() -> parseCommentListener.onParseCommentSuccess(newComments, commentData, null, moreChildrenIds)); } catch (JSONException e) { e.printStackTrace(); handler.post(parseCommentListener::onParseCommentFailed); } }); } static void parseSentComment(Executor executor, Handler handler, String response, int depth, ParseSentCommentListener parseSentCommentListener) { executor.execute(() -> { try { JSONObject sentCommentData = new JSONObject(response); if (!sentCommentData.has(JSONUtils.ID_KEY) && sentCommentData.has(JSONUtils.JSON_KEY)) { sentCommentData = sentCommentData.getJSONObject(JSONUtils.JSON_KEY) .getJSONObject(JSONUtils.DATA_KEY) .getJSONArray(JSONUtils.THINGS_KEY) .getJSONObject(0) .getJSONObject(JSONUtils.DATA_KEY); } Comment comment = parseSingleComment(sentCommentData, depth); handler.post(() -> parseSentCommentListener.onParseSentCommentSuccess(comment)); } catch (JSONException e) { e.printStackTrace(); String errorMessage = parseSentCommentErrorMessage(response); handler.post(() -> parseSentCommentListener.onParseSentCommentFailed(errorMessage)); } }); } private static void parseCommentRecursion(JSONArray comments, ArrayList newCommentData, ArrayList moreChildrenIds, int depth, CommentFilter commentFilter) throws JSONException { int actualCommentLength; if (comments.length() == 0) { return; } JSONObject more = comments.getJSONObject(comments.length() - 1).getJSONObject(JSONUtils.DATA_KEY); //Maybe moreChildrenIds contain only commentsJSONArray and no more info if (more.has(JSONUtils.COUNT_KEY)) { JSONArray childrenArray = more.getJSONArray(JSONUtils.CHILDREN_KEY); for (int i = 0; i < childrenArray.length(); i++) { moreChildrenIds.add(childrenArray.getString(i)); } actualCommentLength = comments.length() - 1; if (moreChildrenIds.isEmpty() && comments.getJSONObject(comments.length() - 1).getString(JSONUtils.KIND_KEY).equals(JSONUtils.KIND_VALUE_MORE)) { newCommentData.add(new Comment(more.getString(JSONUtils.PARENT_ID_KEY), more.getInt(JSONUtils.DEPTH_KEY), Comment.PLACEHOLDER_CONTINUE_THREAD)); return; } } else { actualCommentLength = comments.length(); } for (int i = 0; i < actualCommentLength; i++) { JSONObject data = comments.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); Comment singleComment = parseSingleComment(data, depth); boolean isFilteredOut = false; if (!CommentFilter.isCommentAllowed(singleComment, commentFilter)) { if (commentFilter.displayMode == CommentFilter.DisplayMode.REMOVE_COMMENT) { continue; } isFilteredOut = true; } if (data.get(JSONUtils.REPLIES_KEY) instanceof JSONObject) { JSONArray childrenArray = data.getJSONObject(JSONUtils.REPLIES_KEY) .getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); ArrayList children = new ArrayList<>(); ArrayList nextMoreChildrenIds = new ArrayList<>(); parseCommentRecursion(childrenArray, children, nextMoreChildrenIds, singleComment.getDepth(), commentFilter); singleComment.addChildren(children); singleComment.setMoreChildrenIds(nextMoreChildrenIds); singleComment.setChildCount(getChildCount(singleComment)); } singleComment.setIsFilteredOut(isFilteredOut); newCommentData.add(singleComment); } } private static int getChildCount(Comment comment) { if (comment.getChildren() == null) { return 0; } int count = 0; for (Comment c : comment.getChildren()) { count += getChildCount(c); } return comment.getChildren().size() + count; } private static void expandChildren(ArrayList comments, ArrayList visibleComments, boolean setExpanded) { for (Comment c : comments) { visibleComments.add(c); if (!c.isFilteredOut()) { if (c.hasReply()) { if (setExpanded) { c.setExpanded(true); } expandChildren(c.getChildren(), visibleComments, setExpanded); } else { c.setExpanded(true); } } if (c.hasMoreChildrenIds() && !c.getMoreChildrenIds().isEmpty()) { //Add a load more placeholder Comment placeholder = new Comment(c.getFullName(), c.getDepth() + 1, Comment.PLACEHOLDER_LOAD_MORE_COMMENTS); if (!c.isFilteredOut()) { visibleComments.add(placeholder); } c.addChild(placeholder, c.getChildren().size()); } } } public static Comment parseSingleComment(JSONObject singleCommentData, int depth) throws JSONException { String id = singleCommentData.getString(JSONUtils.ID_KEY); String fullName = singleCommentData.getString(JSONUtils.NAME_KEY); String author = singleCommentData.getString(JSONUtils.AUTHOR_KEY); String authorFullname = ""; if (singleCommentData.has(JSONUtils.AUTHOR_FULLNAME_KEY)) { authorFullname = singleCommentData.getString(JSONUtils.AUTHOR_FULLNAME_KEY); } StringBuilder authorFlairHTMLBuilder = new StringBuilder(); if (singleCommentData.has(JSONUtils.AUTHOR_FLAIR_RICHTEXT_KEY)) { JSONArray flairArray = singleCommentData.getJSONArray(JSONUtils.AUTHOR_FLAIR_RICHTEXT_KEY); for (int i = 0; i < flairArray.length(); i++) { JSONObject flairObject = flairArray.getJSONObject(i); String e = flairObject.getString(JSONUtils.E_KEY); if (e.equals("text")) { authorFlairHTMLBuilder.append(Html.escapeHtml(flairObject.getString(JSONUtils.T_KEY))); } else if (e.equals("emoji")) { authorFlairHTMLBuilder.append(""); } } } String authorFlair = singleCommentData.isNull(JSONUtils.AUTHOR_FLAIR_TEXT_KEY) ? "" : singleCommentData.getString(JSONUtils.AUTHOR_FLAIR_TEXT_KEY); String linkAuthor = singleCommentData.has(JSONUtils.LINK_AUTHOR_KEY) ? singleCommentData.getString(JSONUtils.LINK_AUTHOR_KEY) : null; String linkId = singleCommentData.getString(JSONUtils.LINK_ID_KEY).substring(3); String subredditName = singleCommentData.getString(JSONUtils.SUBREDDIT_KEY); String parentId = singleCommentData.getString(JSONUtils.PARENT_ID_KEY); boolean isSubmitter = singleCommentData.getBoolean(JSONUtils.IS_SUBMITTER_KEY); String distinguished = singleCommentData.getString(JSONUtils.DISTINGUISHED_KEY); Map mediaMetadataMap = JSONUtils.parseMediaMetadata(singleCommentData); String commentMarkdown = ""; if (!singleCommentData.isNull(JSONUtils.BODY_KEY)) { commentMarkdown = Utils.parseRedditImagesBlock( Utils.modifyMarkdown( Utils.trimTrailingWhitespace(singleCommentData.getString(JSONUtils.BODY_KEY))), mediaMetadataMap); } String commentRawText = Utils.trimTrailingWhitespace( Html.fromHtml(singleCommentData.getString(JSONUtils.BODY_HTML_KEY))).toString(); String permalink = Html.fromHtml(singleCommentData.getString(JSONUtils.PERMALINK_KEY)).toString(); int score = singleCommentData.getInt(JSONUtils.SCORE_KEY); int voteType; if (singleCommentData.isNull(JSONUtils.LIKES_KEY)) { voteType = VOTE_TYPE_NO_VOTE; } else { voteType = singleCommentData.getBoolean(JSONUtils.LIKES_KEY) ? VOTE_TYPE_UPVOTE : VOTE_TYPE_DOWNVOTE; score -= voteType; } long submitTime = singleCommentData.getLong(JSONUtils.CREATED_UTC_KEY) * 1000; boolean scoreHidden = singleCommentData.getBoolean(JSONUtils.SCORE_HIDDEN_KEY); boolean saved = singleCommentData.getBoolean(JSONUtils.SAVED_KEY); boolean sendReplies = singleCommentData.getBoolean(JSONUtils.SEND_REPLIES_KEY); boolean locked = singleCommentData.getBoolean(JSONUtils.LOCKED_KEY); boolean canModComment = singleCommentData.getBoolean(JSONUtils.CAN_MOD_POST_KEY); boolean approved = singleCommentData.has(JSONUtils.APPROVED_KEY) && singleCommentData.getBoolean(JSONUtils.APPROVED_KEY); long approvedAtUTC = singleCommentData.has(JSONUtils.APPROVED_AT_UTC_KEY) ? (singleCommentData.isNull(JSONUtils.APPROVED_AT_UTC_KEY) ? 0 : singleCommentData.getLong(JSONUtils.APPROVED_AT_UTC_KEY) * 1000) : 0; String approvedBy = singleCommentData.has(JSONUtils.APPROVED_BY_KEY) ? singleCommentData.getString(JSONUtils.APPROVED_BY_KEY) : null; boolean removed = singleCommentData.has(JSONUtils.REMOVED_KEY) && singleCommentData.getBoolean(JSONUtils.REMOVED_KEY); boolean spam = singleCommentData.has(JSONUtils.SPAM_KEY) && singleCommentData.getBoolean(JSONUtils.SPAM_KEY); if (singleCommentData.has(JSONUtils.DEPTH_KEY)) { depth = singleCommentData.getInt(JSONUtils.DEPTH_KEY); } boolean collapsed = singleCommentData.getBoolean(JSONUtils.COLLAPSED_KEY); boolean hasReply = !(singleCommentData.get(JSONUtils.REPLIES_KEY) instanceof String); // this key can either be a bool (false) or a long (edited timestamp) long edited = singleCommentData.optLong(JSONUtils.EDITED_KEY) * 1000; return new Comment(id, fullName, author, authorFullname, authorFlair, authorFlairHTMLBuilder.toString(), linkAuthor, submitTime, commentMarkdown, commentRawText, linkId, subredditName, parentId, score, voteType, isSubmitter, distinguished, permalink, depth, collapsed, hasReply, scoreHidden, saved, sendReplies, locked, canModComment, approved, approvedAtUTC, approvedBy, removed, spam, edited, mediaMetadataMap); } @Nullable private static String parseSentCommentErrorMessage(String response) { try { JSONObject responseObject = new JSONObject(response).getJSONObject(JSONUtils.JSON_KEY); if (responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() != 0) { JSONArray error = responseObject.getJSONArray(JSONUtils.ERRORS_KEY) .getJSONArray(responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() - 1); if (error.length() != 0) { String errorString; if (error.length() >= 2) { errorString = error.getString(1); } else { errorString = error.getString(0); } return errorString.substring(0, 1).toUpperCase() + errorString.substring(1); } else { return null; } } else { return null; } } catch (JSONException e) { e.printStackTrace(); } return null; } @Nullable private static Comment findCommentByFullName(@NonNull List comments, @NonNull String fullName) { for (Comment comment: comments) { if (comment.getFullName().equals(fullName) && comment.getPlaceholderType() == Comment.NOT_PLACEHOLDER) { return comment; } if (comment.getChildren() != null) { Comment result = findCommentByFullName(comment.getChildren(), fullName); if (result != null) { return result; } } } return null; } private static void updateChildrenCount(@NonNull List comments) { for (Comment comment: comments) { comment.setChildCount(getChildCount(comment)); if (comment.getChildren() != null) { updateChildrenCount(comment.getChildren()); } } } public interface ParseCommentListener { void onParseCommentSuccess(ArrayList topLevelComments, ArrayList expandedComments, String parentId, ArrayList moreChildrenIds); void onParseCommentFailed(); } interface ParseSentCommentListener { void onParseSentCommentSuccess(Comment comment); void onParseSentCommentFailed(@Nullable String errorMessage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/comment/SendComment.java ================================================ package ml.docilealligator.infinityforreddit.comment; import android.content.Context; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.GiphyGif; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.markdown.RichTextJSONConverter; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class SendComment { public static void sendComment(Context context, Executor executor, Handler handler, String commentMarkdown, String thingFullname, int parentDepth, List uploadedImages, @Nullable GiphyGif giphyGif, Retrofit newAuthenticatorOauthRetrofit, Account account, SendCommentListener sendCommentListener) { Map headers = APIUtils.getOAuthHeader(account.getAccessToken()); Map params = new HashMap<>(); params.put(APIUtils.API_TYPE_KEY, "json"); params.put(APIUtils.RETURN_RTJSON_KEY, "true"); if (!uploadedImages.isEmpty() || giphyGif != null) { try { params.put(APIUtils.RICHTEXT_JSON_KEY, new RichTextJSONConverter().constructRichTextJSON( context, commentMarkdown, uploadedImages, giphyGif)); params.put(APIUtils.TEXT_KEY, ""); } catch (JSONException e) { sendCommentListener.sendCommentFailed(context.getString(R.string.convert_to_richtext_json_failed)); return; } } else { params.put(APIUtils.TEXT_KEY, commentMarkdown); } params.put(APIUtils.THING_ID_KEY, thingFullname); newAuthenticatorOauthRetrofit.create(RedditAPI.class).sendCommentOrReplyToMessage(headers, params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ParseComment.parseSentComment(executor, handler, response.body(), parentDepth, new ParseComment.ParseSentCommentListener() { @Override public void onParseSentCommentSuccess(Comment comment) { sendCommentListener.sendCommentSuccess(comment); } @Override public void onParseSentCommentFailed(@Nullable String errorMessage) { sendCommentListener.sendCommentFailed(errorMessage); } }); } else { sendCommentListener.sendCommentFailed(response.message()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { sendCommentListener.sendCommentFailed(t.getMessage()); } }); } public interface SendCommentListener { void sendCommentSuccess(Comment comment); void sendCommentFailed(String errorMessage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/CommentFilter.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import ml.docilealligator.infinityforreddit.comment.Comment; @Entity(tableName = "comment_filter") public class CommentFilter implements Parcelable { @PrimaryKey @NonNull public String name = "New Filter"; @DisplayMode @ColumnInfo(name = "display_mode") public int displayMode; @ColumnInfo(name = "max_vote") public int maxVote = -1; @ColumnInfo(name = "min_vote") public int minVote = -1; @ColumnInfo(name = "exclude_strings") public String excludeStrings; @ColumnInfo(name = "exclude_users") public String excludeUsers; public CommentFilter() { } protected CommentFilter(Parcel in) { name = in.readString(); displayMode = in.readInt(); maxVote = in.readInt(); minVote = in.readInt(); excludeStrings = in.readString(); excludeUsers = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public CommentFilter createFromParcel(Parcel in) { return new CommentFilter(in); } @Override public CommentFilter[] newArray(int size) { return new CommentFilter[size]; } }; public static boolean isCommentAllowed(Comment comment, CommentFilter commentFilter) { if (commentFilter.maxVote > 0 && comment.getVoteType() + comment.getScore() > commentFilter.maxVote) { return false; } if (commentFilter.minVote > 0 && comment.getVoteType() + comment.getScore() < commentFilter.minVote) { return false; } if (commentFilter.excludeStrings != null && !commentFilter.excludeStrings.equals("")) { String[] titles = commentFilter.excludeStrings.split(",", 0); for (String t : titles) { if (!t.trim().equals("") && comment.getCommentRawText().toLowerCase().contains(t.toLowerCase().trim())) { return false; } } } if (commentFilter.excludeUsers != null && !commentFilter.excludeUsers.equals("")) { String[] users = commentFilter.excludeUsers.split(",", 0); for (String u : users) { if (!u.trim().equals("") && comment.getAuthor().equalsIgnoreCase(u.trim())) { return false; } } } return true; } public static CommentFilter mergeCommentFilter(List commentFilterList) { if (commentFilterList.size() == 1) { return commentFilterList.get(0); } CommentFilter commentFilter = new CommentFilter(); StringBuilder stringBuilder; commentFilter.name = "Merged"; for (CommentFilter c : commentFilterList) { commentFilter.displayMode = Math.max(c.displayMode, commentFilter.displayMode); commentFilter.maxVote = Math.min(c.maxVote, commentFilter.maxVote); commentFilter.minVote = Math.max(c.minVote, commentFilter.minVote); if (c.excludeStrings != null && !c.excludeStrings.isEmpty()) { stringBuilder = new StringBuilder(commentFilter.excludeStrings == null ? "" : commentFilter.excludeStrings); stringBuilder.append(",").append(c.excludeStrings); commentFilter.excludeStrings = stringBuilder.toString(); } if (c.excludeUsers != null && !c.excludeUsers.isEmpty()) { stringBuilder = new StringBuilder(commentFilter.excludeUsers == null ? "" : commentFilter.excludeUsers); stringBuilder.append(",").append(c.excludeUsers); commentFilter.excludeUsers = stringBuilder.toString(); } } return commentFilter; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(name); dest.writeInt(displayMode); dest.writeInt(maxVote); dest.writeInt(minVote); dest.writeString(excludeStrings); dest.writeString(excludeUsers); } @IntDef({DisplayMode.REMOVE_COMMENT, DisplayMode.COLLAPSE_COMMENT}) @Retention(RetentionPolicy.SOURCE) public @interface DisplayMode { int REMOVE_COMMENT = 0; int COLLAPSE_COMMENT = 10; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/CommentFilterDao.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; 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.Transaction; import java.util.List; @Dao public interface CommentFilterDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(CommentFilter CommentFilter); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List CommentFilters); @Query("DELETE FROM comment_filter") void deleteAllCommentFilters(); @Delete void deleteCommentFilter(CommentFilter CommentFilter); @Query("DELETE FROM comment_filter WHERE name = :name") void deleteCommentFilter(String name); @Query("SELECT * FROM comment_filter WHERE name = :name LIMIT 1") CommentFilter getCommentFilter(String name); @Query("SELECT * FROM comment_filter ORDER BY name") LiveData> getAllCommentFiltersLiveData(); @Query("SELECT * FROM comment_filter") List getAllCommentFilters(); @Query("SELECT * FROM comment_filter WHERE (comment_filter.name IN " + "(SELECT comment_filter_usage.name FROM comment_filter_usage WHERE (usage = :usage AND name_of_usage = :nameOfUsage COLLATE NOCASE)))" + " OR (comment_filter.name NOT IN (SELECT comment_filter_usage.name FROM comment_filter_usage))") List getValidCommentFilters(int usage, String nameOfUsage); @Transaction @Query("SELECT * FROM comment_filter ORDER BY name") LiveData> getAllCommentFilterWithUsageLiveData(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/CommentFilterUsage.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; @Entity(tableName = "comment_filter_usage", primaryKeys = {"name", "usage", "name_of_usage"}, foreignKeys = @ForeignKey(entity = CommentFilter.class, parentColumns = "name", childColumns = "name", onDelete = ForeignKey.CASCADE)) public class CommentFilterUsage implements Parcelable { public static final int SUBREDDIT_TYPE = 1; @NonNull @ColumnInfo(name = "name") public String name; @ColumnInfo(name = "usage") public int usage; @NonNull @ColumnInfo(name = "name_of_usage") public String nameOfUsage; public CommentFilterUsage(@NonNull String name, int usage, @NonNull String nameOfUsage) { this.name = name; this.usage = usage; this.nameOfUsage = nameOfUsage; } protected CommentFilterUsage(Parcel in) { name = in.readString(); usage = in.readInt(); nameOfUsage = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public CommentFilterUsage createFromParcel(Parcel in) { return new CommentFilterUsage(in); } @Override public CommentFilterUsage[] newArray(int size) { return new CommentFilterUsage[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(name); dest.writeInt(usage); dest.writeString(nameOfUsage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/CommentFilterUsageDao.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; 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 java.util.List; @Dao public interface CommentFilterUsageDao { @Query("SELECT * FROM comment_filter_usage WHERE name = :name") LiveData> getAllCommentFilterUsageLiveData(String name); @Query("SELECT * FROM comment_filter_usage WHERE name = :name") List getAllCommentFilterUsage(String name); @Query("SELECT * FROM comment_filter_usage") List getAllCommentFilterUsageForBackup(); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(CommentFilterUsage CommentFilterUsage); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List CommentFilterUsageList); @Delete void deleteCommentFilterUsage(CommentFilterUsage CommentFilterUsage); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/CommentFilterUsageViewModel.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class CommentFilterUsageViewModel extends ViewModel { private final LiveData> mCommentFilterUsageListLiveData; public CommentFilterUsageViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String name) { mCommentFilterUsageListLiveData = redditDataRoomDatabase.commentFilterUsageDao().getAllCommentFilterUsageLiveData(name); } public LiveData> getCommentFilterUsageListLiveData() { return mCommentFilterUsageListLiveData; } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mName; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String name) { mRedditDataRoomDatabase = redditDataRoomDatabase; mName = name; } @NonNull @Override public T create(@NonNull Class modelClass) { //noinspection unchecked return (T) new CommentFilterUsageViewModel(mRedditDataRoomDatabase, mName); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/CommentFilterWithUsage.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import androidx.room.Embedded; import androidx.room.Relation; import java.util.List; public class CommentFilterWithUsage { @Embedded public CommentFilter commentFilter; @Relation( parentColumn = "name", entityColumn = "name" ) public List commentFilterUsageList; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/CommentFilterWithUsageViewModel.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class CommentFilterWithUsageViewModel extends ViewModel { private final LiveData> mCommentFilterWithUsageListLiveData; public CommentFilterWithUsageViewModel(RedditDataRoomDatabase redditDataRoomDatabase) { mCommentFilterWithUsageListLiveData = redditDataRoomDatabase.commentFilterDao().getAllCommentFilterWithUsageLiveData(); } public LiveData> getCommentFilterWithUsageListLiveData() { return mCommentFilterWithUsageListLiveData; } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; public Factory(RedditDataRoomDatabase redditDataRoomDatabase) { mRedditDataRoomDatabase = redditDataRoomDatabase; } @NonNull @Override public T create(@NonNull Class modelClass) { //noinspection unchecked return (T) new CommentFilterWithUsageViewModel(mRedditDataRoomDatabase); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/DeleteCommentFilter.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeleteCommentFilter { public static void deleteCommentFilter(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, CommentFilter commentFilter) { executor.execute(() -> redditDataRoomDatabase.commentFilterDao().deleteCommentFilter(commentFilter)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/DeleteCommentFilterUsage.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeleteCommentFilterUsage { public static void deleteCommentFilterUsage(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, CommentFilterUsage commentFilterUsage) { executor.execute(() -> redditDataRoomDatabase.commentFilterUsageDao().deleteCommentFilterUsage(commentFilterUsage)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/FetchCommentFilter.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import android.os.Handler; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class FetchCommentFilter { public static void fetchCommentFilter(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String subreddit, FetchCommentFilterListener fetchCommentFilterListener) { executor.execute(() -> { List commentFilterList = redditDataRoomDatabase.commentFilterDao().getValidCommentFilters(CommentFilterUsage.SUBREDDIT_TYPE, subreddit); CommentFilter commentFilter = CommentFilter.mergeCommentFilter(commentFilterList); handler.post(() -> fetchCommentFilterListener.success(commentFilter)); }); } public interface FetchCommentFilterListener { void success(CommentFilter commentFilter); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/SaveCommentFilter.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import android.os.Handler; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SaveCommentFilter { public interface SaveCommentFilterListener { void success(); void duplicate(); } public static void saveCommentFilter(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, CommentFilter commentFilter, String originalName, SaveCommentFilter.SaveCommentFilterListener saveCommentFilterListener) { executor.execute(() -> { if (!originalName.equals(commentFilter.name) && redditDataRoomDatabase.commentFilterDao().getCommentFilter(commentFilter.name) != null) { handler.post(saveCommentFilterListener::duplicate); } else { List commentFilterUsages = redditDataRoomDatabase.commentFilterUsageDao().getAllCommentFilterUsage(originalName); if (!originalName.equals(commentFilter.name)) { redditDataRoomDatabase.commentFilterDao().deleteCommentFilter(originalName); } redditDataRoomDatabase.commentFilterDao().insert(commentFilter); for (CommentFilterUsage commentFilterUsage : commentFilterUsages) { commentFilterUsage.name = commentFilter.name; redditDataRoomDatabase.commentFilterUsageDao().insert(commentFilterUsage); } handler.post(saveCommentFilterListener::success); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/commentfilter/SaveCommentFilterUsage.java ================================================ package ml.docilealligator.infinityforreddit.commentfilter; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SaveCommentFilterUsage { public static void saveCommentFilterUsage(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, CommentFilterUsage commentFilterUsage) { executor.execute(() -> redditDataRoomDatabase.commentFilterUsageDao().insert(commentFilterUsage)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/CustomTheme.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import android.graphics.Color; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Map; @Entity(tableName = "custom_themes") public class CustomTheme implements Parcelable { @PrimaryKey @NonNull @ColumnInfo(name = "name") public String name; @ColumnInfo(name = "is_light_theme") public boolean isLightTheme; @ColumnInfo(name = "is_dark_theme") public boolean isDarkTheme; @ColumnInfo(name = "is_amoled_theme") public boolean isAmoledTheme; @ColumnInfo(name = "color_primary") public int colorPrimary; @ColumnInfo(name = "color_primary_dark") public int colorPrimaryDark; @ColumnInfo(name = "color_accent") public int colorAccent; @ColumnInfo(name = "color_primary_light_theme") public int colorPrimaryLightTheme; @ColumnInfo(name = "primary_text_color") public int primaryTextColor; @ColumnInfo(name = "secondary_text_color") public int secondaryTextColor; @ColumnInfo(name = "post_title_color") public int postTitleColor; @ColumnInfo(name = "post_content_color") public int postContentColor; @ColumnInfo(name = "read_post_title_color") public int readPostTitleColor; @ColumnInfo(name = "read_post_content_color") public int readPostContentColor; @ColumnInfo(name = "comment_color") public int commentColor; @ColumnInfo(name = "button_text_color") public int buttonTextColor; @ColumnInfo(name = "background_color") public int backgroundColor; @ColumnInfo(name = "card_view_background_color") public int cardViewBackgroundColor; @ColumnInfo(name = "read_post_card_view_background_color") public int readPostCardViewBackgroundColor; @ColumnInfo(name = "filled_card_view_background_color") public int filledCardViewBackgroundColor; @ColumnInfo(name = "read_post_filled_card_view_background_color") public int readPostFilledCardViewBackgroundColor; @ColumnInfo(name = "comment_background_color") public int commentBackgroundColor; @ColumnInfo(name = "bottom_app_bar_background_color") public int bottomAppBarBackgroundColor; @ColumnInfo(name = "primary_icon_color") public int primaryIconColor; @ColumnInfo(name = "bottom_app_bar_icon_color") public int bottomAppBarIconColor; @ColumnInfo(name = "post_icon_and_info_color") public int postIconAndInfoColor; @ColumnInfo(name = "comment_icon_and_info_color") public int commentIconAndInfoColor; @ColumnInfo(name = "toolbar_primary_text_and_icon_color") public int toolbarPrimaryTextAndIconColor; @ColumnInfo(name = "toolbar_secondary_text_color") public int toolbarSecondaryTextColor; @ColumnInfo(name = "circular_progress_bar_background") public int circularProgressBarBackground; @ColumnInfo(name = "media_indicator_icon_color") public int mediaIndicatorIconColor; @ColumnInfo(name = "media_indicator_background_color") public int mediaIndicatorBackgroundColor; @ColumnInfo(name = "tab_layout_with_expanded_collapsing_toolbar_tab_background") public int tabLayoutWithExpandedCollapsingToolbarTabBackground; @ColumnInfo(name = "tab_layout_with_expanded_collapsing_toolbar_text_color") public int tabLayoutWithExpandedCollapsingToolbarTextColor; @ColumnInfo(name = "tab_layout_with_expanded_collapsing_toolbar_tab_indicator") public int tabLayoutWithExpandedCollapsingToolbarTabIndicator; @ColumnInfo(name = "tab_layout_with_collapsed_collapsing_toolbar_tab_background") public int tabLayoutWithCollapsedCollapsingToolbarTabBackground; @ColumnInfo(name = "tab_layout_with_collapsed_collapsing_toolbar_text_color") public int tabLayoutWithCollapsedCollapsingToolbarTextColor; @ColumnInfo(name = "tab_layout_with_collapsed_collapsing_toolbar_tab_indicator") public int tabLayoutWithCollapsedCollapsingToolbarTabIndicator; @ColumnInfo(name = "nav_bar_color") public int navBarColor; @ColumnInfo(name = "upvoted") public int upvoted; @ColumnInfo(name = "downvoted") public int downvoted; @ColumnInfo(name = "post_type_background_color") public int postTypeBackgroundColor; @ColumnInfo(name = "post_type_text_color") public int postTypeTextColor; @ColumnInfo(name = "spoiler_background_color") public int spoilerBackgroundColor; @ColumnInfo(name = "spoiler_text_color") public int spoilerTextColor; @ColumnInfo(name = "nsfw_background_color") public int nsfwBackgroundColor; @ColumnInfo(name = "nsfw_text_color") public int nsfwTextColor; @ColumnInfo(name = "flair_background_color") public int flairBackgroundColor; @ColumnInfo(name = "flair_text_color") public int flairTextColor; @ColumnInfo(name = "awards_background_color") public int awardsBackgroundColor; @ColumnInfo(name = "awards_text_color") public int awardsTextColor; @ColumnInfo(name = "archived_tint") public int archivedTint; @ColumnInfo(name = "locked_icon_tint") public int lockedIconTint; @ColumnInfo(name = "crosspost_icon_tint") public int crosspostIconTint; @ColumnInfo(name = "upvote_ratio_icon_tint") public int upvoteRatioIconTint; @ColumnInfo(name = "stickied_post_icon_tint") public int stickiedPostIconTint; @ColumnInfo(name = "no_preview_post_type_icon_tint") public int noPreviewPostTypeIconTint; @ColumnInfo(name = "subscribed") public int subscribed; @ColumnInfo(name = "unsubscribed") public int unsubscribed; @ColumnInfo(name = "username") public int username; @ColumnInfo(name = "subreddit") public int subreddit; @ColumnInfo(name = "author_flair_text_color") public int authorFlairTextColor; @ColumnInfo(name = "submitter") public int submitter; @ColumnInfo(name = "moderator") public int moderator; @ColumnInfo(name = "current_user") public int currentUser; @ColumnInfo(name = "single_comment_thread_background_color") public int singleCommentThreadBackgroundColor; @ColumnInfo(name = "unread_message_background_color") public int unreadMessageBackgroundColor; @ColumnInfo(name = "divider_color") public int dividerColor; @ColumnInfo(name = "no_preview_link_background_color") public int noPreviewPostTypeBackgroundColor; @ColumnInfo(name = "vote_and_reply_unavailable_button_color") public int voteAndReplyUnavailableButtonColor; @ColumnInfo(name = "comment_vertical_bar_color_1") public int commentVerticalBarColor1; @ColumnInfo(name = "comment_vertical_bar_color_2") public int commentVerticalBarColor2; @ColumnInfo(name = "comment_vertical_bar_color_3") public int commentVerticalBarColor3; @ColumnInfo(name = "comment_vertical_bar_color_4") public int commentVerticalBarColor4; @ColumnInfo(name = "comment_vertical_bar_color_5") public int commentVerticalBarColor5; @ColumnInfo(name = "comment_vertical_bar_color_6") public int commentVerticalBarColor6; @ColumnInfo(name = "comment_vertical_bar_color_7") public int commentVerticalBarColor7; @ColumnInfo(name = "fab_icon_color") public int fabIconColor; @ColumnInfo(name = "chip_text_color") public int chipTextColor; @ColumnInfo(name = "link_color") public int linkColor; @ColumnInfo(name = "received_message_text_color") public int receivedMessageTextColor; @ColumnInfo(name = "sent_message_text_color") public int sentMessageTextColor; @ColumnInfo(name = "received_message_background_color") public int receivedMessageBackgroundColor; @ColumnInfo(name = "sent_message_background_color") public int sentMessageBackgroundColor; @ColumnInfo(name = "send_message_icon_color") public int sendMessageIconColor; @ColumnInfo(name = "fully_collapsed_comment_background_color") public int fullyCollapsedCommentBackgroundColor; @ColumnInfo(name = "awarded_comment_background_color") public int awardedCommentBackgroundColor; @ColumnInfo(name = "is_light_status_bar") public boolean isLightStatusBar; @ColumnInfo(name = "is_light_nav_bar") public boolean isLightNavBar; @ColumnInfo(name = "is_change_status_bar_icon_color_after_toolbar_collapsed_in_immersive_interface") public boolean isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface; public CustomTheme() {} public CustomTheme(@NonNull String name) { this.name = name; } protected CustomTheme(Parcel in) { name = in.readString(); isLightTheme = in.readByte() != 0; isDarkTheme = in.readByte() != 0; isAmoledTheme = in.readByte() != 0; colorPrimary = in.readInt(); colorPrimaryDark = in.readInt(); colorAccent = in.readInt(); colorPrimaryLightTheme = in.readInt(); primaryTextColor = in.readInt(); secondaryTextColor = in.readInt(); postTitleColor = in.readInt(); postContentColor = in.readInt(); readPostTitleColor = in.readInt(); readPostContentColor = in.readInt(); commentColor = in.readInt(); buttonTextColor = in.readInt(); backgroundColor = in.readInt(); cardViewBackgroundColor = in.readInt(); readPostCardViewBackgroundColor = in.readInt(); filledCardViewBackgroundColor = in.readInt(); readPostFilledCardViewBackgroundColor = in.readInt(); commentBackgroundColor = in.readInt(); bottomAppBarBackgroundColor = in.readInt(); primaryIconColor = in.readInt(); bottomAppBarIconColor = in.readInt(); postIconAndInfoColor = in.readInt(); commentIconAndInfoColor = in.readInt(); toolbarPrimaryTextAndIconColor = in.readInt(); toolbarSecondaryTextColor = in.readInt(); circularProgressBarBackground = in.readInt(); mediaIndicatorIconColor = in.readInt(); mediaIndicatorBackgroundColor = in.readInt(); tabLayoutWithExpandedCollapsingToolbarTabBackground = in.readInt(); tabLayoutWithExpandedCollapsingToolbarTextColor = in.readInt(); tabLayoutWithExpandedCollapsingToolbarTabIndicator = in.readInt(); tabLayoutWithCollapsedCollapsingToolbarTabBackground = in.readInt(); tabLayoutWithCollapsedCollapsingToolbarTextColor = in.readInt(); tabLayoutWithCollapsedCollapsingToolbarTabIndicator = in.readInt(); navBarColor = in.readInt(); upvoted = in.readInt(); downvoted = in.readInt(); postTypeBackgroundColor = in.readInt(); postTypeTextColor = in.readInt(); spoilerBackgroundColor = in.readInt(); spoilerTextColor = in.readInt(); nsfwBackgroundColor = in.readInt(); nsfwTextColor = in.readInt(); flairBackgroundColor = in.readInt(); flairTextColor = in.readInt(); awardsBackgroundColor = in.readInt(); awardsTextColor = in.readInt(); archivedTint = in.readInt(); lockedIconTint = in.readInt(); crosspostIconTint = in.readInt(); upvoteRatioIconTint = in.readInt(); stickiedPostIconTint = in.readInt(); noPreviewPostTypeIconTint = in.readInt(); subscribed = in.readInt(); unsubscribed = in.readInt(); username = in.readInt(); subreddit = in.readInt(); authorFlairTextColor = in.readInt(); submitter = in.readInt(); moderator = in.readInt(); currentUser = in.readInt(); singleCommentThreadBackgroundColor = in.readInt(); unreadMessageBackgroundColor = in.readInt(); dividerColor = in.readInt(); noPreviewPostTypeBackgroundColor = in.readInt(); voteAndReplyUnavailableButtonColor = in.readInt(); commentVerticalBarColor1 = in.readInt(); commentVerticalBarColor2 = in.readInt(); commentVerticalBarColor3 = in.readInt(); commentVerticalBarColor4 = in.readInt(); commentVerticalBarColor5 = in.readInt(); commentVerticalBarColor6 = in.readInt(); commentVerticalBarColor7 = in.readInt(); fabIconColor = in.readInt(); chipTextColor = in.readInt(); linkColor = in.readInt(); receivedMessageTextColor = in.readInt(); sentMessageTextColor = in.readInt(); receivedMessageBackgroundColor = in.readInt(); sentMessageBackgroundColor = in.readInt(); sendMessageIconColor = in.readInt(); fullyCollapsedCommentBackgroundColor = in.readInt(); awardedCommentBackgroundColor = in.readInt(); isLightStatusBar = in.readByte() != 0; isLightNavBar = in.readByte() != 0; isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public CustomTheme createFromParcel(Parcel in) { return new CustomTheme(in); } @Override public CustomTheme[] newArray(int size) { return new CustomTheme[size]; } }; public String getJSONModel() { Gson gson = getGsonBuilder().create(); return gson.toJson(this); } private static GsonBuilder getGsonBuilder() { GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(CustomTheme.class, new CustomThemeSerializer()); builder.registerTypeAdapter(CustomTheme.class, new CustomThemeDeserializer()); return builder; } public static CustomTheme fromJson(String json) throws JsonParseException { Gson gson = getGsonBuilder().create(); return gson.fromJson(json, CustomTheme.class); } public static CustomTheme convertSettingsItemsToCustomTheme(ArrayList customThemeSettingsItems, String themeName) { CustomTheme customTheme = new CustomTheme(themeName); if (customThemeSettingsItems.isEmpty()) { return customTheme; } customTheme.isLightTheme = customThemeSettingsItems.get(0).isEnabled; customTheme.isDarkTheme = customThemeSettingsItems.get(1).isEnabled; customTheme.isAmoledTheme = customThemeSettingsItems.get(2).isEnabled; customTheme.colorPrimary = customThemeSettingsItems.get(3).colorValue; customTheme.colorPrimaryDark = customThemeSettingsItems.get(4).colorValue; customTheme.colorAccent = customThemeSettingsItems.get(5).colorValue; customTheme.colorPrimaryLightTheme = customThemeSettingsItems.get(6).colorValue; customTheme.primaryTextColor = customThemeSettingsItems.get(7).colorValue; customTheme.secondaryTextColor = customThemeSettingsItems.get(8).colorValue; customTheme.postTitleColor = customThemeSettingsItems.get(9).colorValue; customTheme.postContentColor = customThemeSettingsItems.get(10).colorValue; customTheme.readPostTitleColor = customThemeSettingsItems.get(11).colorValue; customTheme.readPostContentColor = customThemeSettingsItems.get(12).colorValue; customTheme.commentColor = customThemeSettingsItems.get(13).colorValue; customTheme.buttonTextColor = customThemeSettingsItems.get(14).colorValue; customTheme.chipTextColor = customThemeSettingsItems.get(15).colorValue; customTheme.linkColor = customThemeSettingsItems.get(16).colorValue; customTheme.receivedMessageTextColor = customThemeSettingsItems.get(17).colorValue; customTheme.sentMessageTextColor = customThemeSettingsItems.get(18).colorValue; customTheme.backgroundColor = customThemeSettingsItems.get(19).colorValue; customTheme.cardViewBackgroundColor = customThemeSettingsItems.get(20).colorValue; customTheme.readPostCardViewBackgroundColor = customThemeSettingsItems.get(21).colorValue; customTheme.filledCardViewBackgroundColor = customThemeSettingsItems.get(22).colorValue; customTheme.readPostFilledCardViewBackgroundColor = customThemeSettingsItems.get(23).colorValue; customTheme.commentBackgroundColor = customThemeSettingsItems.get(24).colorValue; customTheme.fullyCollapsedCommentBackgroundColor = customThemeSettingsItems.get(25).colorValue; customTheme.awardedCommentBackgroundColor = customThemeSettingsItems.get(26).colorValue; customTheme.receivedMessageBackgroundColor = customThemeSettingsItems.get(27).colorValue; customTheme.sentMessageBackgroundColor = customThemeSettingsItems.get(28).colorValue; customTheme.bottomAppBarBackgroundColor = customThemeSettingsItems.get(29).colorValue; customTheme.primaryIconColor = customThemeSettingsItems.get(30).colorValue; customTheme.bottomAppBarIconColor = customThemeSettingsItems.get(31).colorValue; customTheme.postIconAndInfoColor = customThemeSettingsItems.get(32).colorValue; customTheme.commentIconAndInfoColor = customThemeSettingsItems.get(33).colorValue; customTheme.fabIconColor = customThemeSettingsItems.get(34).colorValue; customTheme.sendMessageIconColor = customThemeSettingsItems.get(35).colorValue; customTheme.toolbarPrimaryTextAndIconColor = customThemeSettingsItems.get(36).colorValue; customTheme.toolbarSecondaryTextColor = customThemeSettingsItems.get(37).colorValue; customTheme.circularProgressBarBackground = customThemeSettingsItems.get(38).colorValue; customTheme.mediaIndicatorIconColor = customThemeSettingsItems.get(39).colorValue; customTheme.mediaIndicatorBackgroundColor = customThemeSettingsItems.get(40).colorValue; customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = customThemeSettingsItems.get(41).colorValue; customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = customThemeSettingsItems.get(42).colorValue; customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = customThemeSettingsItems.get(43).colorValue; customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = customThemeSettingsItems.get(44).colorValue; customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = customThemeSettingsItems.get(45).colorValue; customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = customThemeSettingsItems.get(46).colorValue; customTheme.upvoted = customThemeSettingsItems.get(47).colorValue; customTheme.downvoted = customThemeSettingsItems.get(48).colorValue; customTheme.postTypeBackgroundColor = customThemeSettingsItems.get(49).colorValue; customTheme.postTypeTextColor = customThemeSettingsItems.get(50).colorValue; customTheme.spoilerBackgroundColor = customThemeSettingsItems.get(51).colorValue; customTheme.spoilerTextColor = customThemeSettingsItems.get(52).colorValue; customTheme.nsfwBackgroundColor = customThemeSettingsItems.get(53).colorValue; customTheme.nsfwTextColor = customThemeSettingsItems.get(54).colorValue; customTheme.flairBackgroundColor = customThemeSettingsItems.get(55).colorValue; customTheme.flairTextColor = customThemeSettingsItems.get(56).colorValue; customTheme.awardsBackgroundColor = customThemeSettingsItems.get(57).colorValue; customTheme.awardsTextColor = customThemeSettingsItems.get(58).colorValue; customTheme.archivedTint = customThemeSettingsItems.get(59).colorValue; customTheme.lockedIconTint = customThemeSettingsItems.get(60).colorValue; customTheme.crosspostIconTint = customThemeSettingsItems.get(61).colorValue; customTheme.upvoteRatioIconTint = customThemeSettingsItems.get(62).colorValue; customTheme.stickiedPostIconTint = customThemeSettingsItems.get(63).colorValue; customTheme.noPreviewPostTypeIconTint = customThemeSettingsItems.get(64).colorValue; customTheme.subscribed = customThemeSettingsItems.get(65).colorValue; customTheme.unsubscribed = customThemeSettingsItems.get(66).colorValue; customTheme.username = customThemeSettingsItems.get(67).colorValue; customTheme.subreddit = customThemeSettingsItems.get(68).colorValue; customTheme.authorFlairTextColor = customThemeSettingsItems.get(69).colorValue; customTheme.submitter = customThemeSettingsItems.get(70).colorValue; customTheme.moderator = customThemeSettingsItems.get(71).colorValue; customTheme.currentUser = customThemeSettingsItems.get(72).colorValue; customTheme.singleCommentThreadBackgroundColor = customThemeSettingsItems.get(73).colorValue; customTheme.unreadMessageBackgroundColor = customThemeSettingsItems.get(74).colorValue; customTheme.dividerColor = customThemeSettingsItems.get(75).colorValue; customTheme.noPreviewPostTypeBackgroundColor = customThemeSettingsItems.get(76).colorValue; customTheme.voteAndReplyUnavailableButtonColor = customThemeSettingsItems.get(77).colorValue; customTheme.commentVerticalBarColor1 = customThemeSettingsItems.get(78).colorValue; customTheme.commentVerticalBarColor2 = customThemeSettingsItems.get(79).colorValue; customTheme.commentVerticalBarColor3 = customThemeSettingsItems.get(80).colorValue; customTheme.commentVerticalBarColor4 = customThemeSettingsItems.get(81).colorValue; customTheme.commentVerticalBarColor5 = customThemeSettingsItems.get(82).colorValue; customTheme.commentVerticalBarColor6 = customThemeSettingsItems.get(83).colorValue; customTheme.commentVerticalBarColor7 = customThemeSettingsItems.get(84).colorValue; customTheme.navBarColor = customThemeSettingsItems.get(85).colorValue; customTheme.isLightStatusBar = customThemeSettingsItems.get(86).isEnabled; customTheme.isLightNavBar = customThemeSettingsItems.get(87).isEnabled; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = customThemeSettingsItems.get(88).isEnabled; return customTheme; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(name); dest.writeByte((byte) (isLightTheme ? 1 : 0)); dest.writeByte((byte) (isDarkTheme ? 1 : 0)); dest.writeByte((byte) (isAmoledTheme ? 1 : 0)); dest.writeInt(colorPrimary); dest.writeInt(colorPrimaryDark); dest.writeInt(colorAccent); dest.writeInt(colorPrimaryLightTheme); dest.writeInt(primaryTextColor); dest.writeInt(secondaryTextColor); dest.writeInt(postTitleColor); dest.writeInt(postContentColor); dest.writeInt(readPostTitleColor); dest.writeInt(readPostContentColor); dest.writeInt(commentColor); dest.writeInt(buttonTextColor); dest.writeInt(backgroundColor); dest.writeInt(cardViewBackgroundColor); dest.writeInt(readPostCardViewBackgroundColor); dest.writeInt(filledCardViewBackgroundColor); dest.writeInt(readPostFilledCardViewBackgroundColor); dest.writeInt(commentBackgroundColor); dest.writeInt(bottomAppBarBackgroundColor); dest.writeInt(primaryIconColor); dest.writeInt(bottomAppBarIconColor); dest.writeInt(postIconAndInfoColor); dest.writeInt(commentIconAndInfoColor); dest.writeInt(toolbarPrimaryTextAndIconColor); dest.writeInt(toolbarSecondaryTextColor); dest.writeInt(circularProgressBarBackground); dest.writeInt(mediaIndicatorIconColor); dest.writeInt(mediaIndicatorBackgroundColor); dest.writeInt(tabLayoutWithExpandedCollapsingToolbarTabBackground); dest.writeInt(tabLayoutWithExpandedCollapsingToolbarTextColor); dest.writeInt(tabLayoutWithExpandedCollapsingToolbarTabIndicator); dest.writeInt(tabLayoutWithCollapsedCollapsingToolbarTabBackground); dest.writeInt(tabLayoutWithCollapsedCollapsingToolbarTextColor); dest.writeInt(tabLayoutWithCollapsedCollapsingToolbarTabIndicator); dest.writeInt(navBarColor); dest.writeInt(upvoted); dest.writeInt(downvoted); dest.writeInt(postTypeBackgroundColor); dest.writeInt(postTypeTextColor); dest.writeInt(spoilerBackgroundColor); dest.writeInt(spoilerTextColor); dest.writeInt(nsfwBackgroundColor); dest.writeInt(nsfwTextColor); dest.writeInt(flairBackgroundColor); dest.writeInt(flairTextColor); dest.writeInt(awardsBackgroundColor); dest.writeInt(awardsTextColor); dest.writeInt(archivedTint); dest.writeInt(lockedIconTint); dest.writeInt(crosspostIconTint); dest.writeInt(upvoteRatioIconTint); dest.writeInt(stickiedPostIconTint); dest.writeInt(noPreviewPostTypeIconTint); dest.writeInt(subscribed); dest.writeInt(unsubscribed); dest.writeInt(username); dest.writeInt(subreddit); dest.writeInt(authorFlairTextColor); dest.writeInt(submitter); dest.writeInt(moderator); dest.writeInt(currentUser); dest.writeInt(singleCommentThreadBackgroundColor); dest.writeInt(unreadMessageBackgroundColor); dest.writeInt(dividerColor); dest.writeInt(noPreviewPostTypeBackgroundColor); dest.writeInt(voteAndReplyUnavailableButtonColor); dest.writeInt(commentVerticalBarColor1); dest.writeInt(commentVerticalBarColor2); dest.writeInt(commentVerticalBarColor3); dest.writeInt(commentVerticalBarColor4); dest.writeInt(commentVerticalBarColor5); dest.writeInt(commentVerticalBarColor6); dest.writeInt(commentVerticalBarColor7); dest.writeInt(fabIconColor); dest.writeInt(chipTextColor); dest.writeInt(linkColor); dest.writeInt(receivedMessageTextColor); dest.writeInt(sentMessageTextColor); dest.writeInt(receivedMessageBackgroundColor); dest.writeInt(sentMessageBackgroundColor); dest.writeInt(sendMessageIconColor); dest.writeInt(fullyCollapsedCommentBackgroundColor); dest.writeInt(awardedCommentBackgroundColor); dest.writeByte((byte) (isLightStatusBar ? 1 : 0)); dest.writeByte((byte) (isLightNavBar ? 1 : 0)); dest.writeByte((byte) (isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface ? 1 : 0)); } private static class CustomThemeSerializer implements JsonSerializer { @Override public JsonElement serialize(CustomTheme src, Type typeofSrc, JsonSerializationContext context) { JsonObject obj = new JsonObject(); for (Field field : src.getClass().getDeclaredFields()) { try { if (field.getType() == int.class) { obj.addProperty(field.getName(), String.format("#%08X", field.getInt(src))); } else { obj.add(field.getName(), context.serialize(field.get(src))); } } catch (IllegalAccessException ignored) { } } return obj; } } private static class CustomThemeDeserializer implements JsonDeserializer { @Override public CustomTheme deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { CustomTheme customTheme = new CustomTheme(); JsonObject obj = json.getAsJsonObject(); for (Map.Entry entry : obj.entrySet()) { Field field; try { field = customTheme.getClass().getDeclaredField(entry.getKey()); } catch (NoSuchFieldException e) { // Field not found, skip continue; } JsonElement value = entry.getValue(); try { Class type = field.getType(); if (int.class.equals(type)) { if (value.getAsJsonPrimitive().isString()) { // Hex or text color string field.set(customTheme, Color.parseColor(value.getAsString())); } else { // Int color field.set(customTheme, value.getAsInt()); } } else if (String.class.equals(type)) { field.set(customTheme, value.getAsString()); } else if (boolean.class.equals(type)) { field.set(customTheme, value.getAsBoolean()); } } catch (IllegalAccessException e) { throw new JsonParseException("Failed to access theme field."); } catch (IllegalArgumentException e) { throw new JsonParseException("Invalid color string."); } } return customTheme; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/CustomThemeDao.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; @Dao public interface CustomThemeDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(CustomTheme customTheme); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List customThemes); @Query("SELECT * FROM custom_themes") LiveData> getAllCustomThemes(); @Query("SELECT * FROM custom_themes") List getAllCustomThemesList(); @Query("SELECT * FROM custom_themes WHERE is_light_theme = 1 LIMIT 1") CustomTheme getLightCustomTheme(); @Query("SELECT * FROM custom_themes WHERE is_dark_theme = 1 LIMIT 1") CustomTheme getDarkCustomTheme(); @Query("SELECT * FROM custom_themes WHERE is_amoled_theme = 1 LIMIT 1") CustomTheme getAmoledCustomTheme(); @Query("SELECT * FROM custom_themes WHERE is_light_theme = 1 LIMIT 1") LiveData getLightCustomThemeLiveData(); @Query("SELECT * FROM custom_themes WHERE is_dark_theme = 1 LIMIT 1") LiveData getDarkCustomThemeLiveData(); @Query("SELECT * FROM custom_themes WHERE is_amoled_theme = 1 LIMIT 1") LiveData getAmoledCustomThemeLiveData(); @Query("SELECT * FROM custom_themes WHERE name = :name COLLATE NOCASE LIMIT 1") CustomTheme getCustomTheme(String name); @Query("UPDATE custom_themes SET is_light_theme = 0 WHERE is_light_theme = 1") void unsetLightTheme(); @Query("UPDATE custom_themes SET is_dark_theme = 0 WHERE is_dark_theme = 1") void unsetDarkTheme(); @Query("UPDATE custom_themes SET is_amoled_theme = 0 WHERE is_amoled_theme = 1") void unsetAmoledTheme(); @Query("DELETE FROM custom_themes WHERE name = :name") void deleteCustomTheme(String name); @Query("UPDATE custom_themes SET name = :newName WHERE name = :oldName") void updateName(String oldName, String newName); @Query("DELETE FROM custom_themes") void deleteAllCustomThemes(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/CustomThemeDaoKt.kt ================================================ package ml.docilealligator.infinityforreddit.customtheme import androidx.room.Dao import androidx.room.Query import kotlinx.coroutines.flow.Flow @Dao interface CustomThemeDaoKt { @Query("SELECT * FROM custom_themes WHERE is_light_theme = 1 LIMIT 1") fun getLightCustomThemeFlow(): Flow @Query("SELECT * FROM custom_themes WHERE is_dark_theme = 1 LIMIT 1") fun getDarkCustomThemeFlow(): Flow @Query("SELECT * FROM custom_themes WHERE is_amoled_theme = 1 LIMIT 1") fun getAmoledCustomThemeFlow(): Flow } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/CustomThemeSettingsItem.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import android.content.Context; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; public class CustomThemeSettingsItem implements Parcelable { public String itemName; public String itemDetails; public int colorValue; public boolean isEnabled; private CustomThemeSettingsItem(String itemName, String itemDetails, int colorValue) { this.itemName = itemName; this.itemDetails = itemDetails; this.colorValue = colorValue; } private CustomThemeSettingsItem(String itemName, boolean isEnabled) { this.itemName = itemName; this.isEnabled = isEnabled; } protected CustomThemeSettingsItem(Parcel in) { itemName = in.readString(); itemDetails = in.readString(); colorValue = in.readInt(); isEnabled = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public CustomThemeSettingsItem createFromParcel(Parcel in) { return new CustomThemeSettingsItem(in); } @Override public CustomThemeSettingsItem[] newArray(int size) { return new CustomThemeSettingsItem[size]; } }; public static ArrayList convertCustomThemeToSettingsItem(Context context, CustomTheme customTheme, int androidVersion) { ArrayList customThemeSettingsItems = new ArrayList<>(); if (customTheme == null) { return customThemeSettingsItems; } customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_is_light_theme), customTheme.isLightTheme )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_is_dark_theme), customTheme.isDarkTheme )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_is_amoled_theme), customTheme.isAmoledTheme )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_color_primary), context.getString(R.string.theme_item_color_primary_detail), customTheme.colorPrimary)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_color_primary_dark), context.getString(R.string.theme_item_color_primary_dark_detail), customTheme.colorPrimaryDark)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_color_accent), context.getString(R.string.theme_item_color_accent_detail), customTheme.colorAccent)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_color_primary_light_theme), context.getString(R.string.theme_item_color_primary_light_theme_detail), customTheme.colorPrimaryLightTheme)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_primary_text_color), context.getString(R.string.theme_item_primary_text_color_detail), customTheme.primaryTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_secondary_text_color), context.getString(R.string.theme_item_secondary_text_color_detail), customTheme.secondaryTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_post_title_color), context.getString(R.string.theme_item_post_title_color_detail), customTheme.postTitleColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_post_content_color), context.getString(R.string.theme_item_post_content_color_detail), customTheme.postContentColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_read_post_title_color), context.getString(R.string.theme_item_read_post_title_color_detail), customTheme.readPostTitleColor )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_read_post_content_color), context.getString(R.string.theme_item_read_post_content_color_detail), customTheme.readPostContentColor )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_color), context.getString(R.string.theme_item_comment_color_detail), customTheme.commentColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_button_text_color), context.getString(R.string.theme_item_button_text_color_detail), customTheme.buttonTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_chip_text_color), context.getString(R.string.theme_item_chip_text_color_detail), customTheme.chipTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_link_color), context.getString(R.string.theme_item_link_color_detail), customTheme.linkColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_received_message_text_color), context.getString(R.string.theme_item_received_message_text_color_detail), customTheme.receivedMessageTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_sent_message_text_color), context.getString(R.string.theme_item_sent_message_text_color_detail), customTheme.sentMessageTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_background_color), context.getString(R.string.theme_item_background_color_detail), customTheme.backgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_card_view_background_color), context.getString(R.string.theme_item_card_view_background_color_detail), customTheme.cardViewBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_read_post_card_view_background_color), context.getString(R.string.theme_item_read_post_card_view_background_color_detail), customTheme.readPostCardViewBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_filled_card_view_background_color), context.getString(R.string.theme_item_filled_card_view_background_color_detail), customTheme.filledCardViewBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_read_post_filled_card_view_background_color), context.getString(R.string.theme_item_read_post_filled_card_view_background_color_detail), customTheme.readPostFilledCardViewBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_background_color), context.getString(R.string.theme_item_comment_background_color_detail), customTheme.commentBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_fully_collapsed_comment_background_color), context.getString(R.string.theme_item_fully_collapsed_comment_background_color_detail), customTheme.fullyCollapsedCommentBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_awarded_comment_background_color), context.getString(R.string.theme_item_awarded_comment_background_color_detail), customTheme.awardedCommentBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_received_message_background_color), context.getString(R.string.theme_item_received_message_background_color_detail), customTheme.receivedMessageBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_sent_message_background_color), context.getString(R.string.theme_item_sent_message_background_color_detail), customTheme.sentMessageBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_bottom_app_bar_background_color), context.getString(R.string.theme_item_bottom_app_bar_background_color_detail), customTheme.bottomAppBarBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_primary_icon_color), context.getString(R.string.theme_item_primary_icon_color_detail), customTheme.primaryIconColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_bottom_app_bar_icon_color), context.getString(R.string.theme_item_bottom_app_bar_icon_color_detail), customTheme.bottomAppBarIconColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_post_icon_and_info_color), context.getString(R.string.theme_item_post_icon_and_info_color_detail), customTheme.postIconAndInfoColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_icon_and_info_color), context.getString(R.string.theme_item_comment_icon_and_info_color_detail), customTheme.commentIconAndInfoColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_fab_icon_color), context.getString(R.string.theme_item_fab_icon_color_detail), customTheme.fabIconColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_send_message_icon_color), context.getString(R.string.theme_item_send_message_icon_color_detail), customTheme.sendMessageIconColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_toolbar_primary_text_and_icon_color), context.getString(R.string.theme_item_toolbar_primary_text_and_icon_color_detail), customTheme.toolbarPrimaryTextAndIconColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_toolbar_secondary_text_color), context.getString(R.string.theme_item_toolbar_secondary_text_color_detail), customTheme.toolbarSecondaryTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_circular_progress_bar_background_color), context.getString(R.string.theme_item_circular_progress_bar_background_color_detail), customTheme.circularProgressBarBackground)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_media_indicator_icon_color), context.getString(R.string.theme_item_media_indicator_icon_color_detail), customTheme.mediaIndicatorIconColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_media_indicator_background_color), context.getString(R.string.theme_item_media_indicator_background_color_detail), customTheme.mediaIndicatorBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_tab_layout_with_expanded_collapsing_toolbar_tab_background), context.getString(R.string.theme_item_tab_layout_with_expanded_collapsing_toolbar_tab_background_detail), customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_tab_layout_with_expanded_collapsing_toolbar_text_color), context.getString(R.string.theme_item_tab_layout_with_expanded_collapsing_toolbar_text_color_detail), customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_tab_layout_with_expanded_collapsing_toolbar_tab_indicator), context.getString(R.string.theme_item_tab_layout_with_expanded_collapsing_toolbar_tab_indicator_detail), customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_tab_layout_with_collapsed_collapsing_toolbar_tab_background), context.getString(R.string.theme_item_tab_layout_with_collapsed_collapsing_toolbar_tab_background_detail), customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_tab_layout_with_collapsed_collapsing_toolbar_text_color), context.getString(R.string.theme_item_tab_layout_with_collapsed_collapsing_toolbar_text_color_detail), customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_tab_layout_with_collapsed_collapsing_toolbar_tab_indicator), context.getString(R.string.theme_item_tab_layout_with_collapsed_collapsing_toolbar_tab_indicator_detail), customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_upvoted_color), context.getString(R.string.theme_item_upvoted_color_detail), customTheme.upvoted)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_downvoted_color), context.getString(R.string.theme_item_downvoted_color_detail), customTheme.downvoted)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_post_type_background_color), context.getString(R.string.theme_item_post_type_background_color_detail), customTheme.postTypeBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_post_type_text_color), context.getString(R.string.theme_item_post_type_text_color_detail), customTheme.postTypeTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_spoiler_background_color), context.getString(R.string.theme_item_spoiler_background_color_detail), customTheme.spoilerBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_spoiler_text_color), context.getString(R.string.theme_item_spoiler_text_color_detail), customTheme.spoilerTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_nsfw_background_color), context.getString(R.string.theme_item_nsfw_background_color_detail), customTheme.nsfwBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_nsfw_text_color), context.getString(R.string.theme_item_nsfw_text_color_detail), customTheme.nsfwTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_flair_background_color), context.getString(R.string.theme_item_flair_background_color_detail), customTheme.flairBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_flair_text_color), context.getString(R.string.theme_item_flair_text_color_detail), customTheme.flairTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_awards_background_color), context.getString(R.string.theme_item_awards_background_color_detail), customTheme.awardsBackgroundColor )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_awards_text_color), context.getString(R.string.theme_item_awards_text_color_detail), customTheme.awardsTextColor )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_archived_tint), context.getString(R.string.theme_item_archived_tint_detail), customTheme.archivedTint)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_locked_icon_tint), context.getString(R.string.theme_item_locked_icon_tint_detail), customTheme.lockedIconTint)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_crosspost_icon_tint), context.getString(R.string.theme_item_crosspost_icon_tint_detail), customTheme.crosspostIconTint)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_upvote_ratio_icon_tint), context.getString(R.string.theme_item_upvote_ratio_icon_tint_detail), customTheme.upvoteRatioIconTint )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_stickied_post_icon_tint), context.getString(R.string.theme_item_stickied_post_icon_tint_detail), customTheme.stickiedPostIconTint)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_no_preview_post_type_icon_tint), context.getString(R.string.theme_item_no_preview_post_type_icon_tint_detail), customTheme.noPreviewPostTypeIconTint )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_subscribed_color), context.getString(R.string.theme_item_subscribed_color_detail), customTheme.subscribed)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_unsubscribed_color), context.getString(R.string.theme_item_unsubscribed_color_detail), customTheme.unsubscribed)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_username_color), context.getString(R.string.theme_item_username_color_detail), customTheme.username)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_subreddit_color), context.getString(R.string.theme_item_subreddit_color_detail), customTheme.subreddit)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_author_flair_text_color), context.getString(R.string.theme_item_author_flair_text_color_detail), customTheme.authorFlairTextColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_submitter_color), context.getString(R.string.theme_item_submitter_color_detail), customTheme.submitter)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_moderator_color), context.getString(R.string.theme_item_moderator_color_detail), customTheme.moderator)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_current_user_color), context.getString(R.string.theme_item_current_user_color_detail), customTheme.currentUser )); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_single_comment_thread_background_color), context.getString(R.string.theme_item_single_comment_thread_background_color_detail), customTheme.singleCommentThreadBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_unread_message_background_color), context.getString(R.string.theme_item_unread_message_background_color_detail), customTheme.unreadMessageBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_divider_color), context.getString(R.string.theme_item_divider_color_detail), customTheme.dividerColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_no_preview_post_type_background_color), context.getString(R.string.theme_item_no_preview_post_type_background_color_detail), customTheme.noPreviewPostTypeBackgroundColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_vote_and_reply_unavailable_button_color), context.getString(R.string.theme_item_vote_and_reply_unavailable_button_color_detail), customTheme.voteAndReplyUnavailableButtonColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_vertical_bar_color_1), context.getString(R.string.theme_item_comment_vertical_bar_color_1_detail), customTheme.commentVerticalBarColor1)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_vertical_bar_color_2), context.getString(R.string.theme_item_comment_vertical_bar_color_2_detail), customTheme.commentVerticalBarColor2)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_vertical_bar_color_3), context.getString(R.string.theme_item_comment_vertical_bar_color_3_detail), customTheme.commentVerticalBarColor3)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_vertical_bar_color_4), context.getString(R.string.theme_item_comment_vertical_bar_color_4_detail), customTheme.commentVerticalBarColor4)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_vertical_bar_color_5), context.getString(R.string.theme_item_comment_vertical_bar_color_5_detail), customTheme.commentVerticalBarColor5)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_vertical_bar_color_6), context.getString(R.string.theme_item_comment_vertical_bar_color_6_detail), customTheme.commentVerticalBarColor6)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_comment_vertical_bar_color_7), context.getString(R.string.theme_item_comment_vertical_bar_color_7_detail), customTheme.commentVerticalBarColor7)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_nav_bar_color), context.getString(R.string.theme_item_nav_bar_color_detail), customTheme.navBarColor)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_light_status_bar), customTheme.isLightStatusBar)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_light_nav_bar), customTheme.isLightNavBar)); customThemeSettingsItems.add(new CustomThemeSettingsItem( context.getString(R.string.theme_item_change_status_bar_icon_color_after_toolbar_collapsed_in_immersive_interface), customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface)); if (androidVersion < Build.VERSION_CODES.O) { customThemeSettingsItems.get(customThemeSettingsItems.size() - 2).itemDetails = context.getString(R.string.theme_item_available_on_android_8); } if (androidVersion < Build.VERSION_CODES.M) { customThemeSettingsItems.get(customThemeSettingsItems.size() - 3).itemDetails = context.getString(R.string.theme_item_available_on_android_6); } return customThemeSettingsItems; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(itemName); parcel.writeString(itemDetails); parcel.writeInt(colorValue); parcel.writeByte((byte) (isEnabled ? 1 : 0)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/CustomThemeViewModel.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelKt; import androidx.lifecycle.ViewModelProvider; import androidx.paging.PagingData; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import retrofit2.Retrofit; public class CustomThemeViewModel extends ViewModel { @Nullable private LocalCustomThemeRepository localCustomThemeRepository; @Nullable private OnlineCustomThemeRepository onlineCustomThemeRepository; public CustomThemeViewModel(RedditDataRoomDatabase redditDataRoomDatabase) { localCustomThemeRepository = new LocalCustomThemeRepository(redditDataRoomDatabase); } public CustomThemeViewModel(Executor executor, Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase) { onlineCustomThemeRepository = new OnlineCustomThemeRepository(executor, retrofit, redditDataRoomDatabase, ViewModelKt.getViewModelScope(this)); } @Nullable public LiveData> getAllCustomThemes() { return localCustomThemeRepository.getAllCustomThemes(); } public LiveData getCurrentLightThemeLiveData() { return localCustomThemeRepository.getCurrentLightCustomTheme(); } public LiveData getCurrentDarkThemeLiveData() { return localCustomThemeRepository.getCurrentDarkCustomTheme(); } @Nullable public LiveData getCurrentAmoledThemeLiveData() { return localCustomThemeRepository.getCurrentAmoledCustomTheme(); } public LiveData> getOnlineCustomThemeMetadata() { return onlineCustomThemeRepository.getOnlineCustomThemeMetadata(); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private Executor executor; private Retrofit retrofit; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final boolean isOnline; public Factory(RedditDataRoomDatabase redditDataRoomDatabase) { mRedditDataRoomDatabase = redditDataRoomDatabase; isOnline = false; } public Factory(Executor executor, Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase) { this.executor = executor; this.retrofit = retrofit; mRedditDataRoomDatabase = redditDataRoomDatabase; isOnline = true; } @NonNull @Override public T create(@NonNull Class modelClass) { if (isOnline) { return (T) new CustomThemeViewModel(executor, retrofit, mRedditDataRoomDatabase); } else { return (T) new CustomThemeViewModel(mRedditDataRoomDatabase); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/CustomThemeWrapper.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import static ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils.AMOLED; import static ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils.DARK; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; public class CustomThemeWrapper { private final SharedPreferences lightThemeSharedPreferences; private final SharedPreferences darkThemeSharedPreferences; private final SharedPreferences amoledThemeSharedPreferences; private int themeType; public CustomThemeWrapper(SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences) { this.lightThemeSharedPreferences = lightThemeSharedPreferences; this.darkThemeSharedPreferences = darkThemeSharedPreferences; this.amoledThemeSharedPreferences = amoledThemeSharedPreferences; } private SharedPreferences getThemeSharedPreferences() { switch (themeType) { case DARK: return darkThemeSharedPreferences; case AMOLED: return amoledThemeSharedPreferences; default: return lightThemeSharedPreferences; } } private int getDefaultColor(String normalHex, String darkHex, String amoledDarkHex) { switch (themeType) { case DARK: return Color.parseColor(darkHex); case AMOLED: return Color.parseColor(amoledDarkHex); default: return Color.parseColor(normalHex); } } public int getThemeType() { return themeType; } public void setThemeType(int themeType) { this.themeType = themeType; } public int getColorPrimary() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COLOR_PRIMARY, getDefaultColor("#0336FF", "#242424", "#000000")); } public int getColorPrimaryDark() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COLOR_PRIMARY_DARK, getDefaultColor("#002BF0", "#121212", "#000000")); } public int getColorAccent() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COLOR_ACCENT, getDefaultColor("#FF1868", "#FF1868", "#FF1868")); } public int getColorPrimaryLightTheme() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COLOR_PRIMARY_LIGHT_THEME, getDefaultColor("#0336FF", "#0336FF", "#0336FF")); } public int getPrimaryTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.PRIMARY_TEXT_COLOR, getDefaultColor("#000000", "#FFFFFF", "#FFFFFF")); } public int getSecondaryTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SECONDARY_TEXT_COLOR, getDefaultColor("#8A000000", "#B3FFFFFF", "#B3FFFFFF")); } public int getPostTitleColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.POST_TITLE_COLOR, getDefaultColor("#000000", "#FFFFFF", "#FFFFFF")); } public int getPostContentColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.POST_CONTENT_COLOR, getDefaultColor("#8A000000", "#B3FFFFFF", "#B3FFFFFF")); } public int getReadPostTitleColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.READ_POST_TITLE_COLOR, getDefaultColor("#9D9D9D", "#979797", "#979797")); } public int getReadPostContentColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.READ_POST_CONTENT_COLOR, getDefaultColor("#9D9D9D", "#979797", "#979797")); } public int getCommentColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_COLOR, getDefaultColor("#000000", "#FFFFFF", "#FFFFFF")); } public int getButtonTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.BUTTON_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.BACKGROUND_COLOR, getDefaultColor("#FFFFFF", "#121212", "#000000")); } public int getCardViewBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.CARD_VIEW_BACKGROUND_COLOR, getDefaultColor("#FFFFFF", "#242424", "#000000")); } public int getReadPostCardViewBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.READ_POST_CARD_VIEW_BACKGROUND_COLOR, getDefaultColor("#F5F5F5", "#101010", "#000000")); } public int getFilledCardViewBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.FILLED_CARD_VIEW_BACKGROUND_COLOR, getDefaultColor("#EDF6FD", "#242424", "#000000")); } public int getReadPostFilledCardViewBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.READ_POST_FILLED_CARD_VIEW_BACKGROUND_COLOR, getDefaultColor("#F5F5F5", "#101010", "#000000")); } public int getCommentBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_BACKGROUND_COLOR, getDefaultColor("#FFFFFF", "#242424", "#000000")); } public int getBottomAppBarBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.BOTTOM_APP_BAR_BACKGROUND_COLOR, getDefaultColor("#FFFFFF", "#121212", "#000000")); } public int getPrimaryIconColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.PRIMARY_ICON_COLOR, getDefaultColor("#000000", "#FFFFFF", "#FFFFFF")); } public int getBottomAppBarIconColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.BOTTOM_APP_BAR_ICON_COLOR, getDefaultColor("#000000", "#FFFFFF", "#FFFFFF")); } public int getPostIconAndInfoColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.POST_ICON_AND_INFO_COLOR, getDefaultColor("#8A000000", "#B3FFFFFF", "#B3FFFFFF")); } public int getCommentIconAndInfoColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_ICON_AND_INFO_COLOR, getDefaultColor("#8A000000", "#B3FFFFFF", "#B3FFFFFF")); } public int getToolbarPrimaryTextAndIconColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TOOLBAR_PRIMARY_TEXT_AND_ICON_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getToolbarSecondaryTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TOOLBAR_SECONDARY_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getCircularProgressBarBackground() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.CIRCULAR_PROGRESS_BAR_BACKGROUND, getDefaultColor("#FFFFFF", "#242424", "#000000")); } public int getMediaIndicatorIconColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.MEDIA_INDICATOR_ICON_COLOR, getDefaultColor("#FFFFFF", "#000000", "#000000")); } public int getMediaIndicatorBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.MEDIA_INDICATOR_BACKGROUND_COLOR, getDefaultColor("#000000", "#FFFFFF", "#FFFFFF")); } public int getTabLayoutWithExpandedCollapsingToolbarTabBackground() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TAB_BACKGROUND, getDefaultColor("#FFFFFF", "#242424", "#000000")); } public int getTabLayoutWithExpandedCollapsingToolbarTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TEXT_COLOR, getDefaultColor("#0336FF", "#FFFFFF", "#FFFFFF")); } public int getTabLayoutWithExpandedCollapsingToolbarTabIndicator() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TAB_INDICATOR, getDefaultColor("#0336FF", "#FFFFFF", "#FFFFFF")); } public int getTabLayoutWithCollapsedCollapsingToolbarTabBackground() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TAB_BACKGROUND, getDefaultColor("#0336FF", "#242424", "#000000")); } public int getTabLayoutWithCollapsedCollapsingToolbarTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getTabLayoutWithCollapsedCollapsingToolbarTabIndicator() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TAB_INDICATOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getUpvoted() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.UPVOTED, getDefaultColor("#FF1868", "#FF1868", "#FF1868")); } public int getDownvoted() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.DOWNVOTED, getDefaultColor("#007DDE", "#007DDE", "#007DDE")); } public int getPostTypeBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.POST_TYPE_BACKGROUND_COLOR, getDefaultColor("#002BF0", "#0336FF", "#0336FF")); } public int getPostTypeTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.POST_TYPE_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getSpoilerBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SPOILER_BACKGROUND_COLOR, getDefaultColor("#EE02EB", "#EE02EB", "#EE02EB")); } public int getSpoilerTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SPOILER_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getNsfwBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.NSFW_BACKGROUND_COLOR, getDefaultColor("#FF1868", "#FF1868", "#FF1868")); } public int getNsfwTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.NSFW_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getFlairBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.FLAIR_BACKGROUND_COLOR, getDefaultColor("#00AA8C", "#00AA8C", "#00AA8C")); } public int getFlairTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.FLAIR_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getArchivedIconTint() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.ARCHIVED_ICON_TINT, getDefaultColor("#B4009F", "#B4009F", "#B4009F")); } public int getLockedIconTint() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.LOCKED_ICON_TINT, getDefaultColor("#EE7302", "#EE7302", "#EE7302")); } public int getCrosspostIconTint() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.CROSSPOST_ICON_TINT, getDefaultColor("#FF1868", "#FF1868", "#FF1868")); } public int getUpvoteRatioIconTint() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.UPVOTE_RATIO_ICON_TINT, getDefaultColor("#0256EE", "#0256EE", "#0256EE")); } public int getStickiedPostIconTint() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.STICKIED_POST_ICON_TINT, getDefaultColor("#002BF0", "#0336FF", "#0336FF")); } public int getNoPreviewPostTypeIconTint() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.NO_PREVIEW_POST_TYPE_ICON_TINT, getDefaultColor("#808080", "#808080", "#808080")); } public int getSubscribed() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SUBSCRIBED, getDefaultColor("#FF1868", "#FF1868", "#FF1868")); } public int getUnsubscribed() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.UNSUBSCRIBED, getDefaultColor("#002BF0", "#0336FF", "#0336FF")); } public int getUsername() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.USERNAME, getDefaultColor("#002BF0", "#1E88E5", "#1E88E5")); } public int getSubreddit() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SUBREDDIT, getDefaultColor("#FF1868", "#FF1868", "#FF1868")); } public int getAuthorFlairTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.AUTHOR_FLAIR_TEXT_COLOR, getDefaultColor("#EE02C4", "#EE02C4", "#EE02C4")); } public int getSubmitter() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SUBMITTER, getDefaultColor("#EE8A02", "#EE8A02", "#EE8A02")); } public int getModerator() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.MODERATOR, getDefaultColor("#00BA81", "#00BA81", "#00BA81")); } public int getCurrentUser() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.CURRENT_USER, getDefaultColor("#00D5EA", "#00D5EA", "#00D5EA")); } public int getSingleCommentThreadBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SINGLE_COMMENT_THREAD_BACKGROUND_COLOR, getDefaultColor("#B3E5F9", "#123E77", "#123E77")); } public int getUnreadMessageBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.UNREAD_MESSAGE_BACKGROUND_COLOR, getDefaultColor("#B3E5F9", "#123E77", "#123E77")); } public int getDividerColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.DIVIDER_COLOR, getDefaultColor("#E0E0E0", "#69666C", "#69666C")); } public int getNoPreviewPostTypeBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.NO_PREVIEW_POST_TYPE_BACKGROUND_COLOR, getDefaultColor("#E0E0E0", "#424242", "#424242")); } public int getVoteAndReplyUnavailableButtonColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.VOTE_AND_REPLY_UNAVAILABLE_BUTTON_COLOR, getDefaultColor("#F0F0F0", "#3C3C3C", "#3C3C3C")); } public int getCommentVerticalBarColor1() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_VERTICAL_BAR_COLOR_1, getDefaultColor("#0336FF", "#0336FF", "#0336FF")); } public int getCommentVerticalBarColor2() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_VERTICAL_BAR_COLOR_2, getDefaultColor("#EE02BE", "#C300B3", "#C300B3")); } public int getCommentVerticalBarColor3() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_VERTICAL_BAR_COLOR_3, getDefaultColor("#02DFEE", "#00B8DA", "#00B8DA")); } public int getCommentVerticalBarColor4() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_VERTICAL_BAR_COLOR_4, getDefaultColor("#EED502", "#EDCA00", "#EDCA00")); } public int getCommentVerticalBarColor5() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_VERTICAL_BAR_COLOR_5, getDefaultColor("#EE0220", "#EE0219", "#EE0219")); } public int getCommentVerticalBarColor6() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_VERTICAL_BAR_COLOR_6, getDefaultColor("#02EE6E", "#00B925", "#00B925")); } public int getCommentVerticalBarColor7() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.COMMENT_VERTICAL_BAR_COLOR_7, getDefaultColor("#EE4602", "#EE4602", "#EE4602")); } public int getFABIconColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.FAB_ICON_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getChipTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.CHIP_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getLinkColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.LINK_COLOR, getDefaultColor("#FF1868", "#FF1868", "#FF1868")); } public int getReceivedMessageTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.RECEIVED_MESSAGE_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getSentMessageTextColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SENT_MESSAGE_TEXT_COLOR, getDefaultColor("#FFFFFF", "#FFFFFF", "#FFFFFF")); } public int getReceivedMessageBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.RECEIVED_MESSAGE_BACKROUND_COLOR, getDefaultColor("#4185F4", "#4185F4", "#4185F4")); } public int getSentMessageBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SENT_MESSAGE_BACKGROUND_COLOR, getDefaultColor("#31BF7D", "#31BF7D", "#31BF7D")); } public int getSendMessageIconColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.SEND_MESSAGE_ICON_COLOR, getDefaultColor("#4185F4", "#4185F4", "#4185F4")); } public int getFullyCollapsedCommentBackgroundColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.FULLY_COLLAPSED_COMMENT_BACKGROUND_COLOR, getDefaultColor("#8EDFBA", "#21C561", "#21C561")); } public int getNavBarColor() { return getThemeSharedPreferences().getInt(CustomThemeSharedPreferencesUtils.NAV_BAR_COLOR, getDefaultColor("#FFFFFF", "#121212", "#000000")); } public boolean isLightStatusBar() { return getThemeSharedPreferences().getBoolean(CustomThemeSharedPreferencesUtils.LIGHT_STATUS_BAR, false); } public boolean isLightNavBar() { return getThemeSharedPreferences().getBoolean(CustomThemeSharedPreferencesUtils.LIGHT_NAV_BAR, themeType == CustomThemeSharedPreferencesUtils.LIGHT); } public boolean isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface() { return getThemeSharedPreferences().getBoolean( CustomThemeSharedPreferencesUtils.CHANGE_STATUS_BAR_ICON_COLOR_AFTER_TOOLBAR_COLLAPSED_IN_IMMERSIVE_INTERFACE, themeType == CustomThemeSharedPreferencesUtils.LIGHT); } public static CustomTheme getPredefinedCustomTheme(Context context, String name) { if (name.equals(context.getString(R.string.theme_name_solarized_amoled))) { return getSolarizedAmoled(context); } else if (name.equals(context.getString(R.string.theme_name_indigo_dark))) { return getIndigoDark(context); } else if (name.equals(context.getString(R.string.theme_name_indigo_amoled))) { return getIndigoAmoled(context); } else if (name.equals(context.getString(R.string.theme_name_white))) { return getWhite(context); } else if (name.equals(context.getString(R.string.theme_name_white_dark))) { return getWhiteDark(context); } else if (name.equals(context.getString(R.string.theme_name_white_amoled))) { return getWhiteAmoled(context); } else if (name.equals(context.getString(R.string.theme_name_red))) { return getRed(context); } else if (name.equals(context.getString(R.string.theme_name_red_dark))) { return getRedDark(context); } else if (name.equals(context.getString(R.string.theme_name_red_amoled))) { return getRedAmoled(context); } else if (name.equals(context.getString(R.string.theme_name_dracula))) { return getDracula(context); } else if (name.equals(context.getString(R.string.theme_name_calm_pastel))) { return getCalmPastel(context); } else { return getIndigo(context); } } public static ArrayList getPredefinedThemes(Context context) { ArrayList customThemes = new ArrayList<>(); customThemes.add(getSolarizedAmoled(context)); customThemes.add(getIndigo(context)); customThemes.add(getIndigoDark(context)); customThemes.add(getIndigoAmoled(context)); customThemes.add(getWhite(context)); customThemes.add(getWhiteDark(context)); customThemes.add(getWhiteAmoled(context)); customThemes.add(getRed(context)); customThemes.add(getRedDark(context)); customThemes.add(getRedAmoled(context)); customThemes.add(getDracula(context)); customThemes.add(getCalmPastel(context)); return customThemes; } public static CustomTheme getSolarizedAmoled(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_solarized_amoled)); customTheme.isAmoledTheme = true; customTheme.isDarkTheme = false; customTheme.isLightTheme = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; customTheme.isLightNavBar = false; customTheme.isLightStatusBar = false; customTheme.archivedTint = Color.parseColor("#FFD33682"); customTheme.authorFlairTextColor = Color.parseColor("#FFD33682"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#FF000000"); customTheme.awardsBackgroundColor = Color.parseColor("#FFEEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFE4E4E4"); customTheme.backgroundColor = Color.parseColor("#FF000000"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#FF000000"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFE4E4E4"); customTheme.buttonTextColor = Color.parseColor("#FFE4E4E4"); customTheme.cardViewBackgroundColor = Color.parseColor("#FF000000"); customTheme.chipTextColor = Color.parseColor("#FFE4E4E4"); customTheme.circularProgressBarBackground = Color.parseColor("#FF000000"); customTheme.colorAccent = Color.parseColor("#FFCB4B16"); customTheme.colorPrimary = Color.parseColor("#FF070000"); customTheme.colorPrimaryDark = Color.parseColor("#FF000000"); customTheme.colorPrimaryLightTheme = Color.parseColor("#FF268BD2"); customTheme.commentBackgroundColor = Color.parseColor("#FF000000"); customTheme.commentColor = Color.parseColor("#FFE4E4E4"); customTheme.commentIconAndInfoColor = Color.parseColor("#FF586E75"); customTheme.commentVerticalBarColor1 = Color.parseColor("#FF268BD2"); customTheme.commentVerticalBarColor2 = Color.parseColor("#FFD33682"); customTheme.commentVerticalBarColor3 = Color.parseColor("#FF2AA198"); customTheme.commentVerticalBarColor4 = Color.parseColor("#FFEDCA00"); customTheme.commentVerticalBarColor5 = Color.parseColor("#FFDC322F"); customTheme.commentVerticalBarColor6 = Color.parseColor("#FF5F8700"); customTheme.commentVerticalBarColor7 = Color.parseColor("#FFCB4B16"); customTheme.crosspostIconTint = Color.parseColor("#FFCB4B16"); customTheme.currentUser = Color.parseColor("#FF00D5EA"); customTheme.dividerColor = Color.parseColor("#FF1C1C1C"); customTheme.downvoted = Color.parseColor("#FF268BD2"); customTheme.fabIconColor = Color.parseColor("#FFE4E4E4"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#FF000000"); customTheme.flairBackgroundColor = Color.parseColor("#FF2AA198"); customTheme.flairTextColor = Color.parseColor("#FFE4E4E4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#FF000000"); customTheme.linkColor = Color.parseColor("#FFCB4B16"); customTheme.lockedIconTint = Color.parseColor("#FFEE7302"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#FFE4E4E4"); customTheme.mediaIndicatorIconColor = Color.parseColor("#FF000000"); customTheme.moderator = Color.parseColor("#FF00BA81"); customTheme.navBarColor = Color.parseColor("#FF000000"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#FF424242"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#FF808080"); customTheme.nsfwBackgroundColor = Color.parseColor("#FFCB4B16"); customTheme.nsfwTextColor = Color.parseColor("#FFE4E4E4"); customTheme.postContentColor = Color.parseColor("#FFE4E4E4"); customTheme.postIconAndInfoColor = Color.parseColor("#FF586E75"); customTheme.postTitleColor = Color.parseColor("#FFE4E4E4"); customTheme.postTypeBackgroundColor = Color.parseColor("#FF268BD2"); customTheme.postTypeTextColor = Color.parseColor("#FFE4E4E4"); customTheme.primaryIconColor = Color.parseColor("#FFE4E4E4"); customTheme.primaryTextColor = Color.parseColor("#FFE4E4E4"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#FF000000"); customTheme.readPostContentColor = Color.parseColor("#FF979797"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#FF000000"); customTheme.readPostTitleColor = Color.parseColor("#FF979797"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#FF268BD2"); customTheme.receivedMessageTextColor = Color.parseColor("#FFE4E4E4"); customTheme.secondaryTextColor = Color.parseColor("#FF93A1A1"); customTheme.sendMessageIconColor = Color.parseColor("#FF268BD2"); customTheme.sentMessageBackgroundColor = Color.parseColor("#FF2AA198"); customTheme.sentMessageTextColor = Color.parseColor("#FFE4E4E4"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#FF123E77"); customTheme.spoilerBackgroundColor = Color.parseColor("#FFD33682"); customTheme.spoilerTextColor = Color.parseColor("#FFE4E4E4"); customTheme.stickiedPostIconTint = Color.parseColor("#FF268BD2"); customTheme.submitter = Color.parseColor("#FFEE8A02"); customTheme.subreddit = Color.parseColor("#FFCB4B16"); customTheme.subscribed = Color.parseColor("#FFCB4B16"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#FF000000"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFE4E4E4"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFE4E4E4"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#FF000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFE4E4E4"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFE4E4E4"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFE4E4E4"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFE4E4E4"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#FF123E77"); customTheme.unsubscribed = Color.parseColor("#FF93A1A1"); customTheme.upvoted = Color.parseColor("#FFCB4B16"); customTheme.upvoteRatioIconTint = Color.parseColor("#FF0256EE"); customTheme.username = Color.parseColor("#FF1E88E5"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#FF3C3C3C"); return customTheme; } public static CustomTheme getIndigo(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_indigo)); customTheme.isLightTheme = true; customTheme.isDarkTheme = false; customTheme.isAmoledTheme = false; customTheme.colorPrimary = Color.parseColor("#0336FF"); customTheme.colorPrimaryDark = Color.parseColor("#002BF0"); customTheme.colorAccent = Color.parseColor("#FF1868"); customTheme.colorPrimaryLightTheme = Color.parseColor("#0336FF"); customTheme.primaryTextColor = Color.parseColor("#000000"); customTheme.secondaryTextColor = Color.parseColor("#8A000000"); customTheme.postTitleColor = Color.parseColor("#000000"); customTheme.postContentColor = Color.parseColor("#8A000000"); customTheme.readPostTitleColor = Color.parseColor("#9D9D9D"); customTheme.readPostContentColor = Color.parseColor("#9D9D9D"); customTheme.commentColor = Color.parseColor("#000000"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#FFFFFF"); customTheme.cardViewBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#F5F5F5"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#E6F4FF"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#F5F5F5"); customTheme.commentBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.primaryIconColor = Color.parseColor("#000000"); customTheme.bottomAppBarIconColor = Color.parseColor("#000000"); customTheme.postIconAndInfoColor = Color.parseColor("#8A000000"); customTheme.commentIconAndInfoColor = Color.parseColor("#8A000000"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorIconColor = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#0336FF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#0336FF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#0336FF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#002BF0"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#FF002BF0"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#808080"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#002BF0"); customTheme.username = Color.parseColor("#002BF0"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#B3E5F9"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#B3E5F9"); customTheme.dividerColor = Color.parseColor("#E0E0E0"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#E0E0E0"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#F0F0F0"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#EE02BE"); customTheme.commentVerticalBarColor3 = Color.parseColor("#02DFEE"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EED502"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0220"); customTheme.commentVerticalBarColor6 = Color.parseColor("#02EE6E"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#FFFFFF"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#8EDFBA"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.navBarColor = Color.parseColor("#FFFFFF"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = true; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = true; return customTheme; } public static CustomTheme getIndigoDark(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_indigo_dark)); customTheme.isLightTheme = false; customTheme.isDarkTheme = true; customTheme.isAmoledTheme = false; customTheme.colorPrimary = Color.parseColor("#242424"); customTheme.colorPrimaryDark = Color.parseColor("#121212"); customTheme.colorAccent = Color.parseColor("#FF1868"); customTheme.colorPrimaryLightTheme = Color.parseColor("#0336FF"); customTheme.primaryTextColor = Color.parseColor("#FFFFFF"); customTheme.secondaryTextColor = Color.parseColor("#B3FFFFFF"); customTheme.postTitleColor = Color.parseColor("#FFFFFF"); customTheme.postContentColor = Color.parseColor("#B3FFFFFF"); customTheme.readPostTitleColor = Color.parseColor("#979797"); customTheme.readPostContentColor = Color.parseColor("#979797"); customTheme.commentColor = Color.parseColor("#FFFFFF"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#121212"); customTheme.cardViewBackgroundColor = Color.parseColor("#242424"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#101010"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#242424"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#101010"); customTheme.commentBackgroundColor = Color.parseColor("#242424"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#121212"); customTheme.primaryIconColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFFFFF"); customTheme.postIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.commentIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#242424"); customTheme.mediaIndicatorIconColor = Color.parseColor("#000000"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#242424"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#242424"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#0336FF"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#0336FF"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#808080"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#0336FF"); customTheme.username = Color.parseColor("#1E88E5"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#123E77"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#123E77"); customTheme.dividerColor = Color.parseColor("#69666C"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#424242"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#3C3C3C"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#C300B3"); customTheme.commentVerticalBarColor3 = Color.parseColor("#00B8DA"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EDCA00"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0219"); customTheme.commentVerticalBarColor6 = Color.parseColor("#00B925"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#FFFFFF"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#21C561"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#242424"); customTheme.navBarColor = Color.parseColor("#121212"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } public static CustomTheme getIndigoAmoled(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_indigo_amoled)); customTheme.isLightTheme = false; customTheme.isDarkTheme = false; customTheme.isAmoledTheme = true; customTheme.colorPrimary = Color.parseColor("#000000"); customTheme.colorPrimaryDark = Color.parseColor("#000000"); customTheme.colorAccent = Color.parseColor("#FF1868"); customTheme.colorPrimaryLightTheme = Color.parseColor("#0336FF"); customTheme.primaryTextColor = Color.parseColor("#FFFFFF"); customTheme.secondaryTextColor = Color.parseColor("#B3FFFFFF"); customTheme.postTitleColor = Color.parseColor("#FFFFFF"); customTheme.postContentColor = Color.parseColor("#B3FFFFFF"); customTheme.readPostTitleColor = Color.parseColor("#979797"); customTheme.readPostContentColor = Color.parseColor("#979797"); customTheme.commentColor = Color.parseColor("#FFFFFF"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#000000"); customTheme.cardViewBackgroundColor = Color.parseColor("#000000"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.commentBackgroundColor = Color.parseColor("#000000"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#000000"); customTheme.primaryIconColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFFFFF"); customTheme.postIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.commentIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#000000"); customTheme.mediaIndicatorIconColor = Color.parseColor("#000000"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#000000"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#0336FF"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#0336FF"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#808080"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#0336FF"); customTheme.username = Color.parseColor("#1E88E5"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#123E77"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#123E77"); customTheme.dividerColor = Color.parseColor("#69666C"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#424242"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#3C3C3C"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#C300B3"); customTheme.commentVerticalBarColor3 = Color.parseColor("#00B8DA"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EDCA00"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0219"); customTheme.commentVerticalBarColor6 = Color.parseColor("#00B925"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#FFFFFF"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#21C561"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#000000"); customTheme.navBarColor = Color.parseColor("#000000"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } private static CustomTheme getWhite(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_white)); customTheme.isLightTheme = true; customTheme.isDarkTheme = false; customTheme.isAmoledTheme = false; customTheme.colorPrimary = Color.parseColor("#FFFFFF"); customTheme.colorPrimaryDark = Color.parseColor("#FFFFFF"); customTheme.colorAccent = Color.parseColor("#000000"); customTheme.colorPrimaryLightTheme = Color.parseColor("#000000"); customTheme.primaryTextColor = Color.parseColor("#000000"); customTheme.secondaryTextColor = Color.parseColor("#8A000000"); customTheme.postTitleColor = Color.parseColor("#000000"); customTheme.postContentColor = Color.parseColor("#8A000000"); customTheme.readPostTitleColor = Color.parseColor("#9D9D9D"); customTheme.readPostContentColor = Color.parseColor("#9D9D9D"); customTheme.commentColor = Color.parseColor("#000000"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#FFFFFF"); customTheme.cardViewBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#F5F5F5"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#E6F4FF"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#F5F5F5"); customTheme.commentBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.primaryIconColor = Color.parseColor("#000000"); customTheme.bottomAppBarIconColor = Color.parseColor("#000000"); customTheme.postIconAndInfoColor = Color.parseColor("#3C4043"); customTheme.commentIconAndInfoColor = Color.parseColor("#3C4043"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#3C4043"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#3C4043"); customTheme.circularProgressBarBackground = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorIconColor = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#3C4043"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#3C4043"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#3C4043"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#3C4043"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#002BF0"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#002BF0"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#FFFFFF"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#002BF0"); customTheme.username = Color.parseColor("#002BF0"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#B3E5F9"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#B3E5F9"); customTheme.dividerColor = Color.parseColor("#E0E0E0"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#000000"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#F0F0F0"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#EE02BE"); customTheme.commentVerticalBarColor3 = Color.parseColor("#02DFEE"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EED502"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0220"); customTheme.commentVerticalBarColor6 = Color.parseColor("#02EE6E"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#FFFFFF"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#8EDFBA"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.navBarColor = Color.parseColor("#FFFFFF"); customTheme.isLightStatusBar = true; customTheme.isLightNavBar = true; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } private static CustomTheme getWhiteDark(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_white_dark)); customTheme.isLightTheme = false; customTheme.isDarkTheme = true; customTheme.isAmoledTheme = false; customTheme.colorPrimary = Color.parseColor("#242424"); customTheme.colorPrimaryDark = Color.parseColor("#121212"); customTheme.colorAccent = Color.parseColor("#FFFFFF"); customTheme.colorPrimaryLightTheme = Color.parseColor("#121212"); customTheme.primaryTextColor = Color.parseColor("#FFFFFF"); customTheme.secondaryTextColor = Color.parseColor("#B3FFFFFF"); customTheme.postTitleColor = Color.parseColor("#FFFFFF"); customTheme.postContentColor = Color.parseColor("#B3FFFFFF"); customTheme.readPostTitleColor = Color.parseColor("#979797"); customTheme.readPostContentColor = Color.parseColor("#979797"); customTheme.commentColor = Color.parseColor("#FFFFFF"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#121212"); customTheme.cardViewBackgroundColor = Color.parseColor("#242424"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#101010"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#242424"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#101010"); customTheme.commentBackgroundColor = Color.parseColor("#242424"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#121212"); customTheme.primaryIconColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFFFFF"); customTheme.postIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.commentIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#242424"); customTheme.mediaIndicatorIconColor = Color.parseColor("#000000"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#242424"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#242424"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#0336FF"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#0336FF"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#FFFFFF"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#0336FF"); customTheme.username = Color.parseColor("#1E88E5"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#123E77"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#123E77"); customTheme.dividerColor = Color.parseColor("#69666C"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#000000"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#3C3C3C"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#C300B3"); customTheme.commentVerticalBarColor3 = Color.parseColor("#00B8DA"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EDCA00"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0219"); customTheme.commentVerticalBarColor6 = Color.parseColor("#00B925"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#000000"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#21C561"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#242424"); customTheme.navBarColor = Color.parseColor("#121212"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } private static CustomTheme getWhiteAmoled(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_white_amoled)); customTheme.isLightTheme = false; customTheme.isDarkTheme = false; customTheme.isAmoledTheme = true; customTheme.colorPrimary = Color.parseColor("#000000"); customTheme.colorPrimaryDark = Color.parseColor("#000000"); customTheme.colorAccent = Color.parseColor("#FFFFFF"); customTheme.colorPrimaryLightTheme = Color.parseColor("#000000"); customTheme.primaryTextColor = Color.parseColor("#FFFFFF"); customTheme.secondaryTextColor = Color.parseColor("#B3FFFFFF"); customTheme.postTitleColor = Color.parseColor("#FFFFFF"); customTheme.postContentColor = Color.parseColor("#B3FFFFFF"); customTheme.readPostTitleColor = Color.parseColor("#979797"); customTheme.readPostContentColor = Color.parseColor("#979797"); customTheme.commentColor = Color.parseColor("#FFFFFF"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#000000"); customTheme.cardViewBackgroundColor = Color.parseColor("#000000"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.commentBackgroundColor = Color.parseColor("#000000"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#000000"); customTheme.primaryIconColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFFFFF"); customTheme.postIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.commentIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#000000"); customTheme.mediaIndicatorIconColor = Color.parseColor("#000000"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#000000"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#0336FF"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#0336FF"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#FFFFFF"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#0336FF"); customTheme.username = Color.parseColor("#1E88E5"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#123E77"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#123E77"); customTheme.dividerColor = Color.parseColor("#69666C"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#000000"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#3C3C3C"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#C300B3"); customTheme.commentVerticalBarColor3 = Color.parseColor("#00B8DA"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EDCA00"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0219"); customTheme.commentVerticalBarColor6 = Color.parseColor("#00B925"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#000000"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#21C561"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#000000"); customTheme.navBarColor = Color.parseColor("#000000"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } private static CustomTheme getRed(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_red)); customTheme.isLightTheme = true; customTheme.isDarkTheme = false; customTheme.isAmoledTheme = false; customTheme.colorPrimary = Color.parseColor("#EE0270"); customTheme.colorPrimaryDark = Color.parseColor("#C60466"); customTheme.colorAccent = Color.parseColor("#02EE80"); customTheme.colorPrimaryLightTheme = Color.parseColor("#EE0270"); customTheme.primaryTextColor = Color.parseColor("#000000"); customTheme.secondaryTextColor = Color.parseColor("#8A000000"); customTheme.postTitleColor = Color.parseColor("#000000"); customTheme.postContentColor = Color.parseColor("#8A000000"); customTheme.readPostTitleColor = Color.parseColor("#9D9D9D"); customTheme.readPostContentColor = Color.parseColor("#9D9D9D"); customTheme.commentColor = Color.parseColor("#000000"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#FFFFFF"); customTheme.cardViewBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#F5F5F5"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#FFE9F3"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#F5F5F5"); customTheme.commentBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.primaryIconColor = Color.parseColor("#000000"); customTheme.bottomAppBarIconColor = Color.parseColor("#000000"); customTheme.postIconAndInfoColor = Color.parseColor("#8A000000"); customTheme.commentIconAndInfoColor = Color.parseColor("#8A000000"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorIconColor = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#EE0270"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#EE0270"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#EE0270"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#002BF0"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#002BF0"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#808080"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#002BF0"); customTheme.username = Color.parseColor("#002BF0"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#B3E5F9"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#B3E5F9"); customTheme.dividerColor = Color.parseColor("#E0E0E0"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#E0E0E0"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#F0F0F0"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#EE02BE"); customTheme.commentVerticalBarColor3 = Color.parseColor("#02DFEE"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EED502"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0220"); customTheme.commentVerticalBarColor6 = Color.parseColor("#02EE6E"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#FFFFFF"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#8EDFBA"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.navBarColor = Color.parseColor("#FFFFFF"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = true; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = true; return customTheme; } private static CustomTheme getRedDark(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_red_dark)); customTheme.isLightTheme = false; customTheme.isDarkTheme = true; customTheme.isAmoledTheme = false; customTheme.colorPrimary = Color.parseColor("#242424"); customTheme.colorPrimaryDark = Color.parseColor("#121212"); customTheme.colorAccent = Color.parseColor("#02EE80"); customTheme.colorPrimaryLightTheme = Color.parseColor("#EE0270"); customTheme.primaryTextColor = Color.parseColor("#FFFFFF"); customTheme.secondaryTextColor = Color.parseColor("#B3FFFFFF"); customTheme.postTitleColor = Color.parseColor("#FFFFFF"); customTheme.postContentColor = Color.parseColor("#B3FFFFFF"); customTheme.readPostTitleColor = Color.parseColor("#979797"); customTheme.readPostContentColor = Color.parseColor("#979797"); customTheme.commentColor = Color.parseColor("#FFFFFF"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#121212"); customTheme.cardViewBackgroundColor = Color.parseColor("#242424"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#101010"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#242424"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#101010"); customTheme.commentBackgroundColor = Color.parseColor("#242424"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#121212"); customTheme.primaryIconColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFFFFF"); customTheme.postIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.commentIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#242424"); customTheme.mediaIndicatorIconColor = Color.parseColor("#000000"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#242424"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#242424"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#0336FF"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#0336FF"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#808080"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#0336FF"); customTheme.username = Color.parseColor("#1E88E5"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#123E77"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#123E77"); customTheme.dividerColor = Color.parseColor("#69666C"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#424242"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#3C3C3C"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#C300B3"); customTheme.commentVerticalBarColor3 = Color.parseColor("#00B8DA"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EDCA00"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0219"); customTheme.commentVerticalBarColor6 = Color.parseColor("#00B925"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#FFFFFF"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#21C561"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#242424"); customTheme.navBarColor = Color.parseColor("#121212"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } private static CustomTheme getRedAmoled(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_red_amoled)); customTheme.isLightTheme = false; customTheme.isDarkTheme = false; customTheme.isAmoledTheme = true; customTheme.colorPrimary = Color.parseColor("#000000"); customTheme.colorPrimaryDark = Color.parseColor("#000000"); customTheme.colorAccent = Color.parseColor("#02EE80"); customTheme.colorPrimaryLightTheme = Color.parseColor("#EE0270"); customTheme.primaryTextColor = Color.parseColor("#FFFFFF"); customTheme.secondaryTextColor = Color.parseColor("#B3FFFFFF"); customTheme.postTitleColor = Color.parseColor("#FFFFFF"); customTheme.postContentColor = Color.parseColor("#B3FFFFFF"); customTheme.readPostTitleColor = Color.parseColor("#979797"); customTheme.readPostContentColor = Color.parseColor("#979797"); customTheme.commentColor = Color.parseColor("#FFFFFF"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#000000"); customTheme.cardViewBackgroundColor = Color.parseColor("#000000"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#000000"); customTheme.commentBackgroundColor = Color.parseColor("#000000"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#000000"); customTheme.primaryIconColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFFFFF"); customTheme.postIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.commentIconAndInfoColor = Color.parseColor("#B3FFFFFF"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#000000"); customTheme.mediaIndicatorIconColor = Color.parseColor("#000000"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#000000"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#0336FF"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#0336FF"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#808080"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#0336FF"); customTheme.username = Color.parseColor("#1E88E5"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#123E77"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#123E77"); customTheme.dividerColor = Color.parseColor("#69666C"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#424242"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#3C3C3C"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#C300B3"); customTheme.commentVerticalBarColor3 = Color.parseColor("#00B8DA"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EDCA00"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0219"); customTheme.commentVerticalBarColor6 = Color.parseColor("#00B925"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#FFFFFF"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#21C561"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#000000"); customTheme.navBarColor = Color.parseColor("#000000"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } private static CustomTheme getDracula(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_dracula)); customTheme.isLightTheme = true; customTheme.isDarkTheme = true; customTheme.isAmoledTheme = true; customTheme.colorPrimary = Color.parseColor("#393A59"); customTheme.colorPrimaryDark = Color.parseColor("#393A59"); customTheme.colorAccent = Color.parseColor("#F8F8F2"); customTheme.colorPrimaryLightTheme = Color.parseColor("#393A59"); customTheme.primaryTextColor = Color.parseColor("#FFFFFF"); customTheme.secondaryTextColor = Color.parseColor("#B3FFFFFF"); customTheme.postTitleColor = Color.parseColor("#FFFFFF"); customTheme.postContentColor = Color.parseColor("#B3FFFFFF"); customTheme.readPostTitleColor = Color.parseColor("#9D9D9D"); customTheme.readPostContentColor = Color.parseColor("#9D9D9D"); customTheme.commentColor = Color.parseColor("#FFFFFF"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#282A36"); customTheme.cardViewBackgroundColor = Color.parseColor("#393A59"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#1C1F3D"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#393A59"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#1C1F3D"); customTheme.commentBackgroundColor = Color.parseColor("#393A59"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#393A59"); customTheme.primaryIconColor = Color.parseColor("#FFFFFF"); customTheme.bottomAppBarIconColor = Color.parseColor("#FFFFFF"); customTheme.postIconAndInfoColor = Color.parseColor("#FFFFFF"); customTheme.commentIconAndInfoColor = Color.parseColor("#FFFFFF"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#FFFFFF"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#FFFFFF"); customTheme.circularProgressBarBackground = Color.parseColor("#393A59"); customTheme.mediaIndicatorIconColor = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#393A59"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#393A59"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#FFFFFF"); customTheme.upvoted = Color.parseColor("#FF008C"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#0336FF"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#02ABEE"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#FFFFFF"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#002BF0"); customTheme.username = Color.parseColor("#1E88E5"); customTheme.subreddit = Color.parseColor("#FF4B9C"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#5F5B85"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#5F5B85"); customTheme.dividerColor = Color.parseColor("#69666C"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#6272A4"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#777C82"); customTheme.commentVerticalBarColor1 = Color.parseColor("#8BE9FD"); customTheme.commentVerticalBarColor2 = Color.parseColor("#50FA7B"); customTheme.commentVerticalBarColor3 = Color.parseColor("#FFB86C"); customTheme.commentVerticalBarColor4 = Color.parseColor("#FF79C6"); customTheme.commentVerticalBarColor5 = Color.parseColor("#BD93F9"); customTheme.commentVerticalBarColor6 = Color.parseColor("#FF5555"); customTheme.commentVerticalBarColor7 = Color.parseColor("#F1FA8C"); customTheme.fabIconColor = Color.parseColor("#000000"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#21C561"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#393A59"); customTheme.navBarColor = Color.parseColor("#393A59"); customTheme.isLightStatusBar = false; customTheme.isLightNavBar = false; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } private static CustomTheme getCalmPastel(Context context) { CustomTheme customTheme = new CustomTheme(context.getString(R.string.theme_name_calm_pastel)); customTheme.isLightTheme = true; customTheme.isDarkTheme = false; customTheme.isAmoledTheme = false; customTheme.colorPrimary = Color.parseColor("#D48AE0"); customTheme.colorPrimaryDark = Color.parseColor("#D476E0"); customTheme.colorAccent = Color.parseColor("#775EFF"); customTheme.colorPrimaryLightTheme = Color.parseColor("#D48AE0"); customTheme.primaryTextColor = Color.parseColor("#000000"); customTheme.secondaryTextColor = Color.parseColor("#8A000000"); customTheme.postTitleColor = Color.parseColor("#000000"); customTheme.postContentColor = Color.parseColor("#8A000000"); customTheme.readPostTitleColor = Color.parseColor("#979797"); customTheme.readPostContentColor = Color.parseColor("#979797"); customTheme.commentColor = Color.parseColor("#000000"); customTheme.buttonTextColor = Color.parseColor("#FFFFFF"); customTheme.backgroundColor = Color.parseColor("#DAD0DE"); customTheme.cardViewBackgroundColor = Color.parseColor("#C0F0F4"); customTheme.readPostCardViewBackgroundColor = Color.parseColor("#D2E7EA"); customTheme.filledCardViewBackgroundColor = Color.parseColor("#C0F0F4"); customTheme.readPostFilledCardViewBackgroundColor = Color.parseColor("#D2E7EA"); customTheme.commentBackgroundColor = Color.parseColor("#C0F0F4"); customTheme.bottomAppBarBackgroundColor = Color.parseColor("#D48AE0"); customTheme.primaryIconColor = Color.parseColor("#000000"); customTheme.bottomAppBarIconColor = Color.parseColor("#000000"); customTheme.postIconAndInfoColor = Color.parseColor("#000000"); customTheme.commentIconAndInfoColor = Color.parseColor("#000000"); customTheme.toolbarPrimaryTextAndIconColor = Color.parseColor("#3C4043"); customTheme.toolbarSecondaryTextColor = Color.parseColor("#3C4043"); customTheme.circularProgressBarBackground = Color.parseColor("#D48AE0"); customTheme.mediaIndicatorIconColor = Color.parseColor("#FFFFFF"); customTheme.mediaIndicatorBackgroundColor = Color.parseColor("#000000"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = Color.parseColor("#FFFFFF"); customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = Color.parseColor("#D48AE0"); customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = Color.parseColor("#D48AE0"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = Color.parseColor("#D48AE0"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = Color.parseColor("#3C4043"); customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = Color.parseColor("#3C4043"); customTheme.upvoted = Color.parseColor("#FF1868"); customTheme.downvoted = Color.parseColor("#007DDE"); customTheme.postTypeBackgroundColor = Color.parseColor("#002BF0"); customTheme.postTypeTextColor = Color.parseColor("#FFFFFF"); customTheme.spoilerBackgroundColor = Color.parseColor("#EE02EB"); customTheme.spoilerTextColor = Color.parseColor("#FFFFFF"); customTheme.nsfwBackgroundColor = Color.parseColor("#FF1868"); customTheme.nsfwTextColor = Color.parseColor("#FFFFFF"); customTheme.flairBackgroundColor = Color.parseColor("#00AA8C"); customTheme.flairTextColor = Color.parseColor("#FFFFFF"); customTheme.awardsBackgroundColor = Color.parseColor("#EEAB02"); customTheme.awardsTextColor = Color.parseColor("#FFFFFF"); customTheme.archivedTint = Color.parseColor("#B4009F"); customTheme.lockedIconTint = Color.parseColor("#EE7302"); customTheme.crosspostIconTint = Color.parseColor("#FF1868"); customTheme.upvoteRatioIconTint = Color.parseColor("#0256EE"); customTheme.stickiedPostIconTint = Color.parseColor("#002BF0"); customTheme.noPreviewPostTypeIconTint = Color.parseColor("#808080"); customTheme.subscribed = Color.parseColor("#FF1868"); customTheme.unsubscribed = Color.parseColor("#002BF0"); customTheme.username = Color.parseColor("#002BF0"); customTheme.subreddit = Color.parseColor("#FF1868"); customTheme.authorFlairTextColor = Color.parseColor("#EE02C4"); customTheme.submitter = Color.parseColor("#EE8A02"); customTheme.moderator = Color.parseColor("#00BA81"); customTheme.currentUser = Color.parseColor("#00D5EA"); customTheme.singleCommentThreadBackgroundColor = Color.parseColor("#25D5E5"); customTheme.unreadMessageBackgroundColor = Color.parseColor("#25D5E5"); customTheme.dividerColor = Color.parseColor("#E0E0E0"); customTheme.noPreviewPostTypeBackgroundColor = Color.parseColor("#E0E0E0"); customTheme.voteAndReplyUnavailableButtonColor = Color.parseColor("#F0F0F0"); customTheme.commentVerticalBarColor1 = Color.parseColor("#0336FF"); customTheme.commentVerticalBarColor2 = Color.parseColor("#EE02BE"); customTheme.commentVerticalBarColor3 = Color.parseColor("#02DFEE"); customTheme.commentVerticalBarColor4 = Color.parseColor("#EED502"); customTheme.commentVerticalBarColor5 = Color.parseColor("#EE0220"); customTheme.commentVerticalBarColor6 = Color.parseColor("#02EE6E"); customTheme.commentVerticalBarColor7 = Color.parseColor("#EE4602"); customTheme.fabIconColor = Color.parseColor("#000000"); customTheme.chipTextColor = Color.parseColor("#FFFFFF"); customTheme.linkColor = Color.parseColor("#FF1868"); customTheme.receivedMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.sentMessageTextColor = Color.parseColor("#FFFFFF"); customTheme.receivedMessageBackgroundColor = Color.parseColor("#4185F4"); customTheme.sentMessageBackgroundColor = Color.parseColor("#31BF7D"); customTheme.sendMessageIconColor = Color.parseColor("#4185F4"); customTheme.fullyCollapsedCommentBackgroundColor = Color.parseColor("#8EDFBA"); customTheme.awardedCommentBackgroundColor = Color.parseColor("#C0F0F4"); customTheme.navBarColor = Color.parseColor("#D48AE0"); customTheme.isLightStatusBar = true; customTheme.isLightNavBar = true; customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = false; return customTheme; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/CustomThemeWrapperReceiver.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; public interface CustomThemeWrapperReceiver { void setCustomThemeWrapper(CustomThemeWrapper customThemeWrapper); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/LocalCustomThemeRepository.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import androidx.lifecycle.LiveData; import java.util.List; import kotlinx.coroutines.flow.Flow; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class LocalCustomThemeRepository { private final LiveData> mAllCustomThemes; private final LiveData mCurrentLightCustomTheme; private final LiveData mCurrentDarkCustomTheme; private final LiveData mCurrentAmoledCustomTheme; private final Flow mCurrentLightCustomThemeFlow; private final Flow mCurrentDarkCustomThemeFlow; private final Flow mCurrentAmoledCustomThemeFlow; public LocalCustomThemeRepository(RedditDataRoomDatabase redditDataRoomDatabase) { mAllCustomThemes = redditDataRoomDatabase.customThemeDao().getAllCustomThemes(); mCurrentLightCustomTheme = redditDataRoomDatabase.customThemeDao().getLightCustomThemeLiveData(); mCurrentDarkCustomTheme = redditDataRoomDatabase.customThemeDao().getDarkCustomThemeLiveData(); mCurrentAmoledCustomTheme = redditDataRoomDatabase.customThemeDao().getAmoledCustomThemeLiveData(); mCurrentLightCustomThemeFlow = redditDataRoomDatabase.customThemeDaoKt().getLightCustomThemeFlow(); mCurrentDarkCustomThemeFlow = redditDataRoomDatabase.customThemeDaoKt().getDarkCustomThemeFlow(); mCurrentAmoledCustomThemeFlow = redditDataRoomDatabase.customThemeDaoKt().getAmoledCustomThemeFlow(); } LiveData> getAllCustomThemes() { return mAllCustomThemes; } LiveData getCurrentLightCustomTheme() { return mCurrentLightCustomTheme; } LiveData getCurrentDarkCustomTheme() { return mCurrentDarkCustomTheme; } LiveData getCurrentAmoledCustomTheme() { return mCurrentAmoledCustomTheme; } public Flow getCurrentLightCustomThemeFlow() { return mCurrentLightCustomThemeFlow; } public Flow getCurrentDarkCustomThemeFlow() { return mCurrentDarkCustomThemeFlow; } public Flow getCurrentAmoledCustomThemeFlow() { return mCurrentAmoledCustomThemeFlow; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/OnlineCustomThemeFilter.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; public class OnlineCustomThemeFilter { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/OnlineCustomThemeMetadata.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import com.google.gson.Gson; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; public class OnlineCustomThemeMetadata implements Parcelable { public int id; public String name; public String username; @SerializedName("primary_color") public String colorPrimary; protected OnlineCustomThemeMetadata(Parcel in) { id = in.readInt(); name = in.readString(); username = in.readString(); colorPrimary = in.readString(); } public static final Creator CREATOR = new Creator<>() { @Override public OnlineCustomThemeMetadata createFromParcel(Parcel in) { return new OnlineCustomThemeMetadata(in); } @Override public OnlineCustomThemeMetadata[] newArray(int size) { return new OnlineCustomThemeMetadata[size]; } }; public static OnlineCustomThemeMetadata fromJson(String json) throws JsonParseException { Gson gson = new Gson(); return gson.fromJson(json, OnlineCustomThemeMetadata.class); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(id); dest.writeString(name); dest.writeString(username); dest.writeString(colorPrimary); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/OnlineCustomThemePagingSource.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.paging.ListenableFuturePagingSource; import androidx.paging.PagingState; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.JsonParseException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.ServerAPI; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.HttpException; import retrofit2.Response; import retrofit2.Retrofit; public class OnlineCustomThemePagingSource extends ListenableFuturePagingSource { private final Executor executor; private final ServerAPI api; private final RedditDataRoomDatabase redditDataRoomDatabase; public OnlineCustomThemePagingSource(Executor executor, Retrofit onlineCustomThemesRetrofit, RedditDataRoomDatabase redditDataRoomDatabase) { this.executor = executor; this.redditDataRoomDatabase = redditDataRoomDatabase; api = onlineCustomThemesRetrofit.create(ServerAPI.class); } @Nullable @Override public String getRefreshKey(@NonNull PagingState pagingState) { return null; } @NonNull @Override public ListenableFuture> loadFuture(@NonNull LoadParams loadParams) { ListenableFuture> customThemes; customThemes = api.getCustomThemesListenableFuture(loadParams.getKey()); ListenableFuture> pageFuture = Futures.transform(customThemes, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } public LoadResult transformData(Response response) { if (response.isSuccessful()) { List themeMetadataList = new ArrayList<>(); try { String responseString = response.body(); JSONObject data = new JSONObject(responseString); int page = data.getInt(JSONUtils.PAGE_KEY); JSONArray themesArray = data.getJSONArray(JSONUtils.DATA_KEY); for (int i = 0; i < themesArray.length(); i++) { try { themeMetadataList.add(OnlineCustomThemeMetadata.fromJson(themesArray.getJSONObject(i).toString())); } catch (JsonParseException ignore) { } } if (themeMetadataList.isEmpty()) { return new LoadResult.Page<>(themeMetadataList, null, null); } else { return new LoadResult.Page<>(themeMetadataList, null, Integer.toString(page + 1)); } } catch (JSONException e) { return new LoadResult.Error<>(new Exception("Response failed")); } } else { return new LoadResult.Error<>(new Exception("Response failed")); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customtheme/OnlineCustomThemeRepository.java ================================================ package ml.docilealligator.infinityforreddit.customtheme; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.paging.Pager; import androidx.paging.PagingConfig; import androidx.paging.PagingData; import androidx.paging.PagingLiveData; import java.util.concurrent.Executor; import kotlinx.coroutines.CoroutineScope; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import retrofit2.Retrofit; public class OnlineCustomThemeRepository { private final LiveData> customThemes; private MutableLiveData onlineCustomThemeFilterMutableLiveData; public OnlineCustomThemeRepository(Executor executor, Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase, CoroutineScope viewModelScope) { onlineCustomThemeFilterMutableLiveData = new MutableLiveData<>(new OnlineCustomThemeFilter()); Pager pager = new Pager<>(new PagingConfig(25, 4, false, 10), () -> new OnlineCustomThemePagingSource(executor, retrofit, redditDataRoomDatabase)); customThemes = PagingLiveData.cachedIn(Transformations.switchMap(onlineCustomThemeFilterMutableLiveData, customThemeFilter -> PagingLiveData.getLiveData(pager)), viewModelScope); } public LiveData> getOnlineCustomThemeMetadata() { return customThemes; } public void changeOnlineCustomThemeFilter(OnlineCustomThemeFilter onlineCustomThemeFilter) { onlineCustomThemeFilterMutableLiveData.postValue(onlineCustomThemeFilter); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/AdjustableTouchSlopItemTouchHelper.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.animation.Animator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Build; import android.util.Log; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.view.GestureDetectorCompat; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchUIUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import ml.docilealligator.infinityforreddit.R; public class AdjustableTouchSlopItemTouchHelper extends RecyclerView.ItemDecoration implements RecyclerView.OnChildAttachStateChangeListener { /** * Up direction, used for swipe & drag control. */ public static final int UP = 1; /** * Down direction, used for swipe & drag control. */ public static final int DOWN = 1 << 1; /** * Left direction, used for swipe & drag control. */ public static final int LEFT = 1 << 2; /** * Right direction, used for swipe & drag control. */ public static final int RIGHT = 1 << 3; // If you change these relative direction values, update Callback#convertToAbsoluteDirection, // Callback#convertToRelativeDirection. /** * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout * direction. Used for swipe & drag control. */ public static final int START = LEFT << 2; /** * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout * direction. Used for swipe & drag control. */ public static final int END = RIGHT << 2; /** * ItemTouchHelper is in idle state. At this state, either there is no related motion event by * the user or latest motion events have not yet triggered a swipe or drag. */ public static final int ACTION_STATE_IDLE = 0; /** * A View is currently being swiped. */ @SuppressWarnings("WeakerAccess") public static final int ACTION_STATE_SWIPE = 1; /** * A View is currently being dragged. */ @SuppressWarnings("WeakerAccess") public static final int ACTION_STATE_DRAG = 2; /** * Animation type for views which are swiped successfully. */ @SuppressWarnings("WeakerAccess") public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1; /** * Animation type for views which are not completely swiped thus will animate back to their * original position. */ @SuppressWarnings("WeakerAccess") public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2; /** * Animation type for views that were dragged and now will animate to their final position. */ @SuppressWarnings("WeakerAccess") public static final int ANIMATION_TYPE_DRAG = 1 << 3; private static final String TAG = "ItemTouchHelper"; private static final boolean DEBUG = false; private static final int ACTIVE_POINTER_ID_NONE = -1; static final int DIRECTION_FLAG_COUNT = 8; private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT; /** * The unit we are using to track velocity */ private static final int PIXELS_PER_SECOND = 1000; /** * Views, whose state should be cleared after they are detached from RecyclerView. * This is necessary after swipe dismissing an item. We wait until animator finishes its job * to clean these views. */ final List mPendingCleanup = new ArrayList<>(); /** * Re-use array to calculate dx dy for a ViewHolder */ private final float[] mTmpPosition = new float[2]; /** * Currently selected view holder */ @SuppressWarnings("WeakerAccess") /* synthetic access */ RecyclerView.ViewHolder mSelected = null; /** * The reference coordinates for the action start. For drag & drop, this is the time long * press is completed vs for swipe, this is the initial touch point. */ float mInitialTouchX; float mInitialTouchY; /** * Set when ItemTouchHelper is assigned to a RecyclerView. */ private float mSwipeEscapeVelocity; /** * Set when ItemTouchHelper is assigned to a RecyclerView. */ private float mMaxSwipeVelocity; /** * The diff between the last event and initial touch. */ float mDx; float mDy; /** * The coordinates of the selected view at the time it is selected. We record these values * when action starts so that we can consistently position it even if LayoutManager moves the * View. */ private float mSelectedStartX; private float mSelectedStartY; /** * The pointer we are tracking. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ int mActivePointerId = ACTIVE_POINTER_ID_NONE; /** * Developer callback which controls the behavior of */ @NonNull Callback mCallback; /** * Current mode. */ private int mActionState = ACTION_STATE_IDLE; /** * The direction flags obtained from unmasking * {@link Callback#getAbsoluteMovementFlags(RecyclerView, RecyclerView.ViewHolder)} for the current * action state. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ int mSelectedFlags; /** * When a View is dragged or swiped and needs to go back to where it was, we create a Recover * Animation and animate it to its location using this custom Animator, instead of using * framework Animators. * Using framework animators has the side effect of clashing with ItemAnimator, creating * jumpy UIs. */ @VisibleForTesting List mRecoverAnimations = new ArrayList<>(); private int mSlop; RecyclerView mRecyclerView; /** * When user drags a view to the edge, we start scrolling the LayoutManager as long as View * is partially out of bounds. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mSelected != null && scrollIfNecessary()) { if (mSelected != null) { //it might be lost during scrolling moveIfNecessary(mSelected); } mRecyclerView.removeCallbacks(mScrollRunnable); ViewCompat.postOnAnimation(mRecyclerView, this); } } }; /** * Used for detecting fling swipe */ VelocityTracker mVelocityTracker; //re-used list for selecting a swap target private List mSwapTargets; //re used for for sorting swap targets private List mDistances; /** * If drag & drop is supported, we use child drawing order to bring them to front. */ private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; /** * This keeps a reference to the child dragged by the user. Even after user stops dragging, * until view reaches its final position (end of recover animation), we keep a reference so * that it can be drawn above other children. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ View mOverdrawChild = null; /** * We cache the position of the overdraw child to avoid recalculating it each time child * position callback is called. This value is invalidated whenever a child is attached or * detached. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ int mOverdrawChildPosition = -1; /** * Used to detect long press. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ GestureDetectorCompat mGestureDetector; /** * Callback for when long press occurs. */ private AdjustableTouchSlopItemTouchHelperGestureListener mItemTouchHelperGestureListener; private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { mGestureDetector.onTouchEvent(event); if (DEBUG) { Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); } final int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { mActivePointerId = event.getPointerId(0); mInitialTouchX = event.getX(); mInitialTouchY = event.getY(); obtainVelocityTracker(); if (mSelected == null) { final RecoverAnimation animation = findAnimation(event); if (animation != null) { mInitialTouchX -= animation.mX; mInitialTouchY -= animation.mY; endRecoverAnimation(animation.mViewHolder, true); if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { mCallback.clearView(mRecyclerView, animation.mViewHolder); } select(animation.mViewHolder, animation.mActionState); updateDxDy(event, mSelectedFlags, 0); } } } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mActivePointerId = ACTIVE_POINTER_ID_NONE; select(null, ACTION_STATE_IDLE); } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { // in a non scroll orientation, if distance change is above threshold, we // can select the item final int index = event.findPointerIndex(mActivePointerId); if (DEBUG) { Log.d(TAG, "pointer index " + index); } if (index >= 0) { checkSelectForSwipe(action, event, index); } } if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } return mSelected != null; } @Override public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { mGestureDetector.onTouchEvent(event); if (DEBUG) { Log.d(TAG, "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); } if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { return; } final int action = event.getActionMasked(); final int activePointerIndex = event.findPointerIndex(mActivePointerId); if (activePointerIndex >= 0) { checkSelectForSwipe(action, event, activePointerIndex); } RecyclerView.ViewHolder viewHolder = mSelected; if (viewHolder == null) { return; } switch (action) { case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position if (activePointerIndex >= 0) { updateDxDy(event, mSelectedFlags, activePointerIndex); moveIfNecessary(viewHolder); mRecyclerView.removeCallbacks(mScrollRunnable); mScrollRunnable.run(); mRecyclerView.invalidate(); } break; } case MotionEvent.ACTION_CANCEL: if (mVelocityTracker != null) { mVelocityTracker.clear(); } // fall through case MotionEvent.ACTION_UP: select(null, ACTION_STATE_IDLE); mActivePointerId = ACTIVE_POINTER_ID_NONE; break; case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = event.getActionIndex(); final int pointerId = event.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = event.getPointerId(newPointerIndex); updateDxDy(event, mSelectedFlags, pointerIndex); } break; } } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (!disallowIntercept) { return; } select(null, ACTION_STATE_IDLE); } }; /** * Temporary rect instance that is used when we need to lookup Item decorations. */ private Rect mTmpRect; /** * When user started to drag scroll. Reset when we don't scroll */ private long mDragScrollStartTimeInMs; /** * Creates an ItemTouchHelper that will work with the given Callback. *

* You can attach ItemTouchHelper to a RecyclerView via * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration, * an onItemTouchListener and a Child attach / detach listener to the RecyclerView. * * @param callback The Callback which controls the behavior of this touch helper. */ public AdjustableTouchSlopItemTouchHelper(@NonNull Callback callback) { mCallback = callback; } private static boolean hitTest(View child, float x, float y, float left, float top) { return x >= left && x <= left + child.getWidth() && y >= top && y <= top + child.getHeight(); } /** * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already * attached to a RecyclerView, it will first detach from the previous one. You can call this * method with {@code null} to detach it from the current RecyclerView. * * @param recyclerView The RecyclerView instance to which you want to add this helper or * {@code null} if you want to remove ItemTouchHelper from the current * RecyclerView. */ public void attachToRecyclerView(@Nullable RecyclerView recyclerView, float touchSlopCoefficient) { if (mRecyclerView == recyclerView) { return; // nothing to do } if (mRecyclerView != null) { destroyCallbacks(); } mRecyclerView = recyclerView; if (recyclerView != null) { final Resources resources = recyclerView.getResources(); mSwipeEscapeVelocity = resources .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); mMaxSwipeVelocity = resources .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity); setupCallbacks(touchSlopCoefficient); } } private void setupCallbacks(float touchSlopCoefficient) { ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); mSlop = (int) (vc.getScaledTouchSlop() * touchSlopCoefficient); mRecyclerView.addItemDecoration(this); mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); mRecyclerView.addOnChildAttachStateChangeListener(this); startGestureDetection(); } private void destroyCallbacks() { mRecyclerView.removeItemDecoration(this); mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); mRecyclerView.removeOnChildAttachStateChangeListener(this); // clean all attached final int recoverAnimSize = mRecoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); recoverAnimation.cancel(); mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); } mRecoverAnimations.clear(); mOverdrawChild = null; mOverdrawChildPosition = -1; releaseVelocityTracker(); stopGestureDetection(); } private void startGestureDetection() { mItemTouchHelperGestureListener = new AdjustableTouchSlopItemTouchHelperGestureListener(); mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(), mItemTouchHelperGestureListener); } private void stopGestureDetection() { if (mItemTouchHelperGestureListener != null) { mItemTouchHelperGestureListener.doNotReactToLongPress(); mItemTouchHelperGestureListener = null; } if (mGestureDetector != null) { mGestureDetector = null; } } private void getSelectedDxDy(float[] outPosition) { if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); } else { outPosition[0] = mSelected.itemView.getTranslationX(); } if ((mSelectedFlags & (UP | DOWN)) != 0) { outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); } else { outPosition[1] = mSelected.itemView.getTranslationY(); } } @Override public void onDrawOver( @NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state ) { float dx = 0, dy = 0; if (mSelected != null) { getSelectedDxDy(mTmpPosition); dx = mTmpPosition[0]; dy = mTmpPosition[1]; } mCallback.onDrawOver(c, parent, mSelected, mRecoverAnimations, mActionState, dx, dy); } @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { // we don't know if RV changed something so we should invalidate this index. mOverdrawChildPosition = -1; float dx = 0, dy = 0; if (mSelected != null) { getSelectedDxDy(mTmpPosition); dx = mTmpPosition[0]; dy = mTmpPosition[1]; } mCallback.onDraw(c, parent, mSelected, mRecoverAnimations, mActionState, dx, dy); } /** * Starts dragging or swiping the given View. Call with null if you want to clear it. * * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the * current action, but may not be null if actionState is ACTION_STATE_DRAG. * @param actionState The type of action */ @SuppressWarnings("WeakerAccess") /* synthetic access */ void select(@Nullable RecyclerView.ViewHolder selected, int actionState) { if (selected == mSelected && actionState == mActionState) { return; } mDragScrollStartTimeInMs = Long.MIN_VALUE; final int prevActionState = mActionState; // prevent duplicate animations endRecoverAnimation(selected, true); mActionState = actionState; if (actionState == ACTION_STATE_DRAG) { if (selected == null) { throw new IllegalArgumentException("Must pass a ViewHolder when dragging"); } // we remove after animation is complete. this means we only elevate the last drag // child but that should perform good enough as it is very hard to start dragging a // new child before the previous one settles. mOverdrawChild = selected.itemView; addChildDrawingOrderCallback(); } int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) - 1; boolean preventLayout = false; if (mSelected != null) { final RecyclerView.ViewHolder prevSelected = mSelected; if (prevSelected.itemView.getParent() != null) { final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 : swipeIfNecessary(prevSelected); releaseVelocityTracker(); // find where we should animate to final float targetTranslateX, targetTranslateY; int animationType; switch (swipeDir) { case LEFT: case RIGHT: case START: case END: targetTranslateY = 0; targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); break; case UP: case DOWN: targetTranslateX = 0; targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); break; default: targetTranslateX = 0; targetTranslateY = 0; } if (prevActionState == ACTION_STATE_DRAG) { animationType = ANIMATION_TYPE_DRAG; } else if (swipeDir > 0) { animationType = ANIMATION_TYPE_SWIPE_SUCCESS; } else { animationType = ANIMATION_TYPE_SWIPE_CANCEL; } getSelectedDxDy(mTmpPosition); final float currentTranslateX = mTmpPosition[0]; final float currentTranslateY = mTmpPosition[1]; final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, prevActionState, currentTranslateX, currentTranslateY, targetTranslateX, targetTranslateY) { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (this.mOverridden) { return; } if (swipeDir <= 0) { // this is a drag or failed swipe. recover immediately mCallback.clearView(mRecyclerView, prevSelected); // full cleanup will happen on onDrawOver } else { // wait until remove animation is complete. mPendingCleanup.add(prevSelected.itemView); mIsPendingCleanup = true; if (swipeDir > 0) { // Animation might be ended by other animators during a layout. // We defer callback to avoid editing adapter during a layout. postDispatchSwipe(this, swipeDir); } } // removed from the list after it is drawn for the last time if (mOverdrawChild == prevSelected.itemView) { removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); } } }; final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); rv.setDuration(duration); mRecoverAnimations.add(rv); rv.start(); preventLayout = true; } else { removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); mCallback.clearView(mRecyclerView, prevSelected); } mSelected = null; } if (selected != null) { mSelectedFlags = (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) >> (mActionState * DIRECTION_FLAG_COUNT); mSelectedStartX = selected.itemView.getLeft(); mSelectedStartY = selected.itemView.getTop(); mSelected = selected; if (actionState == ACTION_STATE_DRAG) { mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } } final ViewParent rvParent = mRecyclerView.getParent(); if (rvParent != null) { rvParent.requestDisallowInterceptTouchEvent(mSelected != null); } if (!preventLayout) { mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); } mCallback.onSelectedChanged(mSelected, mActionState); mRecyclerView.invalidate(); } @SuppressWarnings("WeakerAccess") /* synthetic access */ void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { // wait until animations are complete. mRecyclerView.post(new Runnable() { @Override public void run() { if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() && !anim.mOverridden && anim.mViewHolder.getAbsoluteAdapterPosition() != RecyclerView.NO_POSITION) { final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); // if animator is running or we have other active recover animations, we try // not to call onSwiped because DefaultItemAnimator is not good at merging // animations. Instead, we wait and batch. if ((animator == null || !animator.isRunning(null)) && !hasRunningRecoverAnim()) { mCallback.onSwiped(anim.mViewHolder, swipeDir); } else { mRecyclerView.post(this); } } } }); } @SuppressWarnings("WeakerAccess") /* synthetic access */ boolean hasRunningRecoverAnim() { final int size = mRecoverAnimations.size(); for (int i = 0; i < size; i++) { if (!mRecoverAnimations.get(i).mEnded) { return true; } } return false; } /** * If user drags the view to the edge, trigger a scroll if necessary. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ boolean scrollIfNecessary() { if (mSelected == null) { mDragScrollStartTimeInMs = Long.MIN_VALUE; return false; } final long now = System.currentTimeMillis(); final long scrollDuration = mDragScrollStartTimeInMs == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); if (mTmpRect == null) { mTmpRect = new Rect(); } int scrollX = 0; int scrollY = 0; lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); if (lm.canScrollHorizontally()) { int curX = (int) (mSelectedStartX + mDx); final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); if (mDx < 0 && leftDiff < 0) { scrollX = leftDiff; } else if (mDx > 0) { final int rightDiff = curX + mSelected.itemView.getWidth() + mTmpRect.right - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); if (rightDiff > 0) { scrollX = rightDiff; } } } if (lm.canScrollVertically()) { int curY = (int) (mSelectedStartY + mDy); final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); if (mDy < 0 && topDiff < 0) { scrollY = topDiff; } else if (mDy > 0) { final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); if (bottomDiff > 0) { scrollY = bottomDiff; } } } if (scrollX != 0) { scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, mSelected.itemView.getWidth(), scrollX, mRecyclerView.getWidth(), scrollDuration); } if (scrollY != 0) { scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, mSelected.itemView.getHeight(), scrollY, mRecyclerView.getHeight(), scrollDuration); } if (scrollX != 0 || scrollY != 0) { if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { mDragScrollStartTimeInMs = now; } mRecyclerView.scrollBy(scrollX, scrollY); return true; } mDragScrollStartTimeInMs = Long.MIN_VALUE; return false; } private List findSwapTargets(RecyclerView.ViewHolder viewHolder) { if (mSwapTargets == null) { mSwapTargets = new ArrayList<>(); mDistances = new ArrayList<>(); } else { mSwapTargets.clear(); mDistances.clear(); } final int margin = mCallback.getBoundingBoxMargin(); final int left = Math.round(mSelectedStartX + mDx) - margin; final int top = Math.round(mSelectedStartY + mDy) - margin; final int right = left + viewHolder.itemView.getWidth() + 2 * margin; final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; final int centerX = (left + right) / 2; final int centerY = (top + bottom) / 2; final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); final int childCount = lm.getChildCount(); for (int i = 0; i < childCount; i++) { View other = lm.getChildAt(i); if (other == viewHolder.itemView) { continue; //myself! } if (other.getBottom() < top || other.getTop() > bottom || other.getRight() < left || other.getLeft() > right) { continue; } final RecyclerView.ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { // find the index to add final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); final int dist = dx * dx + dy * dy; int pos = 0; final int cnt = mSwapTargets.size(); for (int j = 0; j < cnt; j++) { if (dist > mDistances.get(j)) { pos++; } else { break; } } mSwapTargets.add(pos, otherVh); mDistances.add(pos, dist); } } return mSwapTargets; } /** * Checks if we should swap w/ another view holder. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ void moveIfNecessary(RecyclerView.ViewHolder viewHolder) { if (mRecyclerView.isLayoutRequested()) { return; } if (mActionState != ACTION_STATE_DRAG) { return; } final float threshold = mCallback.getMoveThreshold(viewHolder); final int x = (int) (mSelectedStartX + mDx); final int y = (int) (mSelectedStartY + mDy); if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold && Math.abs(x - viewHolder.itemView.getLeft()) < viewHolder.itemView.getWidth() * threshold) { return; } List swapTargets = findSwapTargets(viewHolder); if (swapTargets.size() == 0) { return; } // may swap. RecyclerView.ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); if (target == null) { mSwapTargets.clear(); mDistances.clear(); return; } final int toPosition = target.getAbsoluteAdapterPosition(); final int fromPosition = viewHolder.getAbsoluteAdapterPosition(); if (mCallback.onMove(mRecyclerView, viewHolder, target)) { // keep target visible mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, target, toPosition, x, y); } } @Override public void onChildViewAttachedToWindow(@NonNull View view) { } @Override public void onChildViewDetachedFromWindow(@NonNull View view) { removeChildDrawingOrderCallbackIfNecessary(view); final RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); if (holder == null) { return; } if (mSelected != null && holder == mSelected) { select(null, ACTION_STATE_IDLE); } else { endRecoverAnimation(holder, false); // this may push it into pending cleanup list. if (mPendingCleanup.remove(holder.itemView)) { mCallback.clearView(mRecyclerView, holder); } } } /** * Returns the animation type or 0 if cannot be found. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ void endRecoverAnimation(RecyclerView.ViewHolder viewHolder, boolean override) { final int recoverAnimSize = mRecoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation anim = mRecoverAnimations.get(i); if (anim.mViewHolder == viewHolder) { anim.mOverridden |= override; if (!anim.mEnded) { anim.cancel(); } mRecoverAnimations.remove(i); return; } } } @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.setEmpty(); } @SuppressWarnings("WeakerAccess") /* synthetic access */ void obtainVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); } mVelocityTracker = VelocityTracker.obtain(); } private void releaseVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } private RecyclerView.ViewHolder findSwipedView(MotionEvent motionEvent) { final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { return null; } final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); if (absDx < mSlop && absDy < mSlop) { return null; } if (absDx > absDy && lm.canScrollHorizontally()) { return null; } else if (absDy > absDx && lm.canScrollVertically()) { return null; } View child = findChildView(motionEvent); if (child == null) { return null; } return mRecyclerView.getChildViewHolder(child); } /** * Checks whether we should select a View for swiping. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { if (mSelected != null || action != MotionEvent.ACTION_MOVE || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { return; } if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { return; } final RecyclerView.ViewHolder vh = findSwipedView(motionEvent); if (vh == null) { return; } final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); if (swipeFlags == 0) { return; } // mDx and mDy are only set in allowed directions. We use custom x/y here instead of // updateDxDy to avoid swiping if user moves more in the other direction final float x = motionEvent.getX(pointerIndex); final float y = motionEvent.getY(pointerIndex); // Calculate the distance moved final float dx = x - mInitialTouchX; final float dy = y - mInitialTouchY; // swipe target is chose w/o applying flags so it does not really check if swiping in that // direction is allowed. This why here, we use mDx mDy to check slope value again. final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); if (absDx < mSlop && absDy < mSlop) { return; } if (absDx > absDy) { if (dx < 0 && (swipeFlags & LEFT) == 0) { return; } if (dx > 0 && (swipeFlags & RIGHT) == 0) { return; } } else { if (dy < 0 && (swipeFlags & UP) == 0) { return; } if (dy > 0 && (swipeFlags & DOWN) == 0) { return; } } mDx = mDy = 0f; mActivePointerId = motionEvent.getPointerId(0); select(vh, ACTION_STATE_SWIPE); } @SuppressWarnings("WeakerAccess") /* synthetic access */ View findChildView(MotionEvent event) { // first check elevated views, if none, then call RV final float x = event.getX(); final float y = event.getY(); if (mSelected != null) { final View selectedView = mSelected.itemView; if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { return selectedView; } } for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { final RecoverAnimation anim = mRecoverAnimations.get(i); final View view = anim.mViewHolder.itemView; if (hitTest(view, x, y, anim.mX, anim.mY)) { return view; } } return mRecyclerView.findChildViewUnder(x, y); } /** * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a * View is long pressed. You can disable that behavior by overriding * {@link Callback#isLongPressDragEnabled()}. *

* For this method to work: *

    *
  • The provided ViewHolder must be a child of the RecyclerView to which this * ItemTouchHelper * is attached.
  • *
  • {@link Callback} must have dragging enabled.
  • *
  • There must be a previous touch event that was reported to the ItemTouchHelper * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener * grabs previous events, this should work as expected.
  • *
* * For example, if you would like to let your user to be able to drag an Item by touching one * of its descendants, you may implement it as follows: *
     *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
     *         public boolean onTouch(View v, MotionEvent event) {
     *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
     *                 mstartDrag(viewHolder);
     *             }
     *             return false;
     *         }
     *     });
     * 
*

* * @param viewHolder The ViewHolder to start dragging. It must be a direct child of * RecyclerView. * @see Callback#isItemViewSwipeEnabled() */ public void startDrag(@NonNull RecyclerView.ViewHolder viewHolder) { if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) { Log.e(TAG, "Start drag has been called but dragging is not enabled"); return; } if (viewHolder.itemView.getParent() != mRecyclerView) { Log.e(TAG, "Start drag has been called with a view holder which is not a child of " + "the RecyclerView which is controlled by this "); return; } obtainVelocityTracker(); mDx = mDy = 0f; select(viewHolder, ACTION_STATE_DRAG); } /** * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View * when user swipes their finger (or mouse pointer) over the View. You can disable this * behavior * by overriding {@link Callback} *

* For this method to work: *

    *
  • The provided ViewHolder must be a child of the RecyclerView to which this * ItemTouchHelper is attached.
  • *
  • {@link Callback} must have swiping enabled.
  • *
  • There must be a previous touch event that was reported to the ItemTouchHelper * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener * grabs previous events, this should work as expected.
  • *
* * For example, if you would like to let your user to be able to swipe an Item by touching one * of its descendants, you may implement it as follows: *
     *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
     *         public boolean onTouch(View v, MotionEvent event) {
     *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
     *                 mstartSwipe(viewHolder);
     *             }
     *             return false;
     *         }
     *     });
     * 
* * @param viewHolder The ViewHolder to start swiping. It must be a direct child of * RecyclerView. */ public void startSwipe(@NonNull RecyclerView.ViewHolder viewHolder) { if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { Log.e(TAG, "Start swipe has been called but swiping is not enabled"); return; } if (viewHolder.itemView.getParent() != mRecyclerView) { Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " + "the RecyclerView controlled by this "); return; } obtainVelocityTracker(); mDx = mDy = 0f; select(viewHolder, ACTION_STATE_SWIPE); } @SuppressWarnings("WeakerAccess") /* synthetic access */ RecoverAnimation findAnimation(MotionEvent event) { if (mRecoverAnimations.isEmpty()) { return null; } View target = findChildView(event); for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { final RecoverAnimation anim = mRecoverAnimations.get(i); if (anim.mViewHolder.itemView == target) { return anim; } } return null; } @SuppressWarnings("WeakerAccess") /* synthetic access */ void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { final float x = ev.getX(pointerIndex); final float y = ev.getY(pointerIndex); // Calculate the distance moved mDx = x - mInitialTouchX; mDy = y - mInitialTouchY; if ((directionFlags & LEFT) == 0) { mDx = Math.max(0, mDx); } if ((directionFlags & RIGHT) == 0) { mDx = Math.min(0, mDx); } if ((directionFlags & UP) == 0) { mDy = Math.max(0, mDy); } if ((directionFlags & DOWN) == 0) { mDy = Math.min(0, mDy); } } private int swipeIfNecessary(RecyclerView.ViewHolder viewHolder) { if (mActionState == ACTION_STATE_DRAG) { return 0; } final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( originalMovementFlags, ViewCompat.getLayoutDirection(mRecyclerView)); final int flags = (absoluteMovementFlags & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); if (flags == 0) { return 0; } final int originalFlags = (originalMovementFlags & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); int swipeDir; if (Math.abs(mDx) > Math.abs(mDy)) { if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { // if swipe dir is not in original flags, it should be the relative direction if ((originalFlags & swipeDir) == 0) { // convert to relative return Callback.convertToRelativeDirection(swipeDir, ViewCompat.getLayoutDirection(mRecyclerView)); } return swipeDir; } if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { return swipeDir; } } else { if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { return swipeDir; } if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { // if swipe dir is not in original flags, it should be the relative direction if ((originalFlags & swipeDir) == 0) { // convert to relative return Callback.convertToRelativeDirection(swipeDir, ViewCompat.getLayoutDirection(mRecyclerView)); } return swipeDir; } } return 0; } private int checkHorizontalSwipe(RecyclerView.ViewHolder viewHolder, int flags) { if ((flags & (LEFT | RIGHT)) != 0) { final int dirFlag = mDx > 0 ? RIGHT : LEFT; if (mVelocityTracker != null && mActivePointerId > -1) { mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; final float absXVelocity = Math.abs(xVelocity); if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) && absXVelocity > Math.abs(yVelocity)) { return velDirFlag; } } final float threshold = mRecyclerView.getWidth() * mCallback .getSwipeThreshold(viewHolder); if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { return dirFlag; } } return 0; } private int checkVerticalSwipe(RecyclerView.ViewHolder viewHolder, int flags) { if ((flags & (UP | DOWN)) != 0) { final int dirFlag = mDy > 0 ? DOWN : UP; if (mVelocityTracker != null && mActivePointerId > -1) { mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); final int velDirFlag = yVelocity > 0f ? DOWN : UP; final float absYVelocity = Math.abs(yVelocity); if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) && absYVelocity > Math.abs(xVelocity)) { return velDirFlag; } } final float threshold = mRecyclerView.getHeight() * mCallback .getSwipeThreshold(viewHolder); if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { return dirFlag; } } return 0; } private void addChildDrawingOrderCallback() { if (Build.VERSION.SDK_INT >= 21) { return; // we use elevation on Lollipop } if (mChildDrawingOrderCallback == null) { mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { @Override public int onGetChildDrawingOrder(int childCount, int i) { if (mOverdrawChild == null) { return i; } int childPosition = mOverdrawChildPosition; if (childPosition == -1) { childPosition = mRecyclerView.indexOfChild(mOverdrawChild); mOverdrawChildPosition = childPosition; } if (i == childCount - 1) { return childPosition; } return i < childPosition ? i : i + 1; } }; } mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); } @SuppressWarnings("WeakerAccess") /* synthetic access */ void removeChildDrawingOrderCallbackIfNecessary(View view) { if (view == mOverdrawChild) { mOverdrawChild = null; // only remove if we've added if (mChildDrawingOrderCallback != null) { mRecyclerView.setChildDrawingOrderCallback(null); } } } /** * An interface which can be implemented by LayoutManager for better integration with * {@link ItemTouchHelper}. */ public interface ViewDropHandler { /** * Called by the {@link ItemTouchHelper} after a View is dropped over another View. *

* A LayoutManager should implement this interface to get ready for the upcoming move * operation. *

* For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that * the View under drag will be used as an anchor View while calculating the next layout, * making layout stay consistent. * * @param view The View which is being dragged. It is very likely that user is still * dragging this View so there might be other calls to * {@code prepareForDrop()} after this one. * @param target The target view which is being dropped on. * @param x The left offset of the View that is being dragged. This value * includes the movement caused by the user. * @param y The top offset of the View that is being dragged. This value * includes the movement caused by the user. */ void prepareForDrop(@NonNull View view, @NonNull View target, int x, int y); } /** * This class is the contract between ItemTouchHelper and your application. It lets you control * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user * performs these actions. *

* To control which actions user can take on each view, you should override * {@link #getMovementFlags(RecyclerView, RecyclerView.ViewHolder)} and return appropriate set * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END}, * {@link #UP}, {@link #DOWN}). You can use * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use * {@link SimpleCallback}. *

* If user drags an item, ItemTouchHelper will call * {@link Callback#onMove(RecyclerView, RecyclerView.ViewHolder, RecyclerView.ViewHolder) * onMove(recyclerView, dragged, target)}. * Upon receiving this callback, you should move the item from the old position * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. * To control where a View can be dropped, you can override * {@link #canDropOver(RecyclerView, RecyclerView.ViewHolder, RecyclerView.ViewHolder)}. When a * dragging View overlaps multiple other views, Callback chooses the closest View with which * dragged View might have changed positions. Although this approach works for many use cases, * if you have a custom LayoutManager, you can override * {@link #chooseDropTarget(RecyclerView.ViewHolder, java.util.List, int, int)} to select a * custom drop target. *

* When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls * {@link #onSwiped(RecyclerView.ViewHolder, int)}. At this point, you should update your * adapter (e.g. remove the item) and call related Adapter#notify event. */ @SuppressWarnings("UnusedParameters") public abstract static class Callback { @SuppressWarnings("WeakerAccess") public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; @SuppressWarnings("WeakerAccess") public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; static final int RELATIVE_DIR_FLAGS = START | END | ((START | END) << DIRECTION_FLAG_COUNT) | ((START | END) << (2 * DIRECTION_FLAG_COUNT)); private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); private static final Interpolator sDragScrollInterpolator = new Interpolator() { @Override public float getInterpolation(float t) { return t * t * t * t * t; } }; private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { @Override public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; /** * Drag scroll speed keeps accelerating until this many milliseconds before being capped. */ private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; private int mCachedMaxScrollSpeed = -1; /** * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for * visual * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different * implementations for different platform versions. *

* By default, {@link Callback} applies these changes on * {@link RecyclerView.ViewHolder#itemView}. *

* For example, if you have a use case where you only want the text to move when user * swipes over the view, you can do the following: *

         *     public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){
         *         getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView);
         *     }
         *     public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
         *         if (viewHolder != null){
         *             getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView);
         *         }
         *     }
         *     public void onChildDraw(Canvas c, RecyclerView recyclerView,
         *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
         *             boolean isCurrentlyActive) {
         *         getDefaultUIUtil().onDraw(c, recyclerView,
         *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
         *                 actionState, isCurrentlyActive);
         *         return true;
         *     }
         *     public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
         *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
         *             boolean isCurrentlyActive) {
         *         getDefaultUIUtil().onDrawOver(c, recyclerView,
         *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
         *                 actionState, isCurrentlyActive);
         *         return true;
         *     }
         * 
* * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback} */ @SuppressWarnings("WeakerAccess") @NonNull public static ItemTouchUIUtil getDefaultUIUtil() { return ItemTouchUIUtilImpl.INSTANCE; } /** * Replaces a movement direction with its relative version by taking layout direction into * account. * * @param flags The flag value that include any number of movement flags. * @param layoutDirection The layout direction of the View. Can be obtained from * {@link ViewCompat#getLayoutDirection(android.view.View)}. * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead * of {@link #LEFT}, {@link #RIGHT}. * @see #convertToAbsoluteDirection(int, int) */ @SuppressWarnings("WeakerAccess") public static int convertToRelativeDirection(int flags, int layoutDirection) { int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; if (masked == 0) { return flags; // does not have any abs flags, good. } flags &= ~masked; //remove left / right. if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { // no change. just OR with 2 bits shifted mask and return flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. return flags; } else { // add RIGHT flag as START flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); // first clean RIGHT bit then add LEFT flag as END flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; } return flags; } /** * Convenience method to create movement flags. *

* For instance, if you want to let your items be drag & dropped vertically and swiped * left to be dismissed, you can call this method with: * makeMovementFlags(UP | DOWN, LEFT); * * @param dragFlags The directions in which the item can be dragged. * @param swipeFlags The directions in which the item can be swiped. * @return Returns an integer composed of the given drag and swipe flags. */ public static int makeMovementFlags(int dragFlags, int swipeFlags) { return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) | makeFlag(ACTION_STATE_SWIPE, swipeFlags) | makeFlag(ACTION_STATE_DRAG, dragFlags); } /** * Shifts the given direction flags to the offset of the given action state. * * @param actionState The action state you want to get flags in. Should be one of * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or * {@link #ACTION_STATE_DRAG}. * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. * @return And integer that represents the given directions in the provided actionState. */ @SuppressWarnings("WeakerAccess") public static int makeFlag(int actionState, int directions) { return directions << (actionState * DIRECTION_FLAG_COUNT); } /** * Should return a composite flag which defines the enabled move directions in each state * (idle, swiping, dragging). *

* Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, * int)} * or {@link #makeFlag(int, int)}. *

* This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next * 8 bits are for SWIPE state and third 8 bits are for DRAG state. * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in * {@link ItemTouchHelper}. *

* For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to * swipe by swiping RIGHT, you can return: *

         *      makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT);
         * 
* This means, allow right movement while IDLE and allow right and left movement while * swiping. * * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. * @param viewHolder The ViewHolder for which the movement information is necessary. * @return flags specifying which movements are allowed on this ViewHolder. * @see #makeMovementFlags(int, int) * @see #makeFlag(int, int) */ public abstract int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder); /** * Converts a given set of flags to absolution direction which means {@link #START} and * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout * direction. * * @param flags The flag value that include any number of movement flags. * @param layoutDirection The layout direction of the RecyclerView. * @return Updated flags which includes only absolute direction values. */ @SuppressWarnings("WeakerAccess") public int convertToAbsoluteDirection(int flags, int layoutDirection) { int masked = flags & RELATIVE_DIR_FLAGS; if (masked == 0) { return flags; // does not have any relative flags, good. } flags &= ~masked; //remove start / end if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { // no change. just OR with 2 bits shifted mask and return flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. return flags; } else { // add START flag as RIGHT flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); // first clean start bit then add END flag as LEFT flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; } return flags; } final int getAbsoluteMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { final int flags = getMovementFlags(recyclerView, viewHolder); return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView)); } boolean hasDragFlag(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); return (flags & ACTION_MODE_DRAG_MASK) != 0; } boolean hasSwipeFlag(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); return (flags & ACTION_MODE_SWIPE_MASK) != 0; } /** * Return true if the current ViewHolder can be dropped over the the target ViewHolder. *

* This method is used when selecting drop target for the dragged View. After Views are * eliminated either via bounds check or via this method, resulting set of views will be * passed to {@link #chooseDropTarget(RecyclerView.ViewHolder, java.util.List, int, int)}. *

* Default implementation returns true. * * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. * @param current The ViewHolder that user is dragging. * @param target The ViewHolder which is below the dragged ViewHolder. * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false * otherwise. */ @SuppressWarnings("WeakerAccess") public boolean canDropOver(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder current, @NonNull RecyclerView.ViewHolder target) { return true; } /** * Called when ItemTouchHelper wants to move the dragged item from its old position to * the new position. *

* If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved * to the adapter position of {@code target} ViewHolder * ({@link RecyclerView.ViewHolder#getAbsoluteAdapterPosition() * ViewHolder#getAdapterPositionInRecyclerView()}). *

* If you don't support drag & drop, this method will never be called. * * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. * @param viewHolder The ViewHolder which is being dragged by the user. * @param target The ViewHolder over which the currently active item is being * dragged. * @return True if the {@code viewHolder} has been moved to the adapter position of * {@code target}. * @see #onMoved(RecyclerView, RecyclerView.ViewHolder, int, RecyclerView.ViewHolder, int, int, int) */ public abstract boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target); /** * Returns whether ItemTouchHelper should start a drag and drop operation if an item is * long pressed. *

* Default value returns true but you may want to disable this if you want to start * dragging on a custom view touch using {@link #startDrag(RecyclerView.ViewHolder)}. * * @return True if ItemTouchHelper should start dragging an item when it is long pressed, * false otherwise. Default value is true. * @see #startDrag(RecyclerView.ViewHolder) */ public boolean isLongPressDragEnabled() { return true; } /** * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped * over the View. *

* Default value returns true but you may want to disable this if you want to start * swiping on a custom view touch using {@link #startSwipe(RecyclerView.ViewHolder)}. * * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer * over the View, false otherwise. Default value is true. * @see #startSwipe(RecyclerView.ViewHolder) */ public boolean isItemViewSwipeEnabled() { return true; } /** * When finding views under a dragged view, by default, ItemTouchHelper searches for views * that overlap with the dragged View. By overriding this method, you can extend or shrink * the search box. * * @return The extra margin to be added to the hit box of the dragged View. */ @SuppressWarnings("WeakerAccess") public int getBoundingBoxMargin() { return 0; } /** * Returns the fraction that the user should move the View to be considered as swiped. * The fraction is calculated with respect to RecyclerView's bounds. *

* Default value is .5f, which means, to swipe a View, user must move the View at least * half of RecyclerView's width or height, depending on the swipe direction. * * @param viewHolder The ViewHolder that is being dragged. * @return A float value that denotes the fraction of the View size. Default value * is .5f . */ @SuppressWarnings("WeakerAccess") public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return .5f; } /** * Returns the fraction that the user should move the View to be considered as it is * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views * below it for a possible drop. * * @param viewHolder The ViewHolder that is being dragged. * @return A float value that denotes the fraction of the View size. Default value is * .5f . */ @SuppressWarnings("WeakerAccess") public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return .5f; } /** * Defines the minimum velocity which will be considered as a swipe action by the user. *

* You can increase this value to make it harder to swipe or decrease it to make it easier. * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure * current direction velocity is larger then the perpendicular one. Otherwise, user's * movement is ambiguous. You can change the threshold by overriding * {@link #getSwipeVelocityThreshold(float)}. *

* The velocity is calculated in pixels per second. *

* The default framework value is passed as a parameter so that you can modify it with a * multiplier. * * @param defaultValue The default value (in pixels per second) used by the * * @return The minimum swipe velocity. The default implementation returns the * defaultValue parameter. * @see #getSwipeVelocityThreshold(float) * @see #getSwipeThreshold(RecyclerView.ViewHolder) */ @SuppressWarnings("WeakerAccess") public float getSwipeEscapeVelocity(float defaultValue) { return defaultValue; } /** * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements. *

* To consider a movement as swipe, ItemTouchHelper requires it to be larger than the * perpendicular movement. If both directions reach to the max threshold, none of them will * be considered as a swipe because it is usually an indication that user rather tried to * scroll then swipe. *

* The velocity is calculated in pixels per second. *

* You can customize this behavior by changing this method. If you increase the value, it * will be easier for the user to swipe diagonally and if you decrease the value, user will * need to make a rather straight finger movement to trigger a swipe. * * @param defaultValue The default value(in pixels per second) used by the * @return The velocity cap for pointer movements. The default implementation returns the * defaultValue parameter. * @see #getSwipeEscapeVelocity(float) */ @SuppressWarnings("WeakerAccess") public float getSwipeVelocityThreshold(float defaultValue) { return defaultValue; } /** * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that * are under the dragged View. *

* Default implementation filters the View with which dragged item have changed position * in the drag direction. For instance, if the view is dragged UP, it compares the * view.getTop() of the two views before and after drag started. If that value * is different, the target view passes the filter. *

* Among these Views which pass the test, the one closest to the dragged view is chosen. *

* This method is called on the main thread every time user moves the View. If you want to * override it, make sure it does not do any expensive operations. * * @param selected The ViewHolder being dragged by the user. * @param dropTargets The list of ViewHolder that are under the dragged View and * candidate as a drop. * @param curX The updated left value of the dragged View after drag translations * are applied. This value does not include margins added by * {@link RecyclerView.ItemDecoration}s. * @param curY The updated top value of the dragged View after drag translations * are applied. This value does not include margins added by * {@link RecyclerView.ItemDecoration}s. * @return A ViewHolder to whose position the dragged ViewHolder should be * moved to. */ @SuppressWarnings("WeakerAccess") @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public RecyclerView.ViewHolder chooseDropTarget(@NonNull RecyclerView.ViewHolder selected, @NonNull List dropTargets, int curX, int curY) { int right = curX + selected.itemView.getWidth(); int bottom = curY + selected.itemView.getHeight(); RecyclerView.ViewHolder winner = null; int winnerScore = -1; final int dx = curX - selected.itemView.getLeft(); final int dy = curY - selected.itemView.getTop(); final int targetsSize = dropTargets.size(); for (int i = 0; i < targetsSize; i++) { final RecyclerView.ViewHolder target = dropTargets.get(i); if (dx > 0) { int diff = target.itemView.getRight() - right; if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } if (dx < 0) { int diff = target.itemView.getLeft() - curX; if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } if (dy < 0) { int diff = target.itemView.getTop() - curY; if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } if (dy > 0) { int diff = target.itemView.getBottom() - bottom; if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } } return winner; } /** * Called when a ViewHolder is swiped by the user. *

* If you are returning relative directions ({@link #START} , {@link #END}) from the * {@link #getMovementFlags(RecyclerView, RecyclerView.ViewHolder)} method, this method * will also use relative directions. Otherwise, it will use absolute directions. *

* If you don't support swiping, this method will never be called. *

* ItemTouchHelper will keep a reference to the View until it is detached from * RecyclerView. * As soon as it is detached, ItemTouchHelper will call * {@link #clearView(RecyclerView, RecyclerView.ViewHolder)}. * * @param viewHolder The ViewHolder which has been swiped by the user. * @param direction The direction to which the ViewHolder is swiped. It is one of * {@link #UP}, {@link #DOWN}, * {@link #LEFT} or {@link #RIGHT}. If your * {@link #getMovementFlags(RecyclerView, RecyclerView.ViewHolder)} * method * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; * `direction` will be relative as well. ({@link #START} or {@link * #END}). */ public abstract void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction); /** * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed. *

* If you override this method, you should call super. * * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if * it is cleared. * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE}, * {@link ItemTouchHelper#ACTION_STATE_SWIPE} or * {@link ItemTouchHelper#ACTION_STATE_DRAG}. * @see #clearView(RecyclerView, RecyclerView.ViewHolder) */ public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) { if (viewHolder != null) { ItemTouchUIUtilImpl.INSTANCE.onSelected(viewHolder.itemView); } } private int getMaxDragScroll(RecyclerView recyclerView) { if (mCachedMaxScrollSpeed == -1) { mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( R.dimen.item_touch_helper_max_drag_scroll_per_frame); } return mCachedMaxScrollSpeed; } /** * Called when {@link #onMove(RecyclerView, RecyclerView.ViewHolder, RecyclerView.ViewHolder)} returns true. *

* ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it * modifies the existing View. Because of this reason, it is important that the View is * still part of the layout after it is moved. This may not work as intended when swapped * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views * which were not eligible for dropping over). *

* This method is responsible to give necessary hint to the LayoutManager so that it will * keep the View in visible area. For example, for LinearLayoutManager, this is as simple * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}. * * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's * new position is likely to be out of bounds. *

* It is important to ensure the ViewHolder will stay visible as otherwise, it might be * removed by the LayoutManager if the move causes the View to go out of bounds. In that * case, drag will end prematurely. * * @param recyclerView The RecyclerView controlled by the * @param viewHolder The ViewHolder under user's control. * @param fromPos The previous adapter position of the dragged item (before it was * moved). * @param target The ViewHolder on which the currently active item has been dropped. * @param toPos The new adapter position of the dragged item. * @param x The updated left value of the dragged View after drag translations * are applied. This value does not include margins added by * {@link RecyclerView.ItemDecoration}s. * @param y The updated top value of the dragged View after drag translations * are applied. This value does not include margins added by * {@link RecyclerView.ItemDecoration}s. */ public void onMoved(@NonNull final RecyclerView recyclerView, @NonNull final RecyclerView.ViewHolder viewHolder, int fromPos, @NonNull final RecyclerView.ViewHolder target, int toPos, int x, int y) { final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof ViewDropHandler) { ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, target.itemView, x, y); return; } // if layout manager cannot handle it, do some guesswork if (layoutManager.canScrollHorizontally()) { final int minLeft = layoutManager.getDecoratedLeft(target.itemView); if (minLeft <= recyclerView.getPaddingLeft()) { recyclerView.scrollToPosition(toPos); } final int maxRight = layoutManager.getDecoratedRight(target.itemView); if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { recyclerView.scrollToPosition(toPos); } } if (layoutManager.canScrollVertically()) { final int minTop = layoutManager.getDecoratedTop(target.itemView); if (minTop <= recyclerView.getPaddingTop()) { recyclerView.scrollToPosition(toPos); } final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { recyclerView.scrollToPosition(toPos); } } } void onDraw(Canvas c, RecyclerView parent, RecyclerView.ViewHolder selected, List recoverAnimationList, int actionState, float dX, float dY) { final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { final RecoverAnimation anim = recoverAnimationList.get(i); anim.update(); final int count = c.save(); onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, false); c.restoreToCount(count); } if (selected != null) { final int count = c.save(); onChildDraw(c, parent, selected, dX, dY, actionState, true); c.restoreToCount(count); } } void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.ViewHolder selected, List recoverAnimationList, int actionState, float dX, float dY) { final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { final RecoverAnimation anim = recoverAnimationList.get(i); final int count = c.save(); onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, false); c.restoreToCount(count); } if (selected != null) { final int count = c.save(); onChildDrawOver(c, parent, selected, dX, dY, actionState, true); c.restoreToCount(count); } boolean hasRunningAnimation = false; for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation anim = recoverAnimationList.get(i); if (anim.mEnded && !anim.mIsPendingCleanup) { recoverAnimationList.remove(i); } else if (!anim.mEnded) { hasRunningAnimation = true; } } if (hasRunningAnimation) { parent.invalidate(); } } /** * Called by the ItemTouchHelper when the user interaction with an element is over and it * also completed its animation. *

* This is a good place to clear all changes on the View that was done in * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, * {@link #onChildDraw(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, * boolean)} or * {@link #onChildDrawOver(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}. * * @param recyclerView The RecyclerView which is controlled by the * @param viewHolder The View that was interacted by the user. */ public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { ItemTouchUIUtilImpl.INSTANCE.clearView(viewHolder.itemView); } /** * Called by ItemTouchHelper on RecyclerView's onDraw callback. *

* If you would like to customize how your View's respond to user interactions, this is * a good place to override. *

* Default implementation translates the child by the given dX, * dY. * ItemTouchHelper also takes care of drawing the child after other children if it is being * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this * is * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L * and after, it changes View's elevation value to be greater than all other children.) * * @param c The canvas which RecyclerView is drawing its children * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to * @param viewHolder The ViewHolder which is being interacted by the User or it was * interacted and simply animating to its original position * @param dX The amount of horizontal displacement caused by user's action * @param dY The amount of vertical displacement caused by user's action * @param actionState The type of interaction on the View. Is either {@link * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. * @param isCurrentlyActive True if this view is currently being controlled by the user or * false it is simply animating back to its original state. * @see #onChildDrawOver(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, * boolean) */ public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive); } /** * Called by ItemTouchHelper on RecyclerView's onDraw callback. *

* If you would like to customize how your View's respond to user interactions, this is * a good place to override. *

* Default implementation translates the child by the given dX, * dY. * ItemTouchHelper also takes care of drawing the child after other children if it is being * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this * is * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L * and after, it changes View's elevation value to be greater than all other children.) * * @param c The canvas which RecyclerView is drawing its children * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to * @param viewHolder The ViewHolder which is being interacted by the User or it was * interacted and simply animating to its original position * @param dX The amount of horizontal displacement caused by user's action * @param dY The amount of vertical displacement caused by user's action * @param actionState The type of interaction on the View. Is either {@link * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. * @param isCurrentlyActive True if this view is currently being controlled by the user or * false it is simply animating back to its original state. * @see #onChildDrawOver(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, * boolean) */ public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { ItemTouchUIUtilImpl.INSTANCE.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive); } /** * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View * will be animated to its final position. *

* Default implementation uses ItemAnimator's duration values. If * animationType is {@link #ANIMATION_TYPE_DRAG}, it returns * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have * any {@link RecyclerView.ItemAnimator} attached, this method returns * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} * depending on the animation type. * * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. * @param animateDx The horizontal distance that the animation will offset * @param animateDy The vertical distance that the animation will offset * @return The duration for the animation */ @SuppressWarnings("WeakerAccess") public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) { final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); if (itemAnimator == null) { return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION : DEFAULT_SWIPE_ANIMATION_DURATION; } else { return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() : itemAnimator.getRemoveDuration(); } } /** * Called by the ItemTouchHelper when user is dragging a view out of bounds. *

* You can override this method to decide how much RecyclerView should scroll in response * to this action. Default implementation calculates a value based on the amount of View * out of bounds and the time it spent there. The longer user keeps the View out of bounds, * the faster the list will scroll. Similarly, the larger portion of the View is out of * bounds, the faster the RecyclerView will scroll. * * @param recyclerView The RecyclerView instance to which ItemTouchHelper is * attached to. * @param viewSize The total size of the View in scroll direction, excluding * item decorations. * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value * is negative if the View is dragged towards left or top edge. * @param totalSize The total size of RecyclerView in the scroll direction. * @param msSinceStartScroll The time passed since View is kept out of bounds. * @return The amount that RecyclerView should scroll. Keep in mind that this value will * be passed to {@link RecyclerView#scrollBy(int, int)} method. */ @SuppressWarnings("WeakerAccess") public int interpolateOutOfBoundsScroll(@NonNull RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) { final int maxScroll = getMaxDragScroll(recyclerView); final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); final int direction = (int) Math.signum(viewSizeOutOfBounds); // might be negative if other direction float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); final int cappedScroll = (int) (direction * maxScroll * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); final float timeRatio; if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { timeRatio = 1f; } else { timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; } final int value = (int) (cappedScroll * sDragScrollInterpolator .getInterpolation(timeRatio)); if (value == 0) { return viewSizeOutOfBounds > 0 ? 1 : -1; } return value; } } /** * A simple wrapper to the default Callback which you can construct with drag and swipe * directions and this class will handle the flag callbacks. You should still override onMove * or * onSwiped depending on your use case. * *

     * ItemTouchHelper mIth = new ItemTouchHelper(
     *     new SimpleCallback(UP | DOWN,
     *         LEFT) {
     *         public boolean onMove(RecyclerView recyclerView,
     *             ViewHolder viewHolder, ViewHolder target) {
     *             final int fromPos = viewHolder.getAdapterPosition();
     *             final int toPos = target.getAdapterPosition();
     *             // move item in `fromPos` to `toPos` in adapter.
     *             return true;// true if moved, false otherwise
     *         }
     *         public void onSwiped(ViewHolder viewHolder, int direction) {
     *             // remove from adapter
     *         }
     * });
     * 
*/ public abstract static class SimpleCallback extends Callback { private int mDefaultSwipeDirs; private int mDefaultDragDirs; /** * Creates a Callback for the given drag and swipe allowance. These values serve as * defaults * and if you want to customize behavior per ViewHolder, you can override * {@link #getSwipeDirs(RecyclerView, RecyclerView.ViewHolder)} * and / or {@link #getDragDirs(RecyclerView, RecyclerView.ViewHolder)}. * * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link * #END}, * {@link #UP} and {@link #DOWN}. * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link * #END}, * {@link #UP} and {@link #DOWN}. */ public SimpleCallback(int dragDirs, int swipeDirs) { mDefaultSwipeDirs = swipeDirs; mDefaultDragDirs = dragDirs; } /** * Updates the default swipe directions. For example, you can use this method to toggle * certain directions depending on your use case. * * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped. */ @SuppressWarnings({"WeakerAccess", "unused"}) public void setDefaultSwipeDirs(@SuppressWarnings("unused") int defaultSwipeDirs) { mDefaultSwipeDirs = defaultSwipeDirs; } /** * Updates the default drag directions. For example, you can use this method to toggle * certain directions depending on your use case. * * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged. */ @SuppressWarnings({"WeakerAccess", "unused"}) public void setDefaultDragDirs(@SuppressWarnings("unused") int defaultDragDirs) { mDefaultDragDirs = defaultDragDirs; } /** * Returns the swipe directions for the provided ViewHolder. * Default implementation returns the swipe directions that was set via constructor or * {@link #setDefaultSwipeDirs(int)}. * * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. * @param viewHolder The ViewHolder for which the swipe direction is queried. * @return A binary OR of direction flags. */ @SuppressWarnings("WeakerAccess") public int getSwipeDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, @NonNull @SuppressWarnings("unused") RecyclerView.ViewHolder viewHolder) { return mDefaultSwipeDirs; } /** * Returns the drag directions for the provided ViewHolder. * Default implementation returns the drag directions that was set via constructor or * {@link #setDefaultDragDirs(int)}. * * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. * @param viewHolder The ViewHolder for which the swipe direction is queried. * @return A binary OR of direction flags. */ @SuppressWarnings("WeakerAccess") public int getDragDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, @SuppressWarnings("unused") @NonNull RecyclerView.ViewHolder viewHolder) { return mDefaultDragDirs; } @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return makeMovementFlags(getDragDirs(recyclerView, viewHolder), getSwipeDirs(recyclerView, viewHolder)); } } private class AdjustableTouchSlopItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { /** * Whether to execute code in response to the the invoking of * {@link AdjustableTouchSlopItemTouchHelperGestureListener#onLongPress(MotionEvent)}. * * It is necessary to control this here because * {@link GestureDetector.SimpleOnGestureListener} can only be set on a * {@link GestureDetector} in a GestureDetector's constructor, a GestureDetector will call * onLongPress if an {@link MotionEvent#ACTION_DOWN} event is not followed by another event * that would cancel it (like {@link MotionEvent#ACTION_UP} or * {@link MotionEvent#ACTION_CANCEL}), the long press responding to the long press event * needs to be cancellable to prevent unexpected behavior. * * @see #doNotReactToLongPress() */ private boolean mShouldReactToLongPress = true; AdjustableTouchSlopItemTouchHelperGestureListener() { } /** * Call to prevent executing code in response to * {@link AdjustableTouchSlopItemTouchHelperGestureListener#onLongPress(MotionEvent)} being called. */ void doNotReactToLongPress() { mShouldReactToLongPress = false; } @Override public boolean onDown(MotionEvent e) { return true; } @Override public void onLongPress(MotionEvent e) { if (!mShouldReactToLongPress) { return; } View child = findChildView(e); if (child != null) { RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(child); if (vh != null) { if (!mCallback.hasDragFlag(mRecyclerView, vh)) { return; } int pointerId = e.getPointerId(0); // Long press is deferred. // Check w/ active pointer id to avoid selecting after motion // event is canceled. if (pointerId == mActivePointerId) { final int index = e.findPointerIndex(mActivePointerId); final float x = e.getX(index); final float y = e.getY(index); mInitialTouchX = x; mInitialTouchY = y; mDx = mDy = 0f; if (DEBUG) { Log.d(TAG, "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); } if (mCallback.isLongPressDragEnabled()) { select(vh, ACTION_STATE_DRAG); } } } } } } @VisibleForTesting static class RecoverAnimation implements Animator.AnimatorListener { final float mStartDx; final float mStartDy; final float mTargetX; final float mTargetY; final RecyclerView.ViewHolder mViewHolder; final int mActionState; @VisibleForTesting final ValueAnimator mValueAnimator; final int mAnimationType; boolean mIsPendingCleanup; float mX; float mY; // if user starts touching a recovering view, we put it into interaction mode again, // instantly. boolean mOverridden = false; boolean mEnded = false; private float mFraction; RecoverAnimation(RecyclerView.ViewHolder viewHolder, int animationType, int actionState, float startDx, float startDy, float targetX, float targetY) { mActionState = actionState; mAnimationType = animationType; mViewHolder = viewHolder; mStartDx = startDx; mStartDy = startDy; mTargetX = targetX; mTargetY = targetY; mValueAnimator = ValueAnimator.ofFloat(0f, 1f); mValueAnimator.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setFraction(animation.getAnimatedFraction()); } }); mValueAnimator.setTarget(viewHolder.itemView); mValueAnimator.addListener(this); setFraction(0f); } public void setDuration(long duration) { mValueAnimator.setDuration(duration); } public void start() { mViewHolder.setIsRecyclable(false); mValueAnimator.start(); } public void cancel() { mValueAnimator.cancel(); } public void setFraction(float fraction) { mFraction = fraction; } /** * We run updates on onDraw method but use the fraction from animator callback. * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. */ public void update() { if (mStartDx == mTargetX) { mX = mViewHolder.itemView.getTranslationX(); } else { mX = mStartDx + mFraction * (mTargetX - mStartDx); } if (mStartDy == mTargetY) { mY = mViewHolder.itemView.getTranslationY(); } else { mY = mStartDy + mFraction * (mTargetY - mStartDy); } } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (!mEnded) { mViewHolder.setIsRecyclable(true); } mEnded = true; } @Override public void onAnimationCancel(Animator animation) { setFraction(1f); //make sure we recover the view's state. } @Override public void onAnimationRepeat(Animator animation) { } } private static class ItemTouchUIUtilImpl implements ItemTouchUIUtil { static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl(); @Override public void onDraw( @NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull View view, float dX, float dY, int actionState, boolean isCurrentlyActive ) { if (Build.VERSION.SDK_INT >= 21) { if (isCurrentlyActive) { Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation); if (originalElevation == null) { originalElevation = ViewCompat.getElevation(view); float newElevation = 1f + findMaxElevation(recyclerView, view); ViewCompat.setElevation(view, newElevation); view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation); } } } view.setTranslationX(dX); view.setTranslationY(dY); } private static float findMaxElevation(RecyclerView recyclerView, View itemView) { final int childCount = recyclerView.getChildCount(); float max = 0; for (int i = 0; i < childCount; i++) { final View child = recyclerView.getChildAt(i); if (child == itemView) { continue; } final float elevation = ViewCompat.getElevation(child); if (elevation > max) { max = elevation; } } return max; } @Override public void onDrawOver( @NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull View view, float dX, float dY, int actionState, boolean isCurrentlyActive ) { } @Override public void clearView(@NonNull View view) { if (Build.VERSION.SDK_INT >= 21) { final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation); if (tag instanceof Float) { ViewCompat.setElevation(view, (Float) tag); } view.setTag(R.id.item_touch_helper_previous_elevation, null); } view.setTranslationX(0f); view.setTranslationY(0f); } @Override public void onSelected(@NonNull View view) { } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/AspectRatioGifImageView.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import pl.droidsonroids.gif.GifImageView; public class AspectRatioGifImageView extends GifImageView { private float ratio; public AspectRatioGifImageView(Context context) { super(context); this.ratio = 1.0F; } public AspectRatioGifImageView(Context context, AttributeSet attrs) { super(context, attrs); this.ratio = 1.0F; this.init(context, attrs); } public final float getRatio() { return this.ratio; } public final void setRatio(float var1) { if (Math.abs(this.ratio - var1) > 0.0001) { this.ratio = var1; requestLayout(); invalidate(); } } private void init(Context context, AttributeSet attrs) { if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, com.santalu.aspectratioimageview.R.styleable.AspectRatioImageView); this.ratio = a.getFloat(com.santalu.aspectratioimageview.R.styleable.AspectRatioImageView_ari_ratio, 1.0F); a.recycle(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (this.ratio > 0) { int width = this.getMeasuredWidth(); int height = this.getMeasuredHeight(); if (width != 0 || height != 0) { if (width > 0) { height = (int) ((float) width * this.ratio); } else { width = (int) ((float) height / this.ratio); } this.setMeasuredDimension(width, height); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/ClickableMotionLayout.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ViewConfiguration; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.motion.widget.MotionLayout; public class ClickableMotionLayout extends MotionLayout { private long mStartTime = 0; public ClickableMotionLayout(@NonNull Context context) { super(context); } public ClickableMotionLayout(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public ClickableMotionLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if ( event.getAction() == MotionEvent.ACTION_DOWN ) { mStartTime = event.getEventTime(); } else if ( event.getAction() == MotionEvent.ACTION_UP ) { if ( event.getEventTime() - mStartTime <= ViewConfiguration.getTapTimeout() ) { return false; } } return super.onInterceptTouchEvent(event); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/ColorPickerDialog.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.graphics.Color; import android.text.Editable; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.SeekBar; import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import ml.docilealligator.infinityforreddit.R; public class ColorPickerDialog extends AlertDialog { private final View colorView; private final EditText colorValueEditText; private final SeekBar seekBarA; private final SeekBar seekBarR; private final SeekBar seekBarG; private final SeekBar seekBarB; private final Button cancelButton; private final Button okButton; private int colorValue; private boolean changeColorValueEditText = true; private ColorPickerListener colorPickerListener; public interface ColorPickerListener { void onColorPicked(int color); } public ColorPickerDialog(Context context, int color, ColorPickerListener colorPickerListener) { super(context); View rootView = getLayoutInflater().inflate(R.layout.color_picker, null); colorView = rootView.findViewById(R.id.color_view_color_picker); colorValueEditText = rootView.findViewById(R.id.color_edit_text_color_picker); seekBarA = rootView.findViewById(R.id.a_seek_bar_color_picker); seekBarR = rootView.findViewById(R.id.r_seek_bar_color_picker); seekBarG = rootView.findViewById(R.id.g_seek_bar_color_picker); seekBarB = rootView.findViewById(R.id.b_seek_bar_color_picker); cancelButton = rootView.findViewById(R.id.cancel_button_color_picker); okButton = rootView.findViewById(R.id.ok_button_color_picker); colorView.setBackgroundColor(color); colorValueEditText.setText(Integer.toHexString(color).toUpperCase()); colorValueEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void afterTextChanged(Editable editable) { String s = editable.toString(); if (s.length() == 6) { try { changeColorValueEditText = false; colorValue = Color.parseColor("#" + s); colorView.setBackgroundColor(colorValue); seekBarA.setProgress(255); seekBarR.setProgress(Integer.parseInt(s.substring(0, 2), 16)); seekBarG.setProgress(Integer.parseInt(s.substring(2, 4), 16)); seekBarB.setProgress(Integer.parseInt(s.substring(4, 6), 16)); changeColorValueEditText = true; } catch (IllegalArgumentException ignored) { } } else if (s.length() == 8) { try { changeColorValueEditText = false; colorValue = Color.parseColor("#" + s); colorView.setBackgroundColor(colorValue); seekBarA.setProgress(Integer.parseInt(s.substring(0, 2), 16)); seekBarR.setProgress(Integer.parseInt(s.substring(2, 4), 16)); seekBarG.setProgress(Integer.parseInt(s.substring(4, 6), 16)); seekBarB.setProgress(Integer.parseInt(s.substring(6, 8), 16)); changeColorValueEditText = true; } catch (IllegalArgumentException ignored) { } } } }); String colorHex = Integer.toHexString(color); if (colorHex.length() == 8) { colorValue = Color.parseColor("#" + colorHex); seekBarA.setProgress(Integer.parseInt(colorHex.substring(0, 2), 16)); seekBarR.setProgress(Integer.parseInt(colorHex.substring(2, 4), 16)); seekBarG.setProgress(Integer.parseInt(colorHex.substring(4, 6), 16)); seekBarB.setProgress(Integer.parseInt(colorHex.substring(6, 8), 16)); } else if (colorHex.length() == 6) { colorValue = Color.parseColor("#" + colorHex); seekBarA.setProgress(255); seekBarR.setProgress(Integer.parseInt(colorHex.substring(0, 2), 16)); seekBarG.setProgress(Integer.parseInt(colorHex.substring(2, 4), 16)); seekBarB.setProgress(Integer.parseInt(colorHex.substring(4, 6), 16)); } setOnSeekBarChangeListener(seekBarA); setOnSeekBarChangeListener(seekBarR); setOnSeekBarChangeListener(seekBarG); setOnSeekBarChangeListener(seekBarB); cancelButton.setOnClickListener(view -> dismiss()); okButton.setOnClickListener(view -> { try { colorValue = Color.parseColor("#" + colorValueEditText.getText().toString()); colorPickerListener.onColorPicked(colorValue); dismiss(); } catch (IllegalArgumentException e) { Toast.makeText(context, R.string.invalid_color, Toast.LENGTH_SHORT).show(); } }); setView(rootView); } private void setOnSeekBarChangeListener(SeekBar seekBar) { seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { if (changeColorValueEditText) { int aValue = seekBarA.getProgress(); int rValue = seekBarR.getProgress(); int gValue = seekBarG.getProgress(); int bValue = seekBarB.getProgress(); String colorHex = String.format("%02x%02x%02x%02x", aValue, rValue, gValue, bValue).toUpperCase(); colorValue = Color.parseColor("#" + colorHex); colorView.setBackgroundColor(colorValue); colorValueEditText.setText(colorHex); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CommentIndentationView.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.widget.LinearLayout; import androidx.annotation.Nullable; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.utils.Utils; public class CommentIndentationView extends LinearLayout { private final Paint paint; private int level; private int[] colors; private ArrayList startXs; private final int spacing; private int pathWidth; private boolean showOnlyOneDivider = false; public CommentIndentationView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); setSaveEnabled(true); setWillNotDraw(false); paint = new Paint(Paint.ANTI_ALIAS_FLAG); pathWidth = (int) Utils.convertDpToPixel(2, context); spacing = pathWidth * 6; paint.setStrokeWidth(pathWidth); paint.setStyle(Paint.Style.STROKE); startXs = new ArrayList<>(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); startXs.clear(); for (int i = 0; i < level; i++) { startXs.add(spacing * (i + 1) + pathWidth); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (showOnlyOneDivider) { if (startXs.size() > 0) { paint.setColor(colors[(startXs.size() - 1) % 7]); canvas.drawLine(level * pathWidth, 0, level * pathWidth, getHeight(), paint); } } else { for (int i = 0; i < startXs.size(); i++) { paint.setColor(colors[i % 7]); canvas.drawLine(startXs.get(i), 0, startXs.get(i), getHeight(), paint); } } } @Nullable @Override protected Parcelable onSaveInstanceState() { Parcelable parcelable = super.onSaveInstanceState(); SavedState myState = new SavedState(parcelable); myState.startXs = this.startXs; myState.colors = this.colors; return parcelable; } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); this.startXs = savedState.startXs; this.colors = savedState.colors; invalidate(); } public void setLevelAndColors(int level, int[] colors) { this.colors = colors; this.level = level; if (level > 0) { int indentationSpacing = showOnlyOneDivider ? pathWidth * level : level * spacing + pathWidth; setPaddingRelative(indentationSpacing, 0, pathWidth, 0); } else { setPaddingRelative(0, 0, 0, 0); } invalidate(); } public void setShowOnlyOneDivider(boolean showOnlyOneDivider) { this.showOnlyOneDivider = showOnlyOneDivider; if (showOnlyOneDivider) { pathWidth = (int) Utils.convertDpToPixel(4, getContext()); } } private static class SavedState extends BaseSavedState { ArrayList startXs; int[] colors; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); startXs = new ArrayList<>(); in.readList(startXs, Integer.class.getClassLoader()); colors = in.createIntArray(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeList(startXs); out.writeIntArray(colors); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator<>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomDrawerLayout.kt ================================================ package ml.docilealligator.infinityforreddit.customviews import android.content.Context import android.util.AttributeSet import androidx.customview.widget.ViewDragHelper import androidx.drawerlayout.widget.DrawerLayout import java.lang.reflect.Field import kotlin.jvm.java import kotlin.math.max class CustomDrawerLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : DrawerLayout(context, attrs) { var swipeEdgeSize = 0 set(value) { if (field != value) { field = value applyLeftDraggerEdgeSize() } } private var leftDraggerField: Field? = null private var edgeSizeField: Field? = null init { try { leftDraggerField = DrawerLayout::class.java.getDeclaredField("mLeftDragger") leftDraggerField?.isAccessible = true edgeSizeField = ViewDragHelper::class.java.getDeclaredField("mEdgeSize") edgeSizeField?.isAccessible = true } catch (_: Exception) {} } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { super.onLayout(changed, l, t, r, b) applyLeftDraggerEdgeSize() } private fun applyLeftDraggerEdgeSize() { try { val viewDragHelper = leftDraggerField?.get(this) as? ViewDragHelper ?: return val originalEdgeSize = edgeSizeField?.get(viewDragHelper) as? Int ?: return edgeSizeField?.setInt(viewDragHelper, max(swipeEdgeSize, originalEdgeSize)) } catch (_: Exception) { } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomToroContainer.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.util.AttributeSet; import androidx.annotation.Nullable; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; public class CustomToroContainer extends Container { private OnWindowFocusChangedListener listener; public CustomToroContainer(Context context) { super(context); } public CustomToroContainer(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public CustomToroContainer(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (listener != null) { listener.onWindowFocusChanged(hasWindowFocus); } } public void addOnWindowFocusChangedListener(OnWindowFocusChangedListener onWindowFocusChangedListener) { this.listener = onWindowFocusChangedListener; } public interface OnWindowFocusChangedListener { void onWindowFocusChanged(boolean hasWindowsFocus); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/GlideGifImageViewFactory.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.net.Uri; import android.view.View; import com.bumptech.glide.Glide; import com.github.piasy.biv.metadata.ImageInfoExtractor; import com.github.piasy.biv.view.BigImageView; import com.github.piasy.biv.view.ImageViewFactory; import java.io.File; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import pl.droidsonroids.gif.GifImageView; public class GlideGifImageViewFactory extends ImageViewFactory { private SaveMemoryCenterInisdeDownsampleStrategy saveMemoryCenterInisdeDownsampleStrategy; public GlideGifImageViewFactory(SaveMemoryCenterInisdeDownsampleStrategy saveMemoryCenterInisdeDownsampleStrategy) { this.saveMemoryCenterInisdeDownsampleStrategy = saveMemoryCenterInisdeDownsampleStrategy; } @Override protected final View createAnimatedImageView(final Context context, final int imageType, final int initScaleType) { switch (imageType) { case ImageInfoExtractor.TYPE_GIF: case ImageInfoExtractor.TYPE_ANIMATED_WEBP: { final GifImageView imageView = new GifImageView(context); imageView.setScaleType(BigImageView.scaleType(initScaleType)); return imageView; } default: return super.createAnimatedImageView(context, imageType, initScaleType); } } @Override public final void loadAnimatedContent(final View view, final int imageType, final File imageFile) { switch (imageType) { case ImageInfoExtractor.TYPE_GIF: case ImageInfoExtractor.TYPE_ANIMATED_WEBP: { if (view instanceof GifImageView) { Glide.with(view.getContext()) .load(imageFile) .centerInside() .downsample(saveMemoryCenterInisdeDownsampleStrategy) .into((GifImageView) view); } break; } default: super.loadAnimatedContent(view, imageType, imageFile); } } @Override public void loadThumbnailContent(final View view, final Uri thumbnail) { if (view instanceof GifImageView) { Glide.with(view.getContext()) .load(thumbnail) .into((GifImageView) view); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/InterceptTouchEventLinearLayout.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.LinearLayout; import androidx.annotation.Nullable; public class InterceptTouchEventLinearLayout extends LinearLayout { public InterceptTouchEventLinearLayout(Context context) { super(context); } public InterceptTouchEventLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public InterceptTouchEventLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } public InterceptTouchEventLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LandscapeExpandedRoundedBottomSheetDialogFragment.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.view.View; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; public class LandscapeExpandedRoundedBottomSheetDialogFragment extends BottomSheetDialogFragment { @Override public void onStart() { super.onStart(); View parentView = (View) requireView().getParent(); BottomSheetBehavior.from(parentView).setState(BottomSheetBehavior.STATE_EXPANDED); BottomSheetBehavior.from(parentView).setSkipCollapsed(true); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LinearLayoutManagerBugFixed.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.util.AttributeSet; import androidx.recyclerview.widget.LinearLayoutManager; public class LinearLayoutManagerBugFixed extends LinearLayoutManager { public LinearLayoutManagerBugFixed(Context context) { super(context); } public LinearLayoutManagerBugFixed(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } public LinearLayoutManagerBugFixed(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public boolean supportsPredictiveItemAnimations() { return false; } public LinearLayoutManagerBugFixed setStackFromEndAndReturnCurrentObject() { setStackFromEnd(true); return this; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LollipopBugFixedWebView.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.util.AttributeSet; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.webkit.WebView; public class LollipopBugFixedWebView extends WebView { private boolean isAnonymous; public LollipopBugFixedWebView(Context context) { super(getFixedContext(context)); } public LollipopBugFixedWebView(Context context, AttributeSet attrs) { super(getFixedContext(context), attrs); } public LollipopBugFixedWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(getFixedContext(context), attrs, defStyleAttr); } // To fix Android Lollipop WebView problem create a new configuration on that Android version only private static Context getFixedContext(Context context) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP || Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) // Android Lollipop 5.0 & 5.1 return context.createConfigurationContext(new Configuration()); return context; } public void setAnonymous(boolean isAnonymous) { this.isAnonymous = isAnonymous; } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { InputConnection inputConnection = super.onCreateInputConnection(outAttrs); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isAnonymous) { outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING; } return inputConnection; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.videoautoplay.Config; import ml.docilealligator.infinityforreddit.videoautoplay.DefaultExoCreator; import ml.docilealligator.infinityforreddit.videoautoplay.ToroExo; @UnstableApi public class LoopAvailableExoCreator extends DefaultExoCreator { private final SharedPreferences sharedPreferences; public LoopAvailableExoCreator(@NonNull ToroExo toro, @NonNull Config config, SharedPreferences sharedPreferences) { super(toro, config); this.sharedPreferences = sharedPreferences; } @NonNull @Override public ExoPlayer createPlayer() { ExoPlayer player = super.createPlayer(); if (sharedPreferences.getBoolean(SharedPreferencesUtils.LOOP_VIDEO, true)) { player.setRepeatMode(Player.REPEAT_MODE_ALL); } else { player.setRepeatMode(Player.REPEAT_MODE_OFF); } return player; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/MovableFloatingActionButton.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.content.SharedPreferences; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import androidx.annotation.Nullable; import com.google.android.material.floatingactionbutton.FloatingActionButton; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class MovableFloatingActionButton extends FloatingActionButton implements View.OnTouchListener { private final static float CLICK_DRAG_TOLERANCE = 50; private long downTime = 0; private boolean moved = false; private boolean longClicked = false; private float downRawX, downRawY; private float dX, dY; @Nullable private Display display; @Nullable private SharedPreferences postDetailsSharedPreferences; private boolean portrait; @Nullable private SliderPanel sliderPanel; public MovableFloatingActionButton(Context context) { super(context); init(); } public MovableFloatingActionButton(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MovableFloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { setOnTouchListener(this); new Handler(Looper.getMainLooper()).post(() -> { ViewParent parent = getParent(); while (parent != null) { if (parent instanceof SliderPanel) { sliderPanel = (SliderPanel) parent; break; } parent = parent.getParent(); } }); } @Override public boolean onTouch(View view, MotionEvent motionEvent) { ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); int action = motionEvent.getAction(); if (action == MotionEvent.ACTION_DOWN) { downTime = System.currentTimeMillis(); moved = false; downRawX = motionEvent.getRawX(); downRawY = motionEvent.getRawY(); dX = view.getX() - downRawX; dY = view.getY() - downRawY; if (sliderPanel != null) { sliderPanel.lock(); } return true; } else if (action == MotionEvent.ACTION_MOVE) { if (!moved) { if (System.currentTimeMillis() - downTime >= 300) { if (!longClicked) { longClicked = true; return performLongClick(); } else { moved = true; } } float upRawX = motionEvent.getRawX(); float upRawY = motionEvent.getRawY(); float upDX = upRawX - downRawX; float upDY = upRawY - downRawY; if (Math.abs(upDX) < CLICK_DRAG_TOLERANCE && Math.abs(upDY) < CLICK_DRAG_TOLERANCE) { return true; } else { moved = true; } } int viewWidth = view.getWidth(); int viewHeight = view.getHeight(); View viewParent = (View) view.getParent(); int parentWidth = viewParent.getWidth(); int parentHeight = viewParent.getHeight(); float newX = motionEvent.getRawX() + dX; newX = Math.max(layoutParams.leftMargin, newX); // Don't allow the FAB past the left hand side of the parent newX = Math.min(parentWidth - viewWidth - layoutParams.rightMargin, newX); // Don't allow the FAB past the right hand side of the parent float newY = motionEvent.getRawY() + dY; newY = Math.max(layoutParams.topMargin, newY); // Don't allow the FAB past the top of the parent newY = Math.min(parentHeight - viewHeight - layoutParams.bottomMargin, newY); // Don't allow the FAB past the bottom of the parent saveCoordinates(newX, newY); view.animate() .x(newX) .y(newY) .setDuration(0) .start(); return true; } else if (action == MotionEvent.ACTION_UP) { if (longClicked) { longClicked = false; return true; } float upRawX = motionEvent.getRawX(); float upRawY = motionEvent.getRawY(); float upDX = upRawX - downRawX; float upDY = upRawY - downRawY; if (sliderPanel != null) { sliderPanel.unlock(); } if (Math.abs(upDX) < CLICK_DRAG_TOLERANCE && Math.abs(upDY) < CLICK_DRAG_TOLERANCE) { return System.currentTimeMillis() - downTime >= 300 ? performLongClick() : performClick(); } else { return true; } } else { if (sliderPanel != null) { sliderPanel.unlock(); } return super.onTouchEvent(motionEvent); } } private void setPositionEnsureVisibility(float newX, float newY) { ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); View viewParent = (View) getParent(); int parentWidth = viewParent.getWidth(); int parentHeight = viewParent.getHeight(); int viewWidth = getWidth(); int viewHeight = getHeight(); newX = Math.max(layoutParams.leftMargin, newX); // Don't allow the FAB past the left hand side of the parent newX = Math.min(parentWidth - viewWidth - layoutParams.rightMargin, newX); // Don't allow the FAB past the right hand side of the parent newY = Math.max(layoutParams.topMargin, newY); // Don't allow the FAB past the top of the parent newY = Math.min(parentHeight - viewHeight - layoutParams.bottomMargin, newY); // Don't allow the FAB past the bottom of the parent setX(newX); setY(newY); } public void bindRequiredData(@Nullable Display display, SharedPreferences postDetailsSharedPreferences, boolean portrait) { this.display = display; this.postDetailsSharedPreferences = postDetailsSharedPreferences; this.portrait = portrait; } public void setCoordinates() { if (postDetailsSharedPreferences == null) { return; } if (portrait) { if (postDetailsSharedPreferences.contains(SharedPreferencesUtils.getPostDetailFabPortraitX(display)) && postDetailsSharedPreferences.contains(SharedPreferencesUtils.getPostDetailFabPortraitY(display))) { setPositionEnsureVisibility(postDetailsSharedPreferences.getFloat(SharedPreferencesUtils.getPostDetailFabPortraitX(display), 0), postDetailsSharedPreferences.getFloat(SharedPreferencesUtils.getPostDetailFabPortraitY(display), 0)); } } else { if (postDetailsSharedPreferences.contains(SharedPreferencesUtils.getPostDetailFabLandscapeX(display)) && postDetailsSharedPreferences.contains(SharedPreferencesUtils.getPostDetailFabLandscapeY(display))) { setPositionEnsureVisibility(postDetailsSharedPreferences.getFloat(SharedPreferencesUtils.getPostDetailFabLandscapeX(display), 0), postDetailsSharedPreferences.getFloat(SharedPreferencesUtils.getPostDetailFabLandscapeY(display), 0)); } } } public void resetCoordinates() { if (portrait) { if (postDetailsSharedPreferences != null) { postDetailsSharedPreferences .edit() .remove(SharedPreferencesUtils.getPostDetailFabPortraitX(display)) .remove(SharedPreferencesUtils.getPostDetailFabPortraitY(display)) .apply(); } } else { if (postDetailsSharedPreferences != null) { postDetailsSharedPreferences .edit() .remove(SharedPreferencesUtils.getPostDetailFabLandscapeX(display)) .remove(SharedPreferencesUtils.getPostDetailFabLandscapeY(display)) .apply(); } } setTranslationX(0); setTranslationY(0); } private void saveCoordinates(float x, float y) { if (postDetailsSharedPreferences == null) { return; } if (portrait) { postDetailsSharedPreferences.edit().putFloat(SharedPreferencesUtils.getPostDetailFabPortraitX(display), x) .putFloat(SharedPreferencesUtils.getPostDetailFabPortraitY(display), y) .apply(); } else { postDetailsSharedPreferences.edit().putFloat(SharedPreferencesUtils.getPostDetailFabLandscapeX(display), x) .putFloat(SharedPreferencesUtils.getPostDetailFabLandscapeY(display), y) .apply(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/NavigationWrapper.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import androidx.appcompat.view.menu.MenuItemImpl; import androidx.core.view.MenuItemCompat; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeUtils; import com.google.android.material.badge.ExperimentalBadgeUtils; import com.google.android.material.bottomappbar.BottomAppBar; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.navigationrail.NavigationRailView; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class NavigationWrapper { public BottomAppBar bottomAppBar; public LinearLayout linearLayoutBottomAppBar; public ImageView option1BottomAppBar; public ImageView option2BottomAppBar; public ImageView option3BottomAppBar; public ImageView option4BottomAppBar; public NavigationRailView navigationRailView; public FloatingActionButton floatingActionButton; private CustomThemeWrapper customThemeWrapper; private int option1 = -1; private int option2 = -1; private int option3 = -1; private int option4 = -1; private int inboxCount; private BadgeDrawable badgeDrawable; public NavigationWrapper(BottomAppBar bottomAppBar, LinearLayout linearLayoutBottomAppBar, ImageView option1BottomAppBar, ImageView option2BottomAppBar, ImageView option3BottomAppBar, ImageView option4BottomAppBar, FloatingActionButton floatingActionButton, NavigationRailView navigationRailView, CustomThemeWrapper customThemeWrapper, boolean showBottomAppBar) { this.bottomAppBar = bottomAppBar; this.linearLayoutBottomAppBar = linearLayoutBottomAppBar; this.option1BottomAppBar = option1BottomAppBar; this.option2BottomAppBar = option2BottomAppBar; this.option3BottomAppBar = option3BottomAppBar; this.option4BottomAppBar = option4BottomAppBar; this.navigationRailView = navigationRailView; this.customThemeWrapper = customThemeWrapper; if (navigationRailView != null) { if (showBottomAppBar) { this.floatingActionButton = (FloatingActionButton) navigationRailView.getHeaderView(); } else { navigationRailView.setVisibility(View.GONE); this.floatingActionButton = floatingActionButton; } } else { this.floatingActionButton = floatingActionButton; } } public void applyCustomTheme(int bottomAppBarIconColor, int bottomAppBarBackgroundColor) { if (navigationRailView == null) { option1BottomAppBar.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); option2BottomAppBar.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); option3BottomAppBar.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); option4BottomAppBar.setColorFilter(bottomAppBarIconColor, android.graphics.PorterDuff.Mode.SRC_IN); bottomAppBar.setBackgroundTint(ColorStateList.valueOf(bottomAppBarBackgroundColor)); } else { navigationRailView.setBackgroundColor(bottomAppBarBackgroundColor); applyMenuItemTheme(navigationRailView.getMenu(), bottomAppBarIconColor); } } @SuppressLint("RestrictedApi") private void applyMenuItemTheme(Menu menu, int bottomAppBarIconColor) { for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); if (((MenuItemImpl) item).requestsActionButton()) { MenuItemCompat.setIconTintList(item, ColorStateList.valueOf(bottomAppBarIconColor)); } } } public void bindOptionDrawableResource(int... imageResources) { if (navigationRailView == null) { bottomAppBar.setVisibility(View.VISIBLE); } else { navigationRailView.setVisibility(View.VISIBLE); } if (imageResources.length == 2) { if (navigationRailView == null) { linearLayoutBottomAppBar.setWeightSum(3); option1BottomAppBar.setVisibility(View.GONE); option3BottomAppBar.setVisibility(View.GONE); option2BottomAppBar.setImageResource(imageResources[0]); option4BottomAppBar.setImageResource(imageResources[1]); } else { Menu menu = navigationRailView.getMenu(); menu.findItem(R.id.navigation_rail_option_1).setIcon(imageResources[0]); menu.findItem(R.id.navigation_rail_option_2).setIcon(imageResources[1]); menu.findItem(R.id.navigation_rail_option_3).setVisible(false); menu.findItem(R.id.navigation_rail_option_4).setVisible(false); } } else { if (navigationRailView == null) { option1BottomAppBar.setImageResource(imageResources[0]); option2BottomAppBar.setImageResource(imageResources[1]); option3BottomAppBar.setImageResource(imageResources[2]); option4BottomAppBar.setImageResource(imageResources[3]); } else { Menu menu = navigationRailView.getMenu(); menu.findItem(R.id.navigation_rail_option_1).setIcon(imageResources[0]); menu.findItem(R.id.navigation_rail_option_2).setIcon(imageResources[1]); menu.findItem(R.id.navigation_rail_option_3).setIcon(imageResources[2]); menu.findItem(R.id.navigation_rail_option_4).setIcon(imageResources[3]); } } } public void bindOptions(int... options) { if (options.length == 2) { if (navigationRailView == null) { option2 = options[0]; option4 = options[1]; } else { option1 = options[0]; option2 = options[1]; } } else { option1 = options[0]; option2 = options[1]; option3 = options[2]; option4 = options[3]; } } public void setOtherActivitiesContentDescription(Context context, View view, int option) { switch (option) { case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME: view.setContentDescription(context.getString(R.string.content_description_home)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS: view.setContentDescription(context.getString(R.string.content_description_subscriptions)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX: view.setContentDescription(context.getString(R.string.content_description_inbox)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE: view.setContentDescription(context.getString(R.string.content_description_profile)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS: view.setContentDescription(context.getString(R.string.content_description_multireddits)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS: view.setContentDescription(context.getString(R.string.content_description_submit_post)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH: view.setContentDescription(context.getString(R.string.content_description_refresh)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE: view.setContentDescription(context.getString(R.string.content_description_change_sort_type)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT: view.setContentDescription(context.getString(R.string.content_description_change_post_layout)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH: view.setContentDescription(context.getString(R.string.content_description_search)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT: view.setContentDescription(context.getString(R.string.content_description_go_to_subreddit)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER: view.setContentDescription(context.getString(R.string.content_description_go_to_user)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS: view.setContentDescription(context.getString(R.string.content_description_hide_read_posts)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS: view.setContentDescription(context.getString(R.string.content_description_filter_posts)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED: view.setContentDescription(context.getString(R.string.content_description_upvoted)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED: view.setContentDescription(context.getString(R.string.content_description_downvoted)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN: view.setContentDescription(context.getString(R.string.content_description_hidden)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED: view.setContentDescription(context.getString(R.string.content_description_saved)); break; case SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP: default: view.setContentDescription(context.getString(R.string.content_description_go_to_top)); break; } } public void showNavigation() { if (bottomAppBar != null) { bottomAppBar.performShow(); } } public void hideNavigation() { if (bottomAppBar != null) { bottomAppBar.performHide(); } } public void showFab() { if (navigationRailView == null) { floatingActionButton.show(); } } public void hideFab() { if (navigationRailView == null) { floatingActionButton.hide(); } } @ExperimentalBadgeUtils public void setInboxCount(Context context, int inboxCount) { if (inboxCount < 0) { this.inboxCount = Math.max(0, this.inboxCount + inboxCount); } else { this.inboxCount = inboxCount; } if (option1 == SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX || option1 == SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX) { if (navigationRailView == null) { if (this.inboxCount == 0) { BadgeUtils.detachBadgeDrawable(badgeDrawable, option1BottomAppBar); badgeDrawable = null; } else { BadgeUtils.attachBadgeDrawable(getBadgeDrawable(context, inboxCount, option1BottomAppBar), option1BottomAppBar); } } } else if (option2 == SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX || option2 == SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX) { if (navigationRailView == null) { if (this.inboxCount == 0) { BadgeUtils.detachBadgeDrawable(badgeDrawable, option2BottomAppBar); badgeDrawable = null; } else { BadgeUtils.attachBadgeDrawable(getBadgeDrawable(context, inboxCount, option2BottomAppBar), option2BottomAppBar); } } } else if (option3 == SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX || option3 == SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX) { if (navigationRailView == null) { if (this.inboxCount == 0) { BadgeUtils.detachBadgeDrawable(badgeDrawable, option3BottomAppBar); badgeDrawable = null; } else { BadgeUtils.attachBadgeDrawable(getBadgeDrawable(context, inboxCount, option3BottomAppBar), option3BottomAppBar); } } } else if (option4 == SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX || option4 == SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX) { if (navigationRailView == null) { if (this.inboxCount == 0) { BadgeUtils.detachBadgeDrawable(badgeDrawable, option4BottomAppBar); badgeDrawable = null; } else { BadgeUtils.attachBadgeDrawable(getBadgeDrawable(context, inboxCount, option4BottomAppBar), option4BottomAppBar); } } } } private BadgeDrawable getBadgeDrawable(Context context, int inboxCount, View anchorView) { BadgeDrawable badgeDrawable = BadgeDrawable.create(context); badgeDrawable.setNumber(inboxCount); badgeDrawable.setBackgroundColor(customThemeWrapper.getColorAccent()); badgeDrawable.setBadgeTextColor(customThemeWrapper.getButtonTextColor()); badgeDrawable.setHorizontalOffset(anchorView.getWidth() / 2); this.badgeDrawable = badgeDrawable; return badgeDrawable; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/SpoilerOnClickTextView.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.util.AttributeSet; import androidx.annotation.Nullable; public class SpoilerOnClickTextView extends androidx.appcompat.widget.AppCompatTextView { private boolean isSpoilerOnClick; public SpoilerOnClickTextView(Context context) { super(context); } public SpoilerOnClickTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public SpoilerOnClickTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public boolean isSpoilerOnClick() { return isSpoilerOnClick; } public void setSpoilerOnClick(boolean spoilerOnClick) { isSpoilerOnClick = spoilerOnClick; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/SwipeLockInterface.java ================================================ package ml.docilealligator.infinityforreddit.customviews; public interface SwipeLockInterface { void lockSwipe(); void unlockSwipe(); default void setSwipeLocked(boolean swipeLocked) {} } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/SwipeLockLinearLayout.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class SwipeLockLinearLayout extends LinearLayout implements SwipeLockView { @Nullable private SwipeLockInterface swipeLockInterface = null; private boolean locked = false; public SwipeLockLinearLayout(@NonNull Context context) { super(context); } public SwipeLockLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public SwipeLockLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public SwipeLockLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void setSwipeLockInterface(@Nullable SwipeLockInterface swipeLockInterface) { this.swipeLockInterface = swipeLockInterface; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { updateSwipeLock(ev); return locked; } @SuppressLint("ClickableViewAccessibility") // we are just listening to touch events @Override public boolean onTouchEvent(MotionEvent ev) { updateSwipeLock(ev); return super.onTouchEvent(ev); } /** * Unlocks swipe if the view cannot be scrolled right anymore or if {@code ev} is * {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} */ private void updateSwipeLock(MotionEvent ev) { if (swipeLockInterface != null) { int action = ev.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { // calling SlidrInterface#unlock aborts the swipe // so don't call unlock if it is already unlocked if (locked) { swipeLockInterface.unlockSwipe(); locked = false; } } else { if (!locked) { swipeLockInterface.lockSwipe(); locked = true; } } swipeLockInterface.setSwipeLocked(locked); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/SwipeLockLinearLayoutManager.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.view.View; import androidx.annotation.Nullable; public class SwipeLockLinearLayoutManager extends LinearLayoutManagerBugFixed { @Nullable private final SwipeLockInterface swipeLockInterface; public SwipeLockLinearLayoutManager(Context context, @Nullable SwipeLockInterface swipeLockInterface) { super(context); this.swipeLockInterface = swipeLockInterface; } public SwipeLockLinearLayoutManager(Context context, int orientation, boolean reverseLayout, @Nullable SwipeLockInterface swipeLockInterface) { super(context, orientation, reverseLayout); this.swipeLockInterface = swipeLockInterface; } @Override public void addView(View child) { super.addView(child); if (child instanceof SwipeLockView) { ((SwipeLockView) child).setSwipeLockInterface(swipeLockInterface); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/SwipeLockView.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import androidx.annotation.Nullable; public interface SwipeLockView { void setSwipeLockInterface(@Nullable SwipeLockInterface swipeLockInterface); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/TableHorizontalScrollView.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewParent; import android.widget.HorizontalScrollView; import androidx.annotation.Nullable; import androidx.viewpager2.widget.ViewPager2; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; public class TableHorizontalScrollView extends HorizontalScrollView { @Nullable private CustomToroContainer toroContainer; @Nullable private ViewPager2 viewPager2; @Nullable private SliderPanel sliderPanel; private float lastX = 0.0f; private float lastY = 0.0f; private boolean allowScroll; private boolean isViewPager2Enabled; private int touchSlop; public TableHorizontalScrollView(Context context) { super(context); init(context); } public TableHorizontalScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public TableHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } public TableHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context); } private void init(Context context) { new Handler(Looper.getMainLooper()).post(() -> { ViewParent parent = getParent(); while (parent != null) { if (parent instanceof CustomToroContainer) { toroContainer = (CustomToroContainer) parent; } else if (parent instanceof ViewPager2) { viewPager2 = (ViewPager2) parent; isViewPager2Enabled = viewPager2.isUserInputEnabled(); } else if (parent instanceof SliderPanel) { sliderPanel = (SliderPanel) parent; } parent = parent.getParent(); } touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); }); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { processMotionEvent(ev); return allowScroll || super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { processMotionEvent(ev); return super.onTouchEvent(ev); } private void processMotionEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: lastX = ev.getX(); lastY = ev.getY(); if (toroContainer != null) { toroContainer.requestDisallowInterceptTouchEvent(true); } if (viewPager2 != null && isViewPager2Enabled) { viewPager2.setUserInputEnabled(false); } if (sliderPanel != null) { sliderPanel.lock(); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: allowScroll = false; if (toroContainer != null) { toroContainer.requestDisallowInterceptTouchEvent(false); } if (viewPager2 != null && isViewPager2Enabled) { viewPager2.setUserInputEnabled(true); } if (sliderPanel != null) { sliderPanel.unlock(); } break; case MotionEvent.ACTION_MOVE: float currentX = ev.getX(); float currentY = ev.getY(); float dx = Math.abs(currentX - lastX); float dy = Math.abs(currentY - lastY); allowScroll = dy < dx && dy > touchSlop && dx > touchSlop; if (toroContainer != null) { toroContainer.requestDisallowInterceptTouchEvent(allowScroll); } if (viewPager2 != null && isViewPager2Enabled) { viewPager2.setUserInputEnabled(false); } if (sliderPanel != null) { sliderPanel.lock(); } break; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/ThemedMaterialSwitch.kt ================================================ package ml.docilealligator.infinityforreddit.customviews import android.R import android.content.Context import android.content.res.ColorStateList import android.util.AttributeSet import com.google.android.material.materialswitch.MaterialSwitch import ml.docilealligator.infinityforreddit.Infinity import ml.docilealligator.infinityforreddit.utils.deriveContrastingColor class ThemedMaterialSwitch @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = com.google.android.material.R.attr.materialSwitchStyle ): MaterialSwitch(context, attrs, defStyleAttr) { init { val app = context.applicationContext if (app is Infinity) { val customThemeWrapper = (context.applicationContext as Infinity).customThemeWrapper setThumbTintList(ColorStateList.valueOf(customThemeWrapper.colorAccent)) val states = arrayOf( intArrayOf(R.attr.state_checked) ) val colors = intArrayOf( deriveContrastingColor(customThemeWrapper.colorAccent) ) setTrackTintList(ColorStateList(states, colors)) } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/TouchInterceptableMaterialCardView.kt ================================================ package ml.docilealligator.infinityforreddit.customviews import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import com.google.android.material.card.MaterialCardView class TouchInterceptableMaterialCardView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : MaterialCardView(context, attrs) { private var mShouldInterceptTouchEvent: Boolean = false override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { return mShouldInterceptTouchEvent || super.onInterceptTouchEvent(ev) } public fun setShouldInterceptTouch(value: Boolean) { mShouldInterceptTouchEvent = value } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/ViewPagerBugFixed.java ================================================ package ml.docilealligator.infinityforreddit.customviews; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager.widget.ViewPager; public class ViewPagerBugFixed extends ViewPager { public ViewPagerBugFixed(@NonNull Context context) { super(context); } public ViewPagerBugFixed(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent ev) { try { return super.onTouchEvent(ev); } catch (IllegalArgumentException ignore) {} return false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException ignore) {} return false; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/compose/AppTheme.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.compose import android.content.Context import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.flow.onEach import ml.docilealligator.infinityforreddit.Infinity import ml.docilealligator.infinityforreddit.customtheme.CustomTheme import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper import ml.docilealligator.infinityforreddit.customtheme.LocalCustomThemeRepository import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils val LocalAppTheme = staticCompositionLocalOf { error("No default theme values") } @Composable fun AppTheme(themeType: Int, content: @Composable () -> Unit) { val context = LocalContext.current val localCustomThemeRepository = LocalCustomThemeRepository(((context.applicationContext) as Infinity).mRedditDataRoomDatabase) var themeLoaded by remember { mutableStateOf(false) } val currentThemeFlow = when(themeType) { CustomThemeSharedPreferencesUtils.LIGHT -> localCustomThemeRepository.currentLightCustomThemeFlow CustomThemeSharedPreferencesUtils.DARK -> localCustomThemeRepository.currentDarkCustomThemeFlow CustomThemeSharedPreferencesUtils.AMOLED -> localCustomThemeRepository.currentAmoledCustomThemeFlow else -> localCustomThemeRepository.currentLightCustomThemeFlow }.onEach { themeLoaded = true } val customTheme by currentThemeFlow.collectAsState(initial = null) if (themeLoaded) { CompositionLocalProvider(LocalAppTheme provides (customTheme ?: getDefaultTheme(context, themeType))) { MaterialTheme { content() } } } } private fun getDefaultTheme(context: Context, themeType: Int): CustomTheme { return when(themeType) { CustomThemeSharedPreferencesUtils.LIGHT -> CustomThemeWrapper.getIndigo(context) CustomThemeSharedPreferencesUtils.DARK -> CustomThemeWrapper.getIndigoDark(context) CustomThemeSharedPreferencesUtils.AMOLED -> CustomThemeWrapper.getIndigoAmoled(context) else -> CustomThemeWrapper.getIndigo(context) } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/compose/CustomAppBar.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.compose import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.res.stringResource import androidx.core.view.WindowInsetsControllerCompat import ml.docilealligator.infinityforreddit.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun ThemedTopAppBar( modifier: Modifier = Modifier, titleStringResId: Int, isImmersiveInterfaceEnabled: Boolean, scrollBehavior: TopAppBarScrollBehavior, windowInsetsController: WindowInsetsControllerCompat, actions: @Composable RowScope.() -> Unit = {}, onBack: () -> Unit ) { val customTheme = LocalAppTheme.current val appBarColor = lerp( start = Color(customTheme.colorPrimary), stop = Color.Transparent, fraction = scrollBehavior.state.collapsedFraction ) if (isImmersiveInterfaceEnabled) { LaunchedEffect(scrollBehavior.state.collapsedFraction) { if (customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface) { windowInsetsController.isAppearanceLightStatusBars = if (scrollBehavior.state.collapsedFraction > 0.5f) !customTheme.isLightStatusBar else customTheme.isLightStatusBar } } } TopAppBar( modifier = modifier, colors = TopAppBarDefaults.topAppBarColors( containerColor = appBarColor, scrolledContainerColor = appBarColor, titleContentColor = Color(LocalAppTheme.current.toolbarPrimaryTextAndIconColor), ), title = { Text(stringResource(titleStringResId)) }, navigationIcon = { IconButton(onClick = onBack) { ToolbarIcon( contentDescription = stringResource(R.string.action_back_content_description) ) } }, actions = actions, scrollBehavior = scrollBehavior, windowInsets = if (isImmersiveInterfaceEnabled) TopAppBarDefaults.windowInsets else WindowInsets(0, 0, 0, 0) ) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/compose/CustomImage.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.compose import androidx.compose.foundation.Image import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ml.docilealligator.infinityforreddit.R @Composable fun PrimaryIcon(modifier: Modifier = Modifier, drawableId: Int, contentDescription: String) { Image( modifier = modifier, painter = painterResource(drawableId), contentDescription = contentDescription, colorFilter = ColorFilter.tint(Color(LocalAppTheme.current.primaryIconColor)) ) } @Composable fun ToolbarIcon(modifier: Modifier = Modifier, drawableId: Int = R.drawable.ic_arrow_back_24dp, contentDescription: String) { Image( modifier = modifier.size(24.dp), painter = painterResource(drawableId), contentDescription = contentDescription, colorFilter = ColorFilter.tint(Color(LocalAppTheme.current.toolbarPrimaryTextAndIconColor)) ) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/compose/CustomLoadingIndicator.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.compose import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LoadingIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun CustomLoadingIndicator(modifier: Modifier = Modifier) { LoadingIndicator( modifier = modifier, color = Color(LocalAppTheme.current.colorAccent) ) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/compose/CustomSwitch.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.compose import android.R.attr.checked import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.google.common.math.LinearTransformation.horizontal import ml.docilealligator.infinityforreddit.utils.deriveContrastingColor @Composable fun SwitchRow( modifier: Modifier = Modifier, checked: Boolean, title: String, subTitle: String? = null, onCheckedChange: (Boolean) -> Unit ) { Row( modifier = modifier .fillMaxWidth() .clickable { onCheckedChange(!checked); } .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = title, color = Color(LocalAppTheme.current.primaryTextColor) ) Spacer(modifier = Modifier.weight(1f)) Switch( checked = checked, onCheckedChange = onCheckedChange, colors = SwitchDefaults.colors( checkedThumbColor = Color(LocalAppTheme.current.colorAccent), checkedTrackColor = Color(deriveContrastingColor(LocalAppTheme.current.colorAccent)) ) ) } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/compose/CustomText.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.compose import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @Composable fun PrimaryText(stringResourceId: Int, textAlign: TextAlign? = null) { Text( stringResource(stringResourceId), color = Color(LocalAppTheme.current.primaryTextColor), textAlign = textAlign ) } @Composable fun PrimaryText(text: String, textAlign: TextAlign? = null) { Text( text, color = Color(LocalAppTheme.current.primaryTextColor), textAlign = textAlign ) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/compose/CustomTextField.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.compose import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @Composable fun CustomTextField( modifier: Modifier = Modifier, state: TextFieldState, placeholder: String, lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default ) { OutlinedTextField( modifier = modifier, state = state, placeholder = { Text( text = placeholder, color = Color(LocalAppTheme.current.secondaryTextColor) ) }, lineLimits = lineLimits, colors = OutlinedTextFieldDefaults.colors( focusedTextColor = Color(LocalAppTheme.current.primaryTextColor), unfocusedTextColor = Color(LocalAppTheme.current.primaryTextColor), focusedBorderColor = Color(LocalAppTheme.current.primaryTextColor), unfocusedBorderColor = Color(LocalAppTheme.current.secondaryTextColor), cursorColor = Color(LocalAppTheme.current.colorPrimary) ) ) } @Composable fun CustomTextField( modifier: Modifier = Modifier, value: String, placeholder: String, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, onValueChange: (String) -> Unit ) { OutlinedTextField( value = value, modifier = modifier, placeholder = { Text( text = placeholder, color = Color(LocalAppTheme.current.secondaryTextColor) ) }, singleLine = singleLine, maxLines = maxLines, minLines = minLines, colors = OutlinedTextFieldDefaults.colors( focusedTextColor = Color(LocalAppTheme.current.primaryTextColor), unfocusedTextColor = Color(LocalAppTheme.current.primaryTextColor), focusedBorderColor = Color(LocalAppTheme.current.primaryTextColor), unfocusedBorderColor = Color(LocalAppTheme.current.secondaryTextColor), cursorColor = Color(LocalAppTheme.current.colorPrimary) ), onValueChange = onValueChange ) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomFontEditTextPreference.java ================================================ package ml.docilealligator.infinityforreddit.customviews.preference; import android.content.Context; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import androidx.preference.EditTextPreference; import androidx.preference.PreferenceViewHolder; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapperReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; public class CustomFontEditTextPreference extends EditTextPreference implements CustomFontReceiver, CustomThemeWrapperReceiver { private CustomThemeWrapper customThemeWrapper; private Typeface typeface; public CustomFontEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public CustomFontEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CustomFontEditTextPreference(Context context, AttributeSet attrs) { super(context, attrs); } public CustomFontEditTextPreference(Context context) { super(context); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); View iconImageView = holder.findViewById(android.R.id.icon); View titleTextView = holder.findViewById(android.R.id.title); View summaryTextView = holder.findViewById(android.R.id.summary); if (customThemeWrapper != null) { if (iconImageView instanceof ImageView) { if (isEnabled()) { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getPrimaryIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } else { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getSecondaryTextColor(), android.graphics.PorterDuff.Mode.SRC_IN); } } if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTextColor(customThemeWrapper.getPrimaryTextColor()); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTextColor(customThemeWrapper.getSecondaryTextColor()); } } if (typeface != null) { if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTypeface(typeface); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTypeface(typeface); } } } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } @Override public void setCustomThemeWrapper(CustomThemeWrapper customThemeWrapper) { this.customThemeWrapper = customThemeWrapper; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomFontListPreference.java ================================================ package ml.docilealligator.infinityforreddit.customviews.preference; import android.content.Context; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.preference.ListPreference; import androidx.preference.PreferenceViewHolder; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapperReceiver; public class CustomFontListPreference extends ListPreference implements CustomFontReceiver, CustomThemeWrapperReceiver { private CustomThemeWrapper customThemeWrapper; private Typeface typeface; public CustomFontListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public CustomFontListPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CustomFontListPreference(Context context, AttributeSet attrs) { super(context, attrs); } public CustomFontListPreference(Context context) { super(context); } @Override public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); View iconImageView = holder.findViewById(android.R.id.icon); View titleTextView = holder.findViewById(android.R.id.title); View summaryTextView = holder.findViewById(android.R.id.summary); if (customThemeWrapper != null) { if (iconImageView instanceof ImageView) { if (isEnabled()) { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getPrimaryIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } else { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getSecondaryTextColor(), android.graphics.PorterDuff.Mode.SRC_IN); } } if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTextColor(customThemeWrapper.getPrimaryTextColor()); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTextColor(customThemeWrapper.getSecondaryTextColor()); } } if (typeface != null) { if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTypeface(typeface); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTypeface(typeface); } } } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } @Override public void setCustomThemeWrapper(CustomThemeWrapper customThemeWrapper) { this.customThemeWrapper = customThemeWrapper; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomFontPreference.java ================================================ package ml.docilealligator.infinityforreddit.customviews.preference; import android.content.Context; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapperReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; public class CustomFontPreference extends Preference implements CustomFontReceiver, CustomThemeWrapperReceiver { private CustomThemeWrapper customThemeWrapper; private Typeface typeface; public CustomFontPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public CustomFontPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CustomFontPreference(Context context, AttributeSet attrs) { super(context, attrs); } public CustomFontPreference(Context context) { super(context); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); View iconImageView = holder.findViewById(android.R.id.icon); View titleTextView = holder.findViewById(android.R.id.title); View summaryTextView = holder.findViewById(android.R.id.summary); if (customThemeWrapper != null) { if (iconImageView instanceof ImageView) { if (isEnabled()) { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getPrimaryIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } else { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getSecondaryTextColor(), android.graphics.PorterDuff.Mode.SRC_IN); } } if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTextColor(customThemeWrapper.getPrimaryTextColor()); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTextColor(customThemeWrapper.getSecondaryTextColor()); } } if (typeface != null) { if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTypeface(typeface); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTypeface(typeface); } } } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } @Override public void setCustomThemeWrapper(CustomThemeWrapper customThemeWrapper) { this.customThemeWrapper = customThemeWrapper; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomFontPreferenceCategory.java ================================================ package ml.docilealligator.infinityforreddit.customviews.preference; import android.content.Context; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceViewHolder; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapperReceiver; public class CustomFontPreferenceCategory extends PreferenceCategory implements CustomFontReceiver, CustomThemeWrapperReceiver { private CustomThemeWrapper customThemeWrapper; private Typeface typeface; public CustomFontPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public CustomFontPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CustomFontPreferenceCategory(Context context, AttributeSet attrs) { super(context, attrs); } public CustomFontPreferenceCategory(Context context) { super(context); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); View titleTextView = holder.findViewById(android.R.id.title); holder.setDividerAllowedAbove(false); /*View iconView = holder.findViewById(R.id.icon_frame); if (iconView != null) { iconView.setVisibility(View.GONE); }*/ if (customThemeWrapper != null) { if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTextColor(customThemeWrapper.getColorAccent()); } } if (typeface != null) { if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTypeface(typeface); } } } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } @Override public void setCustomThemeWrapper(CustomThemeWrapper customThemeWrapper) { this.customThemeWrapper = customThemeWrapper; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomFontPreferenceFragmentCompat.java ================================================ package ml.docilealligator.infinityforreddit.customviews.preference; import android.content.Context; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.DialogFragment; import androidx.preference.EditTextPreference; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapperReceiver; import ml.docilealligator.infinityforreddit.utils.Utils; public abstract class CustomFontPreferenceFragmentCompat extends PreferenceFragmentCompat implements PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback { private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; protected SettingsActivity mActivity; protected View view; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); this.view = view; applyStyle(); } protected void applyStyle() { PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen == null) return; int preferenceCount = preferenceScreen.getPreferenceCount(); for (int i = 0; i < preferenceCount; i++) { Preference preference = preferenceScreen.getPreference(i); if (preference instanceof CustomThemeWrapperReceiver) { ((CustomThemeWrapperReceiver) preference).setCustomThemeWrapper(mActivity.customThemeWrapper); } if (preference instanceof CustomFontReceiver) { ((CustomFontReceiver) preference).setCustomFont(mActivity.typeface, null, null); } } view.setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { View recyclerView = getListView(); if (recyclerView != null) { ViewCompat.setOnApplyWindowInsetsListener(view, new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); recyclerView.setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } } } @Override public boolean onPreferenceDisplayDialog(@NonNull PreferenceFragmentCompat caller, @NonNull Preference pref) { if (pref instanceof ListPreference) { DialogFragment f = CustomStyleListPreferenceDialogFragmentCompat.newInstance(pref.getKey()); f.setTargetFragment(this, 0); f.show(getParentFragmentManager(), DIALOG_FRAGMENT_TAG); return true; } else if (pref instanceof EditTextPreference) { DialogFragment f = CustomStyleEditTextPreferenceDialogFragmentCompat.newInstance(pref.getKey()); f.setTargetFragment(this, 0); f.show(getParentFragmentManager(), DIALOG_FRAGMENT_TAG); return true; } return false; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomFontPreferenceWithBackground.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.preference import android.content.Context import android.content.res.ColorStateList import android.graphics.PorterDuff import android.graphics.Typeface import android.util.AttributeSet import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.widget.ImageView import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import ml.docilealligator.infinityforreddit.CustomFontReceiver import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapperReceiver import ml.docilealligator.infinityforreddit.utils.Utils class CustomFontPreferenceWithBackground @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, defStyleRes: Int = 0 ) : Preference(context, attrs, defStyleAttr, defStyleRes), CustomFontReceiver, CustomThemeWrapperReceiver { private var customThemeWrapper: CustomThemeWrapper? = null private var typeface: Typeface? = null private var top = false private var bottom = false init { context.theme.obtainStyledAttributes( attrs, R.styleable.CustomFontPreference, 0, 0 ).let { a -> try { top = a.getBoolean(R.styleable.CustomFontPreference_top, false) bottom = a.getBoolean(R.styleable.CustomFontPreference_bottom, false) } finally { a.recycle() } } } override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) val margin16 = Utils.convertDpToPixel( 16f, context ).toInt() val margin2 = Utils.convertDpToPixel( 2f, context ).toInt() if (top) { if (bottom) { holder.itemView.background = AppCompatResources.getDrawable( context, R.drawable.preference_background_top_and_bottom ) } else { holder.itemView.background = AppCompatResources.getDrawable(context, R.drawable.preference_background_top) } setMargins(holder.itemView, margin16, margin16, margin16, -1) } else if (bottom) { holder.itemView.background = AppCompatResources.getDrawable(context, R.drawable.preference_background_bottom) setMargins(holder.itemView, margin16, margin2, margin16, -1) } else { holder.itemView.background = AppCompatResources.getDrawable(context, R.drawable.preference_background_middle) setMargins(holder.itemView, margin16, margin2, margin16, -1) } val iconImageView = holder.findViewById(android.R.id.icon) val titleTextView = holder.findViewById(android.R.id.title) val summaryTextView = holder.findViewById(android.R.id.summary) customThemeWrapper?.let { holder.itemView.backgroundTintList = ColorStateList.valueOf(it.filledCardViewBackgroundColor) if (iconImageView is ImageView) { if (isEnabled) { iconImageView.setColorFilter( it.primaryIconColor, PorterDuff.Mode.SRC_IN ) } else { iconImageView.setColorFilter( it.secondaryTextColor, PorterDuff.Mode.SRC_IN ) } } if (titleTextView is TextView) { titleTextView.setTextColor(it.primaryTextColor) } if (summaryTextView is TextView) { summaryTextView.setTextColor(it.secondaryTextColor) } } if (typeface != null) { if (titleTextView is TextView) { titleTextView.setTypeface(typeface) } if (summaryTextView is TextView) { summaryTextView.setTypeface(typeface) } } } override fun setCustomFont( typeface: Typeface?, titleTypeface: Typeface?, contentTypeface: Typeface? ) { this.typeface = typeface } override fun setCustomThemeWrapper(customThemeWrapper: CustomThemeWrapper) { this.customThemeWrapper = customThemeWrapper } fun setTop(top: Boolean) { this.top = top } companion object { fun setMargins(view: T, left: Int, top: Int, right: Int, bottom: Int) { val lp = view!!.layoutParams if (lp is MarginLayoutParams) { val marginParams = lp if (top >= 0) { marginParams.topMargin = top } if (bottom >= 0) { marginParams.bottomMargin = bottom } if (left >= 0) { marginParams.marginStart = left } if (right >= 0) { marginParams.marginEnd = right } view.layoutParams = marginParams } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomFontSwitchPreference.java ================================================ package ml.docilealligator.infinityforreddit.customviews.preference; import android.content.Context; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.preference.PreferenceViewHolder; import androidx.preference.SwitchPreference; import com.google.android.material.materialswitch.MaterialSwitch; import ml.docilealligator.infinityforreddit.CustomFontReceiver; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapperReceiver; public class CustomFontSwitchPreference extends SwitchPreference implements CustomFontReceiver, CustomThemeWrapperReceiver { private CustomThemeWrapper customThemeWrapper; private Typeface typeface; private MaterialSwitch materialSwitch; public CustomFontSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setWidgetLayoutResource(R.layout.preference_switch); } public CustomFontSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setWidgetLayoutResource(R.layout.preference_switch); } public CustomFontSwitchPreference(Context context, AttributeSet attrs) { super(context, attrs); setWidgetLayoutResource(R.layout.preference_switch); } public CustomFontSwitchPreference(Context context) { super(context); setWidgetLayoutResource(R.layout.preference_switch); } @Override public void setChecked(boolean checked) { super.setChecked(checked); if (materialSwitch != null) { materialSwitch.setChecked(checked); } } @Override public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); View iconImageView = holder.findViewById(android.R.id.icon); View titleTextView = holder.findViewById(android.R.id.title); View summaryTextView = holder.findViewById(android.R.id.summary); materialSwitch = (MaterialSwitch) holder.findViewById(R.id.material_switch_switch_preference); materialSwitch.setChecked(isChecked()); materialSwitch.setOnClickListener(view -> { onClick(); }); if (customThemeWrapper != null) { if (iconImageView instanceof ImageView) { if (isEnabled()) { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getPrimaryIconColor(), android.graphics.PorterDuff.Mode.SRC_IN); } else { ((ImageView) iconImageView).setColorFilter(customThemeWrapper.getSecondaryTextColor(), android.graphics.PorterDuff.Mode.SRC_IN); } } if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTextColor(customThemeWrapper.getPrimaryTextColor()); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTextColor(customThemeWrapper.getSecondaryTextColor()); } } if (typeface != null) { if (titleTextView instanceof TextView) { ((TextView) titleTextView).setTypeface(typeface); } if (summaryTextView instanceof TextView) { ((TextView) summaryTextView).setTypeface(typeface); } } } @Override public void setCustomFont(Typeface typeface, Typeface titleTypeface, Typeface contentTypeface) { this.typeface = typeface; } @Override public void setCustomThemeWrapper(CustomThemeWrapper customThemeWrapper) { this.customThemeWrapper = customThemeWrapper; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomStyleEditTextPreferenceDialogFragmentCompat.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.preference import android.app.Dialog import android.os.Bundle import androidx.preference.EditTextPreferenceDialogFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import ml.docilealligator.infinityforreddit.R class CustomStyleEditTextPreferenceDialogFragmentCompat : EditTextPreferenceDialogFragmentCompat() { companion object { @JvmStatic fun newInstance(key: String?): CustomStyleEditTextPreferenceDialogFragmentCompat { val fragment = CustomStyleEditTextPreferenceDialogFragmentCompat() val b = Bundle(1) b.putString(ARG_KEY, key) fragment.arguments = b return fragment } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialogTheme) .setTitle(preference.dialogTitle) .setIcon(preference.dialogIcon) .setPositiveButton(preference.positiveButtonText, this) .setNegativeButton(preference.negativeButtonText, this) val contentView = onCreateDialogView(requireContext()) if (contentView != null) { onBindDialogView(contentView) builder.setView(contentView) } else { builder.setMessage(preference.dialogMessage) } onPrepareDialogBuilder(builder) return builder.create() } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/CustomStyleListPreferenceDialogFragmentCompat.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.preference import android.app.Dialog import android.os.Bundle import androidx.preference.ListPreferenceDialogFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import ml.docilealligator.infinityforreddit.R class CustomStyleListPreferenceDialogFragmentCompat : ListPreferenceDialogFragmentCompat() { companion object { @JvmStatic fun newInstance(key: String?): CustomStyleListPreferenceDialogFragmentCompat { val fragment = CustomStyleListPreferenceDialogFragmentCompat() val b = Bundle(1) b.putString(ARG_KEY, key) fragment.arguments = b return fragment } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialogTheme) .setTitle(preference.dialogTitle) .setIcon(preference.dialogIcon) .setPositiveButton(preference.positiveButtonText, this) .setNegativeButton(preference.negativeButtonText, this) val contentView = onCreateDialogView(requireContext()) if (contentView != null) { onBindDialogView(contentView) builder.setView(contentView) } else { builder.setMessage(preference.dialogMessage) } onPrepareDialogBuilder(builder) return builder.create() } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/preference/SliderPreference.kt ================================================ package ml.docilealligator.infinityforreddit.customviews.preference import android.content.Context import android.content.res.ColorStateList import android.content.res.TypedArray import android.graphics.PorterDuff import android.graphics.Typeface import android.util.AttributeSet import android.widget.ImageView import android.widget.TextView import androidx.preference.Preference import androidx.preference.Preference.SummaryProvider import androidx.preference.PreferenceViewHolder import com.google.android.material.slider.Slider import ml.docilealligator.infinityforreddit.Infinity import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper import ml.docilealligator.infinityforreddit.utils.deriveContrastingColor class SliderPreference @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, defStyleRes: Int = 0 ) : Preference(context, attrs, defStyleAttr, defStyleRes) { var min: Int var max: Int var stepSize: Int var defaultValue: Int = 0 private val customThemeWrapper: CustomThemeWrapper? private val typeface: Typeface? init { layoutResource = R.layout.preference_slider val app = context.applicationContext if (app is Infinity) { customThemeWrapper = app.customThemeWrapper typeface = app.typeface } else { customThemeWrapper = null typeface = null } summaryProvider = SummaryProvider { preference -> preference.getPersistedInt(defaultValue).toString() } context.theme.obtainStyledAttributes( attrs, R.styleable.SliderPreference, 0, 0 ).let { try { min = it.getInt(R.styleable.SliderPreference_sliderMin, 0) max = it.getInt(R.styleable.SliderPreference_sliderMax, 100) stepSize = it.getInt(R.styleable.SliderPreference_sliderStepSize, 1) } finally { it.recycle() } } } override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) val slider = holder.findViewById(R.id.slider_preference_slider) as? Slider slider?.apply { valueFrom = min.toFloat() valueTo = max.toFloat() stepSize = this@SliderPreference.stepSize.toFloat() value = getPersistedInt(defaultValue).toFloat() addOnChangeListener { _, newValue, _ -> persistInt(newValue.toInt()) notifyChanged() } } val iconImageView = holder.findViewById(android.R.id.icon) val titleTextView = holder.findViewById(android.R.id.title) val summaryTextView = holder.findViewById(android.R.id.summary) customThemeWrapper?.let { if (iconImageView is ImageView) { if (isEnabled) { iconImageView.setColorFilter( it.primaryIconColor, PorterDuff.Mode.SRC_IN ) } else { iconImageView.setColorFilter( it.secondaryTextColor, PorterDuff.Mode.SRC_IN ) } } if (titleTextView is TextView) { titleTextView.setTextColor(it.primaryTextColor) } if (summaryTextView is TextView) { summaryTextView.setTextColor(it.secondaryTextColor) } slider?.thumbTintList = ColorStateList.valueOf(it.colorAccent) slider?.trackActiveTintList = ColorStateList.valueOf(it.colorAccent) slider?.trackInactiveTintList = ColorStateList.valueOf(deriveContrastingColor(it.colorAccent)) } if (typeface != null) { if (titleTextView is TextView) { titleTextView.setTypeface(typeface) } if (summaryTextView is TextView) { summaryTextView.setTypeface(typeface) } } } override fun onSetInitialValue(defaultValue: Any?) { if (defaultValue is Int) { this.defaultValue = defaultValue } else { this.defaultValue = 0 } notifyChanged() } override fun onGetDefaultValue(a: TypedArray, index: Int): Any? { return a.getInt(index, 0) } fun setSummaryTemplate(stringResId: Int) { summaryProvider = SummaryProvider { preference -> context.getString( stringResId, preference.getPersistedInt(defaultValue)) } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/ColorPanelSlideListener.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr; import android.animation.ArgbEvaluator; import android.app.Activity; import android.os.Build; import androidx.annotation.ColorInt; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; class ColorPanelSlideListener implements SliderPanel.OnPanelSlideListener { private final Activity activity; private final int primaryColor; private final int secondaryColor; private final ArgbEvaluator evaluator = new ArgbEvaluator(); ColorPanelSlideListener(Activity activity, @ColorInt int primaryColor, @ColorInt int secondaryColor) { this.activity = activity; this.primaryColor = primaryColor; this.secondaryColor = secondaryColor; } @Override public void onStateChanged(int state) { // Unused. } @Override public void onClosed() { activity.finish(); activity.overridePendingTransition(0, 0); } @Override public void onOpened() { // Unused. } @Override public void onSlideChange(float percent) { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && areColorsValid()){ int newColor = (int) evaluator.evaluate(percent, getPrimaryColor(), getSecondaryColor()); activity.getWindow().setStatusBarColor(newColor); } } protected int getPrimaryColor() { return primaryColor; } protected int getSecondaryColor() { return secondaryColor; } protected boolean areColorsValid() { return getPrimaryColor() != -1 && getSecondaryColor() != -1; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/ConfigPanelSlideListener.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr; import android.app.Activity; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrConfig; class ConfigPanelSlideListener extends ColorPanelSlideListener { private final SlidrConfig config; ConfigPanelSlideListener(@NonNull Activity activity, @NonNull SlidrConfig config) { super(activity, -1, -1); this.config = config; } @Override public void onStateChanged(int state) { if(config.getListener() != null){ config.getListener().onSlideStateChanged(state); } } @Override public void onClosed() { if(config.getListener() != null){ if(config.getListener().onSlideClosed()) { return; } } super.onClosed(); } @Override public void onOpened() { if(config.getListener() != null){ config.getListener().onSlideOpened(); } } @Override public void onSlideChange(float percent) { super.onSlideChange(percent); if(config.getListener() != null){ config.getListener().onSlideChange(percent); } } @Override protected int getPrimaryColor() { return config.getPrimaryColor(); } @Override protected int getSecondaryColor() { return config.getSecondaryColor(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/FragmentPanelSlideListener.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr; import android.view.View; import androidx.annotation.NonNull; import androidx.fragment.app.FragmentActivity; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrConfig; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; class FragmentPanelSlideListener implements SliderPanel.OnPanelSlideListener { private final View view; private final SlidrConfig config; FragmentPanelSlideListener(@NonNull View view, @NonNull SlidrConfig config) { this.view = view; this.config = config; } @Override public void onStateChanged(int state) { if (config.getListener() != null) { config.getListener().onSlideStateChanged(state); } } @Override public void onClosed() { if (config.getListener() != null) { if(config.getListener().onSlideClosed()) { return; } } // Ensure that we are attached to a FragmentActivity if (view.getContext() instanceof FragmentActivity) { final FragmentActivity activity = (FragmentActivity) view.getContext(); if (activity.getSupportFragmentManager().getBackStackEntryCount() == 0) { activity.finish(); activity.overridePendingTransition(0, 0); } else { activity.getSupportFragmentManager().popBackStack(); } } } @Override public void onOpened() { if (config.getListener() != null) { config.getListener().onSlideOpened(); } } @Override public void onSlideChange(float percent) { if (config.getListener() != null) { config.getListener().onSlideChange(percent); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/Slidr.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr; import android.app.Activity; import android.view.View; import android.view.ViewGroup; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrConfig; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrInterface; import ml.docilealligator.infinityforreddit.customviews.slidr.widget.SliderPanel; /** * This attacher class is used to attach the sliding mechanism to any {@link android.app.Activity} * that lets the user slide (or swipe) the activity away as a form of back or up action. The action * causes {@link android.app.Activity#finish()} to be called. */ public final class Slidr { /** * Attach a slideable mechanism to an activity that adds the slide to dismiss functionality * * @param activity the activity to attach the slider to * @return a {@link ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrInterface} that allows * the user to lock/unlock the sliding mechanism for whatever purpose. */ @NonNull public static SliderPanel attach(@NonNull Activity activity, float sensitivity) { return attach(activity, sensitivity, -1, -1); } /** * Attach a slideable mechanism to an activity that adds the slide to dismiss functionality * and allows for the statusbar to transition between colors * * @param activity the activity to attach the slider to * @param statusBarColor1 the primaryDark status bar color of the interface that this will slide back to * @param statusBarColor2 the primaryDark status bar color of the activity this is attaching to that will transition * back to the statusBarColor1 color * @return a {@link ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrInterface} that allows * the user to lock/unlock the sliding mechanism for whatever purpose. */ @NonNull public static SliderPanel attach(@NonNull Activity activity, float sensitivity, @ColorInt int statusBarColor1, @ColorInt int statusBarColor2) { // Setup the slider panel and attach it to the decor final SliderPanel panel = attachSliderPanel(activity, new SlidrConfig.Builder().sensitivity(sensitivity).build()); // Set the panel slide listener for when it becomes closed or opened panel.setOnPanelSlideListener(new ColorPanelSlideListener(activity, statusBarColor1, statusBarColor2)); // Return the lock interface return panel; } /** * Attach a slider mechanism to an activity based on the passed {@link ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrConfig} * * @param activity the activity to attach the slider to * @param config the slider configuration to make * @return a {@link ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrInterface} that allows * the user to lock/unlock the sliding mechanism for whatever purpose. */ @NonNull public static SlidrInterface attach(@NonNull Activity activity, @NonNull SlidrConfig config) { // Setup the slider panel and attach it to the decor final SliderPanel panel = attachSliderPanel(activity, config); // Set the panel slide listener for when it becomes closed or opened panel.setOnPanelSlideListener(new ConfigPanelSlideListener(activity, config)); // Return the lock interface return panel.getDefaultInterface(); } /** * Attach a new {@link SliderPanel} to the root of the activity's content */ @NonNull private static SliderPanel attachSliderPanel(@NonNull Activity activity, @Nullable SlidrConfig config) { // Hijack the decorview ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); View oldScreen = decorView.getChildAt(0); decorView.removeViewAt(0); // Setup the slider panel and attach it to the decor SliderPanel panel = new SliderPanel(activity, oldScreen, config); panel.setId(R.id.slidable_panel); oldScreen.setId(R.id.slidable_content); panel.addView(oldScreen); decorView.addView(panel, 0); return panel; } /** * Attach a slider mechanism to a fragment view replacing an internal view * * @param oldScreen the view within a fragment to replace * @param config the slider configuration to attach with * @return a {@link ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrInterface} that allows * the user to lock/unlock the sliding mechanism for whatever purpose. */ @NonNull public static SlidrInterface replace(@NonNull final View oldScreen, @NonNull final SlidrConfig config) { ViewGroup parent = (ViewGroup) oldScreen.getParent(); ViewGroup.LayoutParams params = oldScreen.getLayoutParams(); parent.removeView(oldScreen); // Setup the slider panel and attach it final SliderPanel panel = new SliderPanel(oldScreen.getContext(), oldScreen, config); panel.setId(R.id.slidable_panel); oldScreen.setId(R.id.slidable_content); panel.addView(oldScreen); parent.addView(panel, 0, params); // Set the panel slide listener for when it becomes closed or opened panel.setOnPanelSlideListener(new FragmentPanelSlideListener(oldScreen, config)); // Return the lock interface return panel.getDefaultInterface(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/model/SlidrConfig.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr.model; import android.graphics.Color; import androidx.annotation.ColorInt; import androidx.annotation.FloatRange; import androidx.customview.widget.ViewDragHelper; /** * This class contains the configuration information for all the options available in * this library */ public class SlidrConfig { private int colorPrimary = -1; private int colorSecondary = -1; private float touchSize = -1f; private float sensitivity = 1f; private int scrimColor = Color.BLACK; private float scrimStartAlpha = 0.8f; private float scrimEndAlpha = 0f; private float velocityThreshold = 5f; private float distanceThreshold = 0.25f; private boolean edgeOnly = false; private float edgeSize = 0.18f; private SlidrPosition position = SlidrPosition.LEFT; private SlidrListener listener; private SlidrConfig() { // Unused. } /*********************************************************************************************** * * Getters * */ /** * Get the primary color that the slider will interpolate. That is this color is the color * of the status bar of the Activity you are returning to * * @return the primary status bar color */ public int getPrimaryColor(){ return colorPrimary; } /** * Get the secondary color that the slider will interpolatel That is the color of the Activity * that you are making slidable * * @return the secondary status bar color */ public int getSecondaryColor(){ return colorSecondary; } /** * Get the color of the background scrim * * @return the scrim color integer */ @ColorInt public int getScrimColor(){ return scrimColor; } /** * Get teh start alpha value for when the activity is not swiped at all * * @return the start alpha value (0.0 to 1.0) */ public float getScrimStartAlpha(){ return scrimStartAlpha; } /** * Get the end alpha value for when the user almost swipes the activity off the screen * * @return the end alpha value (0.0 to 1.0) */ public float getScrimEndAlpha(){ return scrimEndAlpha; } /** * Get the position of the slidable mechanism for this configuration. This is the position on * the screen that the user can swipe the activity away from * * @return the slider position */ public SlidrPosition getPosition(){ return position; } /** * Get the touch 'width' to be used in the gesture detection. This value should incorporate with * the device's touch slop * * @return the touch area size */ public float getTouchSize(){ return touchSize; } /** * Get the velocity threshold at which the slide action is completed regardless of offset * distance of the drag * * @return the velocity threshold */ public float getVelocityThreshold(){ return velocityThreshold; } /** * Get at what % of the screen is the minimum viable distance the activity has to be dragged * in-order to be slinged off the screen * * @return the distant threshold as a percentage of the screen size (width or height) */ public float getDistanceThreshold(){ return distanceThreshold; } /** * Get the touch sensitivity set in the {@link ViewDragHelper} when * creating it. * * @return the touch sensitivity */ public float getSensitivity(){ return sensitivity; } /** * Get the slidr listener set by the user to respond to certain events in the sliding * mechanism. * * @return the slidr listener */ public SlidrListener getListener(){ return listener; } /** * Has the user configured slidr to only catch at the edge of the screen ? * * @return true if is edge capture only */ public boolean isEdgeOnly() { return edgeOnly; } /** * Get the size of the edge field that is catchable * * @see #isEdgeOnly() * @return the size of the edge that is grabable */ public float getEdgeSize(float size) { return edgeSize * size; } /*********************************************************************************************** * * Setters * */ public void setColorPrimary(int colorPrimary) { this.colorPrimary = colorPrimary; } public void setColorSecondary(int colorSecondary) { this.colorSecondary = colorSecondary; } public void setTouchSize(float touchSize) { this.touchSize = touchSize; } public void setSensitivity(float sensitivity) { this.sensitivity = sensitivity; } public void setScrimColor(@ColorInt int scrimColor) { this.scrimColor = scrimColor; } public void setScrimStartAlpha(float scrimStartAlpha) { this.scrimStartAlpha = scrimStartAlpha; } public void setScrimEndAlpha(float scrimEndAlpha) { this.scrimEndAlpha = scrimEndAlpha; } public void setVelocityThreshold(float velocityThreshold) { this.velocityThreshold = velocityThreshold; } public void setDistanceThreshold(float distanceThreshold) { this.distanceThreshold = distanceThreshold; } /** * The Builder for this configuration class. This is the only way to create a * configuration */ public static class Builder{ private final SlidrConfig config; public Builder(){ config = new SlidrConfig(); } public Builder primaryColor(@ColorInt int color){ config.colorPrimary = color; return this; } public Builder secondaryColor(@ColorInt int color){ config.colorSecondary = color; return this; } public Builder position(SlidrPosition position){ config.position = position; return this; } public Builder touchSize(float size){ config.touchSize = size; return this; } public Builder sensitivity(float sensitivity){ config.sensitivity = sensitivity; return this; } public Builder scrimColor(@ColorInt int color){ config.scrimColor = color; return this; } public Builder scrimStartAlpha(@FloatRange(from = 0.0, to = 1.0) float alpha){ config.scrimStartAlpha = alpha; return this; } public Builder scrimEndAlpha(@FloatRange(from = 0.0, to = 1.0) float alpha){ config.scrimEndAlpha = alpha; return this; } public Builder velocityThreshold(float threshold){ config.velocityThreshold = threshold; return this; } public Builder distanceThreshold(@FloatRange(from = .1f, to = .9f) float threshold){ config.distanceThreshold = threshold; return this; } public Builder edge(boolean flag){ config.edgeOnly = flag; return this; } public Builder edgeSize(@FloatRange(from = 0f, to = 1f) float edgeSize){ config.edgeSize = edgeSize; return this; } public Builder listener(SlidrListener listener){ config.listener = listener; return this; } public SlidrConfig build(){ return config; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/model/SlidrInterface.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr.model; public interface SlidrInterface { void lock(); void unlock(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/model/SlidrListener.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr.model; import androidx.customview.widget.ViewDragHelper; /** * This listener interface is for receiving events from the sliding panel such as state changes * and slide progress */ public interface SlidrListener { /** * This is called when the {@link ViewDragHelper} calls it's * state change callback. * * @see ViewDragHelper#STATE_IDLE * @see ViewDragHelper#STATE_DRAGGING * @see ViewDragHelper#STATE_SETTLING * * @param state the {@link ViewDragHelper} state */ void onSlideStateChanged(int state); void onSlideChange(float percent); void onSlideOpened(); /** * @return true than event was processed in the callback. */ boolean onSlideClosed(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/model/SlidrListenerAdapter.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr.model; public class SlidrListenerAdapter implements SlidrListener { @Override public void onSlideStateChanged(int state) { } @Override public void onSlideChange(float percent) { } @Override public void onSlideOpened() { } @Override public boolean onSlideClosed() { return false; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/model/SlidrPosition.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr.model; public enum SlidrPosition { LEFT, RIGHT, TOP, BOTTOM, VERTICAL, HORIZONTAL } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/util/ViewDragHelper.java ================================================ /** * Created by Gabriele Guerrisi on 24/11/2017. */ package ml.docilealligator.infinityforreddit.customviews.slidr.util; import android.content.Context; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Interpolator; import androidx.annotation.Nullable; import androidx.core.view.MotionEventCompat; import androidx.core.view.VelocityTrackerCompat; import androidx.core.view.ViewCompat; import androidx.core.widget.ScrollerCompat; import java.util.Arrays; /** * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number * of useful operations and state tracking for allowing a user to drag and reposition * views within their parent ViewGroup. */ public class ViewDragHelper { private static final String TAG = "ViewDragHelper"; /** * A null/invalid pointer ID. */ public static final int INVALID_POINTER = -1; /** * A view is not currently being dragged or animating as a result of a fling/snap. */ public static final int STATE_IDLE = 0; /** * A view is currently being dragged. The position is currently changing as a result * of user input or simulated user input. */ public static final int STATE_DRAGGING = 1; /** * A view is currently settling into place as a result of a fling or * predefined non-interactive motion. */ public static final int STATE_SETTLING = 2; /** * Edge flag indicating that the left edge should be affected. */ public static final int EDGE_LEFT = 1 << 0; /** * Edge flag indicating that the right edge should be affected. */ public static final int EDGE_RIGHT = 1 << 1; /** * Edge flag indicating that the top edge should be affected. */ public static final int EDGE_TOP = 1 << 2; /** * Edge flag indicating that the bottom edge should be affected. */ public static final int EDGE_BOTTOM = 1 << 3; /** * Edge flag set indicating all edges should be affected. */ public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; /** * Indicates that a check should occur along the horizontal axis */ public static final int DIRECTION_HORIZONTAL = 1 << 0; /** * Indicates that a check should occur along the vertical axis */ public static final int DIRECTION_VERTICAL = 1 << 1; /** * Indicates that a check should occur along all axes */ public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; private static final int EDGE_SIZE = 20; // dp private static final int BASE_SETTLE_DURATION = 256; // ms private static final int MAX_SETTLE_DURATION = 600; // ms // Current drag state; idle, dragging or settling private int mDragState; // Distance to travel before a drag may begin private int mTouchSlop; // Last known position/pointer tracking private int mActivePointerId = INVALID_POINTER; private float[] mInitialMotionX; private float[] mInitialMotionY; private float[] mLastMotionX; private float[] mLastMotionY; private int[] mInitialEdgesTouched; private int[] mEdgeDragsInProgress; private int[] mEdgeDragsLocked; private int mPointersDown; private VelocityTracker mVelocityTracker; private final float mMaxVelocity; private float mMinVelocity; private final int mEdgeSize; private int mTrackingEdges; private final ScrollerCompat mScroller; private final Callback mCallback; private View mCapturedView; private boolean mReleaseInProgress; private final ViewGroup mParentView; /** * A Callback is used as a communication channel with the ViewDragHelper back to the * parent view using it. on*methods are invoked on siginficant events and several * accessor methods are expected to provide the ViewDragHelper with more information * about the state of the parent view upon request. The callback also makes decisions * governing the range and draggability of child views. */ public static abstract class Callback { /** * Called when the drag state changes. See the STATE_* constants * for more information. * * @param state The new drag state * @see #STATE_IDLE * @see #STATE_DRAGGING * @see #STATE_SETTLING */ public void onViewDragStateChanged(int state) { } /** * Called when the captured view's position changes as the result of a drag or settle. * * @param changedView View whose position changed * @param left New X coordinate of the left edge of the view * @param top New Y coordinate of the top edge of the view * @param dx Change in X position from the last call * @param dy Change in Y position from the last call */ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { } /** * Called when a child view is captured for dragging or settling. The ID of the pointer * currently dragging the captured view is supplied. If activePointerId is * identified as {@link #INVALID_POINTER} the capture is programmatic instead of * pointer-initiated. * * @param capturedChild Child view that was captured * @param activePointerId Pointer id tracking the child capture */ public void onViewCaptured(View capturedChild, int activePointerId) { } /** * Called when the child view is no longer being actively dragged. * The fling velocity is also supplied, if relevant. The velocity values may * be clamped to system minimums or maximums. *

*

Calling code may decide to fling or otherwise release the view to let it * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} * and the view capture will not fully end until it comes to a complete stop. * If neither of these methods is invoked before onViewReleased returns, * the view will stop in place and the ViewDragHelper will return to * {@link #STATE_IDLE}.

* * @param releasedChild The captured child view now being released * @param xvel X velocity of the pointer as it left the screen in pixels per second. * @param yvel Y velocity of the pointer as it left the screen in pixels per second. */ public void onViewReleased(View releasedChild, float xvel, float yvel) { } /** * Called when one of the subscribed edges in the parent view has been touched * by the user while no child view is currently captured. * * @param edgeFlags A combination of edge flags describing the edge(s) currently touched * @param pointerId ID of the pointer touching the described edge(s) * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM */ public void onEdgeTouched(int edgeFlags, int pointerId) { } /** * Called when the given edge may become locked. This can happen if an edge drag * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} * was called. This method should return true to lock this edge or false to leave it * unlocked. The default behavior is to leave edges unlocked. * * @param edgeFlags A combination of edge flags describing the edge(s) locked * @return true to lock the edge, false to leave it unlocked */ public boolean onEdgeLock(int edgeFlags) { return false; } /** * Called when the user has started a deliberate drag away from one * of the subscribed edges in the parent view while no child view is currently captured. * * @param edgeFlags A combination of edge flags describing the edge(s) dragged * @param pointerId ID of the pointer touching the described edge(s) * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM */ public void onEdgeDragStarted(int edgeFlags, int pointerId) { } /** * Called to determine the Z-order of child views. * * @param index the ordered position to query for * @return index of the view that should be ordered at position index */ public int getOrderedChildIndex(int index) { return index; } /** * Return the magnitude of a draggable child view's horizontal range of motion in pixels. * This method should return 0 for views that cannot move horizontally. * * @param child Child view to check * @return range of horizontal motion in pixels */ public int getViewHorizontalDragRange(View child) { return 0; } /** * Return the magnitude of a draggable child view's vertical range of motion in pixels. * This method should return 0 for views that cannot move vertically. * * @param child Child view to check * @return range of vertical motion in pixels */ public int getViewVerticalDragRange(View child) { return 0; } /** * Called when the user's input indicates that they want to capture the given child view * with the pointer indicated by pointerId. The callback should return true if the user * is permitted to drag the given view with the indicated pointer. *

*

ViewDragHelper may call this method multiple times for the same view even if * the view is already captured; this indicates that a new pointer is trying to take * control of the view.

*

*

If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} * will follow if the capture is successful.

* * @param child Child the user is attempting to capture * @param pointerId ID of the pointer attempting the capture * @return true if capture should be allowed, false otherwise */ public abstract boolean tryCaptureView(View child, int pointerId); /** * Restrict the motion of the dragged child view along the horizontal axis. * The default implementation does not allow horizontal motion; the extending * class must override this method and provide the desired clamping. * * @param child Child view being dragged * @param left Attempted motion along the X axis * @param dx Proposed change in position for left * @return The new clamped position for left */ public int clampViewPositionHorizontal(View child, int left, int dx) { return 0; } /** * Restrict the motion of the dragged child view along the vertical axis. * The default implementation does not allow vertical motion; the extending * class must override this method and provide the desired clamping. * * @param child Child view being dragged * @param top Attempted motion along the Y axis * @param dy Proposed change in position for top * @return The new clamped position for top */ public int clampViewPositionVertical(View child, int top, int dy) { return 0; } } /** * Interpolator defining the animation curve for mScroller */ private static final Interpolator sInterpolator = new Interpolator() { public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; private final Runnable mSetIdleRunnable = new Runnable() { public void run() { setDragState(STATE_IDLE); } }; /** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ public static ViewDragHelper create(ViewGroup forParent, Callback cb) { return new ViewDragHelper(forParent.getContext(), forParent, cb); } /** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param sensitivity Multiplier for how sensitive the helper should be about detecting * the start of a drag. Larger values are more sensitive. 1.0f is normal. * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { final ViewDragHelper helper = create(forParent, cb); helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); return helper; } /** * Apps should use ViewDragHelper.create() to get a new instance. * This will allow VDH to use internal compatibility implementations for different * platform versions. * * @param context Context to initialize config-dependent params from * @param forParent Parent view to monitor */ private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { if (forParent == null) { throw new IllegalArgumentException("Parent view may not be null"); } if (cb == null) { throw new IllegalArgumentException("Callback may not be null"); } mParentView = forParent; mCallback = cb; final ViewConfiguration vc = ViewConfiguration.get(context); final float density = context.getResources().getDisplayMetrics().density; mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); mTouchSlop = vc.getScaledTouchSlop(); mMaxVelocity = vc.getScaledMaximumFlingVelocity(); mMinVelocity = vc.getScaledMinimumFlingVelocity(); mScroller = ScrollerCompat.create(context, sInterpolator); } /** * Set the minimum velocity that will be detected as having a magnitude greater than zero * in pixels per second. Callback methods accepting a velocity will be clamped appropriately. * * @param minVel Minimum velocity to detect */ public void setMinVelocity(float minVel) { mMinVelocity = minVel; } /** * Return the currently configured minimum velocity. Any flings with a magnitude less * than this value in pixels per second. Callback methods accepting a velocity will receive * zero as a velocity value if the real detected velocity was below this threshold. * * @return the minimum velocity that will be detected */ public float getMinVelocity() { return mMinVelocity; } /** * Retrieve the current drag state of this helper. This will return one of * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. * * @return The current drag state */ public int getViewDragState() { return mDragState; } /** * Enable edge tracking for the selected edges of the parent view. * The callback's {@link Callback#onEdgeTouched(int, int)} and * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked * for edges for which edge tracking has been enabled. * * @param edgeFlags Combination of edge flags describing the edges to watch * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM */ public void setEdgeTrackingEnabled(int edgeFlags) { mTrackingEdges = edgeFlags; } /** * Return the size of an edge. This is the range in pixels along the edges of this view * that will actively detect edge touches or drags if edge tracking is enabled. * * @return The size of an edge in pixels * @see #setEdgeTrackingEnabled(int) */ public int getEdgeSize() { return mEdgeSize; } /** * Capture a specific child view for dragging within the parent. The callback will be notified * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to * capture this view. * * @param childView Child view to capture * @param activePointerId ID of the pointer that is dragging the captured child view */ public void captureChildView(View childView, int activePointerId) { if (childView.getParent() != mParentView) { throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); } mCapturedView = childView; mActivePointerId = activePointerId; mCallback.onViewCaptured(childView, activePointerId); setDragState(STATE_DRAGGING); } /** * @return The currently captured view, or null if no view has been captured. */ public View getCapturedView() { return mCapturedView; } /** * @return The ID of the pointer currently dragging the captured view, * or {@link #INVALID_POINTER}. */ public int getActivePointerId() { return mActivePointerId; } /** * @return The minimum distance in pixels that the user must travel to initiate a drag */ public int getTouchSlop() { return mTouchSlop; } /** * The result of a call to this method is equivalent to * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. */ public void cancel() { mActivePointerId = INVALID_POINTER; clearMotionHistory(); if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } /** * {@link #cancel()}, but also abort all motion in progress and snap to the end of any * animation. */ public void abort() { cancel(); if (mDragState == STATE_SETTLING) { final int oldX = mScroller.getCurrX(); final int oldY = mScroller.getCurrY(); mScroller.abortAnimation(); final int newX = mScroller.getCurrX(); final int newY = mScroller.getCurrY(); mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); } setDragState(STATE_IDLE); } /** * Animate the view child to the given (left, top) position. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. *

*

This operation does not count as a capture event, though {@link #getCapturedView()} * will still report the sliding view while the slide is in progress.

* * @param child Child view to capture and animate * @param finalLeft Final left position of child * @param finalTop Final top position of child * @return true if animation should continue through {@link #continueSettling(boolean)} calls */ public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { mCapturedView = child; mActivePointerId = INVALID_POINTER; boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) { // If we're in an IDLE state to begin with and aren't moving anywhere, we // end up having a non-null capturedView with an IDLE dragState mCapturedView = null; } return continueSliding; } /** * Settle the captured view at the given (left, top) position. * The appropriate velocity from prior motion will be taken into account. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. * * @param finalLeft Settled left edge position for the captured view * @param finalTop Settled top edge position for the captured view * @return true if animation should continue through {@link #continueSettling(boolean)} calls */ public boolean settleCapturedViewAt(int finalLeft, int finalTop) { if (!mReleaseInProgress) { throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + "Callback#onViewReleased"); } return forceSettleCapturedViewAt(finalLeft, finalTop, (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); } /** * Settle the captured view at the given (left, top) position. * * @param finalLeft Target left position for the captured view * @param finalTop Target top position for the captured view * @param xvel Horizontal velocity * @param yvel Vertical velocity * @return true if animation should continue through {@link #continueSettling(boolean)} calls */ private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { final int startLeft = mCapturedView.getLeft(); final int startTop = mCapturedView.getTop(); final int dx = finalLeft - startLeft; final int dy = finalTop - startTop; if (dx == 0 && dy == 0) { // Nothing to do. Send callbacks, be done. mScroller.abortAnimation(); setDragState(STATE_IDLE); return false; } final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); mScroller.startScroll(startLeft, startTop, dx, dy, duration); setDragState(STATE_SETTLING); return true; } private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); final int absDx = Math.abs(dx); final int absDy = Math.abs(dy); final int absXVel = Math.abs(xvel); final int absYVel = Math.abs(yvel); final int addedVel = absXVel + absYVel; final int addedDistance = absDx + absDy; final float xweight = xvel != 0 ? (float) absXVel / addedVel : (float) absDx / addedDistance; final float yweight = yvel != 0 ? (float) absYVel / addedVel : (float) absDy / addedDistance; int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); return (int) (xduration * xweight + yduration * yweight); } private int computeAxisDuration(int delta, int velocity, int motionRange) { if (delta == 0) { return 0; } final int width = mParentView.getWidth(); final int halfWidth = width / 2; final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio); int duration; velocity = Math.abs(velocity); if (velocity > 0) { duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); } else { final float range = (float) Math.abs(delta) / motionRange; duration = (int) ((range + 1) * BASE_SETTLE_DURATION); } return Math.min(duration, MAX_SETTLE_DURATION); } /** * Clamp the magnitude of value for absMin and absMax. * If the value is below the minimum, it will be clamped to zero. * If the value is above the maximum, it will be clamped to the maximum. * * @param value Value to clamp * @param absMin Absolute value of the minimum significant value to return * @param absMax Absolute value of the maximum value to return * @return The clamped value with the same sign as value */ private int clampMag(int value, int absMin, int absMax) { final int absValue = Math.abs(value); if (absValue < absMin) return 0; if (absValue > absMax) return value > 0 ? absMax : -absMax; return value; } /** * Clamp the magnitude of value for absMin and absMax. * If the value is below the minimum, it will be clamped to zero. * If the value is above the maximum, it will be clamped to the maximum. * * @param value Value to clamp * @param absMin Absolute value of the minimum significant value to return * @param absMax Absolute value of the maximum value to return * @return The clamped value with the same sign as value */ private float clampMag(float value, float absMin, float absMax) { final float absValue = Math.abs(value); if (absValue < absMin) return 0; if (absValue > absMax) return value > 0 ? absMax : -absMax; return value; } private float distanceInfluenceForSnapDuration(float f) { f -= 0.5f; // center the values about 0. f *= 0.3f * Math.PI / 2.0f; return (float) Math.sin(f); } /** * Settle the captured view based on standard free-moving fling behavior. * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame * to continue the motion until it returns false. * * @param minLeft Minimum X position for the view's left edge * @param minTop Minimum Y position for the view's top edge * @param maxLeft Maximum X position for the view's left edge * @param maxTop Maximum Y position for the view's top edge */ public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { if (!mReleaseInProgress) { throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + "Callback#onViewReleased"); } mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), minLeft, maxLeft, minTop, maxTop); setDragState(STATE_SETTLING); } /** * Move the captured settling view by the appropriate amount for the current time. * If continueSettling returns true, the caller should call it again * on the next frame to continue. * * @param deferCallbacks true if state callbacks should be deferred via posted message. * Set this to true if you are calling this method from * {@link android.view.View#computeScroll()} or similar methods * invoked as part of layout or drawing. * @return true if settle is still in progress */ public boolean continueSettling(boolean deferCallbacks) { if (mDragState == STATE_SETTLING) { boolean keepGoing = mScroller.computeScrollOffset(); final int x = mScroller.getCurrX(); final int y = mScroller.getCurrY(); final int dx = x - mCapturedView.getLeft(); final int dy = y - mCapturedView.getTop(); if (dx != 0) { ViewCompat.offsetLeftAndRight(mCapturedView, dx); } if (dy != 0) { ViewCompat.offsetTopAndBottom(mCapturedView, dy); } if (dx != 0 || dy != 0) { mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); } if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { // Close enough. The interpolator/scroller might think we're still moving // but the user sure doesn't. mScroller.abortAnimation(); keepGoing = false; } if (!keepGoing) { if (deferCallbacks) { mParentView.post(mSetIdleRunnable); } else { setDragState(STATE_IDLE); } } } return mDragState == STATE_SETTLING; } /** * Like all callback events this must happen on the UI thread, but release * involves some extra semantics. During a release (mReleaseInProgress) * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} * or {@link #flingCapturedView(int, int, int, int)}. */ private void dispatchViewReleased(float xvel, float yvel) { mReleaseInProgress = true; mCallback.onViewReleased(mCapturedView, xvel, yvel); mReleaseInProgress = false; if (mDragState == STATE_DRAGGING) { // onViewReleased didn't call a method that would have changed this. Go idle. setDragState(STATE_IDLE); } } private void clearMotionHistory() { if (mInitialMotionX == null) { return; } Arrays.fill(mInitialMotionX, 0); Arrays.fill(mInitialMotionY, 0); Arrays.fill(mLastMotionX, 0); Arrays.fill(mLastMotionY, 0); Arrays.fill(mInitialEdgesTouched, 0); Arrays.fill(mEdgeDragsInProgress, 0); Arrays.fill(mEdgeDragsLocked, 0); mPointersDown = 0; } private void clearMotionHistory(int pointerId) { if (mInitialMotionX == null || !isPointerDown(pointerId)) { return; } mInitialMotionX[pointerId] = 0; mInitialMotionY[pointerId] = 0; mLastMotionX[pointerId] = 0; mLastMotionY[pointerId] = 0; mInitialEdgesTouched[pointerId] = 0; mEdgeDragsInProgress[pointerId] = 0; mEdgeDragsLocked[pointerId] = 0; mPointersDown &= ~(1 << pointerId); } private void ensureMotionHistorySizeForId(int pointerId) { if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { float[] imx = new float[pointerId + 1]; float[] imy = new float[pointerId + 1]; float[] lmx = new float[pointerId + 1]; float[] lmy = new float[pointerId + 1]; int[] iit = new int[pointerId + 1]; int[] edip = new int[pointerId + 1]; int[] edl = new int[pointerId + 1]; if (mInitialMotionX != null) { System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length); System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length); } mInitialMotionX = imx; mInitialMotionY = imy; mLastMotionX = lmx; mLastMotionY = lmy; mInitialEdgesTouched = iit; mEdgeDragsInProgress = edip; mEdgeDragsLocked = edl; } } private void saveInitialMotion(float x, float y, int pointerId) { ensureMotionHistorySizeForId(pointerId); mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y); mPointersDown |= 1 << pointerId; } private void saveLastMotion(MotionEvent ev) { final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0; i < pointerCount; i++) { final int pointerId = MotionEventCompat.getPointerId(ev, i); // If pointer is invalid then skip saving on ACTION_MOVE. if (!isValidPointerForActionMove(pointerId)) { continue; } final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); mLastMotionX[pointerId] = x; mLastMotionY[pointerId] = y; } } /** * Check if the given pointer ID represents a pointer that is currently down (to the best * of the ViewDragHelper's knowledge). *

*

The state used to report this information is populated by the methods * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not * been called for all relevant MotionEvents to track, the information reported * by this method may be stale or incorrect.

* * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent * @return true if the pointer with the given ID is still down */ public boolean isPointerDown(int pointerId) { return (mPointersDown & 1 << pointerId) != 0; } void setDragState(int state) { mParentView.removeCallbacks(mSetIdleRunnable); if (mDragState != state) { mDragState = state; mCallback.onViewDragStateChanged(state); if (mDragState == STATE_IDLE) { mCapturedView = null; } } } /** * Attempt to capture the view with the given pointer ID. The callback will be involved. * This will put us into the "dragging" state. If we've already captured this view with * this pointer this method will immediately return true without consulting the callback. * * @param toCapture View to capture * @param pointerId Pointer to capture with * @return true if capture was successful */ boolean tryCaptureViewForDrag(View toCapture, int pointerId) { if (toCapture == mCapturedView && mActivePointerId == pointerId) { // Already done! return true; } if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { mActivePointerId = pointerId; captureChildView(toCapture, pointerId); return true; } return false; } /** * Tests scrollability within child views of v given a delta of dx. * * @param v View to test for horizontal scrollability * @param checkV Whether the view v passed should itself be checked for scrollability (true), * or just its children (false). * @param dx Delta scrolled in pixels along the X axis * @param dy Delta scrolled in pixels along the Y axis * @param x X coordinate of the active touch point * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { // TODO: Add versioned support here for transformed views. // This will not work for transformed views in Honeycomb+ final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return checkV && (ViewCompat.canScrollHorizontally(v, -dx) || ViewCompat.canScrollVertically(v, -dy)); } /** * Check if this event as provided to the parent view's onInterceptTouchEvent should * cause the parent to intercept the touch event stream. * * @param ev MotionEvent provided to onInterceptTouchEvent * @return true if the parent view should return true from onInterceptTouchEvent */ public boolean shouldInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); final int actionIndex = MotionEventCompat.getActionIndex(ev); if (action == MotionEvent.ACTION_DOWN) { // Reset things for a new event stream, just in case we didn't get // the whole previous stream. cancel(); } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); switch (action) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); final int pointerId = MotionEventCompat.getPointerId(ev, 0); saveInitialMotion(x, y, pointerId); final View toCapture = findTopChildUnder((int) x, (int) y); // Catch a settling view if possible. if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { tryCaptureViewForDrag(toCapture, pointerId); } final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); final float x = MotionEventCompat.getX(ev, actionIndex); final float y = MotionEventCompat.getY(ev, actionIndex); saveInitialMotion(x, y, pointerId); // A ViewDragHelper can only manipulate one view at a time. if (mDragState == STATE_IDLE) { final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } } else if (mDragState == STATE_SETTLING) { // Catch a settling view if possible. final View toCapture = findTopChildUnder((int) x, (int) y); if (toCapture == mCapturedView) { tryCaptureViewForDrag(toCapture, pointerId); } } break; } case MotionEvent.ACTION_MOVE: { if (mInitialMotionX == null || mInitialMotionY == null) break; // First to cross a touch slop over a draggable view wins. Also report edge drags. final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0; i < pointerCount; i++) { final int pointerId = MotionEventCompat.getPointerId(ev, i); // If pointer is invalid then skip the ACTION_MOVE. if (!isValidPointerForActionMove(pointerId)) continue; final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); final float dx = x - mInitialMotionX[pointerId]; final float dy = y - mInitialMotionY[pointerId]; final View toCapture = findTopChildUnder((int) x, (int) y); final boolean pastSlop = checkTouchSlop(toCapture, dx, dy); if (pastSlop) { // check the callback's // getView[Horizontal|Vertical]DragRange methods to know // if you can move at all along an axis, then see if it // would clamp to the same value. If you can't move at // all in every dimension with a nonzero range, bail. final int oldLeft = toCapture.getLeft(); final int targetLeft = oldLeft + (int) dx; final int newLeft = mCallback.clampViewPositionHorizontal(toCapture, targetLeft, (int) dx); final int oldTop = toCapture.getTop(); final int targetTop = oldTop + (int) dy; final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop, (int) dy); final int horizontalDragRange = mCallback.getViewHorizontalDragRange( toCapture); final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture); if ((horizontalDragRange == 0 || horizontalDragRange > 0 && newLeft == oldLeft) && (verticalDragRange == 0 || verticalDragRange > 0 && newTop == oldTop)) { break; } } reportNewEdgeDrags(dx, dy, pointerId); if (mDragState == STATE_DRAGGING) { // Callback might have started an edge drag break; } if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) { break; } } saveLastMotion(ev); break; } case MotionEventCompat.ACTION_POINTER_UP: { final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); clearMotionHistory(pointerId); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { cancel(); break; } } return mDragState == STATE_DRAGGING; } /** * Process a touch event received by the parent view. This method will dispatch callback events * as needed before returning. The parent view's onTouchEvent implementation should call this. * * @param ev The touch event received by the parent view */ public void processTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); final int actionIndex = MotionEventCompat.getActionIndex(ev); if (action == MotionEvent.ACTION_DOWN) { // Reset things for a new event stream, just in case we didn't get // the whole previous stream. cancel(); } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); switch (action) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); final int pointerId = MotionEventCompat.getPointerId(ev, 0); final View toCapture = findTopChildUnder((int) x, (int) y); saveInitialMotion(x, y, pointerId); // Since the parent is already directly processing this touch event, // there is no reason to delay for a slop before dragging. // Start immediately if possible. tryCaptureViewForDrag(toCapture, pointerId); final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); final float x = MotionEventCompat.getX(ev, actionIndex); final float y = MotionEventCompat.getY(ev, actionIndex); saveInitialMotion(x, y, pointerId); // A ViewDragHelper can only manipulate one view at a time. if (mDragState == STATE_IDLE) { // If we're idle we can do anything! Treat it like a normal down event. final View toCapture = findTopChildUnder((int) x, (int) y); tryCaptureViewForDrag(toCapture, pointerId); final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } } else if (isCapturedViewUnder((int) x, (int) y)) { // We're still tracking a captured view. If the same view is under this // point, we'll swap to controlling it with this pointer instead. // (This will still work if we're "catching" a settling view.) tryCaptureViewForDrag(mCapturedView, pointerId); } break; } case MotionEvent.ACTION_MOVE: { if (mDragState == STATE_DRAGGING) { // If pointer is invalid then skip the ACTION_MOVE. if (!isValidPointerForActionMove(mActivePointerId)) break; final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, index); final float y = MotionEventCompat.getY(ev, index); final int idx = (int) (x - mLastMotionX[mActivePointerId]); final int idy = (int) (y - mLastMotionY[mActivePointerId]); dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); saveLastMotion(ev); } else { // Check to see if any pointer is now over a draggable view. final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0; i < pointerCount; i++) { final int pointerId = MotionEventCompat.getPointerId(ev, i); // If pointer is invalid then skip the ACTION_MOVE. if (!isValidPointerForActionMove(pointerId)) continue; final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); final float dx = x - mInitialMotionX[pointerId]; final float dy = y - mInitialMotionY[pointerId]; reportNewEdgeDrags(dx, dy, pointerId); if (mDragState == STATE_DRAGGING) { // Callback might have started an edge drag. break; } final View toCapture = findTopChildUnder((int) x, (int) y); if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) { break; } } saveLastMotion(ev); } break; } case MotionEventCompat.ACTION_POINTER_UP: { final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { // Try to find another pointer that's still holding on to the captured view. int newActivePointer = INVALID_POINTER; final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0; i < pointerCount; i++) { final int id = MotionEventCompat.getPointerId(ev, i); if (id == mActivePointerId) { // This one's going away, skip. continue; } final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); if (findTopChildUnder((int) x, (int) y) == mCapturedView && tryCaptureViewForDrag(mCapturedView, id)) { newActivePointer = mActivePointerId; break; } } if (newActivePointer == INVALID_POINTER) { // We didn't find another pointer still touching the view, release it. releaseViewForPointerUp(); } } clearMotionHistory(pointerId); break; } case MotionEvent.ACTION_UP: { if (mDragState == STATE_DRAGGING) { releaseViewForPointerUp(); } cancel(); break; } case MotionEvent.ACTION_CANCEL: { if (mDragState == STATE_DRAGGING) { dispatchViewReleased(0, 0); } cancel(); break; } } } private void reportNewEdgeDrags(float dx, float dy, int pointerId) { int dragsStarted = 0; if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { dragsStarted |= EDGE_LEFT; } if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { dragsStarted |= EDGE_TOP; } if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { dragsStarted |= EDGE_RIGHT; } if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { dragsStarted |= EDGE_BOTTOM; } if (dragsStarted != 0) { mEdgeDragsInProgress[pointerId] |= dragsStarted; mCallback.onEdgeDragStarted(dragsStarted, pointerId); } } private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { final float absDelta = Math.abs(delta); final float absODelta = Math.abs(odelta); if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || (mEdgeDragsLocked[pointerId] & edge) == edge || (mEdgeDragsInProgress[pointerId] & edge) == edge || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { return false; } if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { mEdgeDragsLocked[pointerId] |= edge; return false; } return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; } /** * Check if we've crossed a reasonable touch slop for the given child view. * If the child cannot be dragged along the horizontal or vertical axis, motion * along that axis will not count toward the slop check. * * @param child Child to check * @param dx Motion since initial position along X axis * @param dy Motion since initial position along Y axis * @return true if the touch slop has been crossed */ private boolean checkTouchSlop(View child, float dx, float dy) { if (child == null) { return false; } final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; float temp_dy = dy; if (temp_dy < 0) temp_dy = -temp_dy; if (checkHorizontal && checkVertical) { return dx * dx + dy * dy > mTouchSlop * mTouchSlop; } else if (checkVertical) { return Math.abs(dy) > mTouchSlop; } else if (checkHorizontal && 3 * temp_dy < dx) { return Math.abs(dx) > mTouchSlop; } return false; } /** * Check if any pointer tracked in the current gesture has crossed * the required slop threshold. *

*

This depends on internal state populated by * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on * the results of this method after all currently available touch data * has been provided to one of these two methods.

* * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} * @return true if the slop threshold has been crossed, false otherwise */ public boolean checkTouchSlop(int directions) { final int count = mInitialMotionX.length; for (int i = 0; i < count; i++) { if (checkTouchSlop(directions, i)) { return true; } } return false; } /** * Check if the specified pointer tracked in the current gesture has crossed * the required slop threshold. *

*

This depends on internal state populated by * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on * the results of this method after all currently available touch data * has been provided to one of these two methods.

* * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} * @param pointerId ID of the pointer to slop check as specified by MotionEvent * @return true if the slop threshold has been crossed, false otherwise */ public boolean checkTouchSlop(int directions, int pointerId) { if (!isPointerDown(pointerId)) { return false; } final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; if (checkHorizontal && checkVertical) { return dx * dx + dy * dy > mTouchSlop * mTouchSlop; } else if (checkHorizontal) { return Math.abs(dx) > mTouchSlop; } else if (checkVertical) { return Math.abs(dy) > mTouchSlop; } return false; } /** * Check if any of the edges specified were initially touched in the currently active gesture. * If there is no currently active gesture this method will return false. * * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and * {@link #EDGE_ALL} * @return true if any of the edges specified were initially touched in the current gesture */ public boolean isEdgeTouched(int edges) { final int count = mInitialEdgesTouched.length; for (int i = 0; i < count; i++) { if (isEdgeTouched(edges, i)) { return true; } } return false; } /** * Check if any of the edges specified were initially touched by the pointer with * the specified ID. If there is no currently active gesture or if there is no pointer with * the given ID currently down this method will return false. * * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and * {@link #EDGE_ALL} * @return true if any of the edges specified were initially touched in the current gesture */ public boolean isEdgeTouched(int edges, int pointerId) { return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0; } private void releaseViewForPointerUp() { mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final float xvel = clampMag( VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), mMinVelocity, mMaxVelocity); final float yvel = clampMag( VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), mMinVelocity, mMaxVelocity); dispatchViewReleased(xvel, yvel); } private void dragTo(int left, int top, int dx, int dy) { int clampedX = left; int clampedY = top; final int oldLeft = mCapturedView.getLeft(); final int oldTop = mCapturedView.getTop(); if (dx != 0) { clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft); } if (dy != 0) { clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop); } if (dx != 0 || dy != 0) { final int clampedDx = clampedX - oldLeft; final int clampedDy = clampedY - oldTop; mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); } } /** * Determine if the currently captured view is under the given point in the * parent view's coordinate system. If there is no captured view this method * will return false. * * @param x X position to test in the parent's coordinate system * @param y Y position to test in the parent's coordinate system * @return true if the captured view is under the given point, false otherwise */ public boolean isCapturedViewUnder(int x, int y) { return isViewUnder(mCapturedView, x, y); } /** * Determine if the supplied view is under the given point in the * parent view's coordinate system. * * @param view Child view of the parent to hit test * @param x X position to test in the parent's coordinate system * @param y Y position to test in the parent's coordinate system * @return true if the supplied view is under the given point, false otherwise */ public boolean isViewUnder(View view, int x, int y) { if (view == null) { return false; } return x >= view.getLeft() && x < view.getRight() && y >= view.getTop() && y < view.getBottom(); } /** * Find the topmost child under the given point within the parent view's coordinate system. * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. * * @param x X position to test in the parent's coordinate system * @param y Y position to test in the parent's coordinate system * @return The topmost child view under (x, y) or null if none found. */ @Nullable public View findTopChildUnder(int x, int y) { final int childCount = mParentView.getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); if (x >= child.getLeft() && x < child.getRight() && y >= child.getTop() && y < child.getBottom()) { return child; } } return null; } private int getEdgesTouched(int x, int y) { int result = 0; if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT; if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP; if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT; if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM; return result; } private boolean isValidPointerForActionMove(int pointerId) { if (!isPointerDown(pointerId)) { Log.e(TAG, "Ignoring pointerId=" + pointerId + " because ACTION_DOWN was not received " + "for this pointer before ACTION_MOVE. It likely happened because " + " ViewDragHelper did not receive all the events in the event stream."); return false; } return true; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/widget/ScrimRenderer.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr.widget; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrPosition; final class ScrimRenderer { private final View rootView; private final View decorView; private final Rect dirtyRect; ScrimRenderer(@NonNull View rootView, @NonNull View decorView) { this.rootView = rootView; this.decorView = decorView; dirtyRect = new Rect(); } void render(Canvas canvas, SlidrPosition position, Paint paint) { switch (position) { case LEFT: renderLeft(canvas, paint); break; case RIGHT: renderRight(canvas, paint); break; case TOP: renderTop(canvas, paint); break; case BOTTOM: renderBottom(canvas, paint); break; case VERTICAL: renderVertical(canvas, paint); break; case HORIZONTAL: renderHorizontal(canvas, paint); break; } } Rect getDirtyRect(SlidrPosition position) { switch (position) { case LEFT: dirtyRect.set(0, 0, decorView.getLeft(), rootView.getMeasuredHeight()); break; case RIGHT: dirtyRect.set(decorView.getRight(), 0, rootView.getMeasuredWidth(), rootView.getMeasuredHeight()); break; case TOP: dirtyRect.set(0, 0, rootView.getMeasuredWidth(), decorView.getTop()); break; case BOTTOM: dirtyRect.set(0, decorView.getBottom(), rootView.getMeasuredWidth(), rootView.getMeasuredHeight()); break; case VERTICAL: if (decorView.getTop() > 0) { dirtyRect.set(0, 0, rootView.getMeasuredWidth(), decorView.getTop()); } else { dirtyRect.set(0, decorView.getBottom(), rootView.getMeasuredWidth(), rootView.getMeasuredHeight()); } break; case HORIZONTAL: if (decorView.getLeft() > 0) { dirtyRect.set(0, 0, decorView.getLeft(), rootView.getMeasuredHeight()); } else { dirtyRect.set(decorView.getRight(), 0, rootView.getMeasuredWidth(), rootView.getMeasuredHeight()); } break; } return dirtyRect; } private void renderLeft(Canvas canvas, Paint paint) { canvas.drawRect(0, 0, decorView.getLeft(), rootView.getMeasuredHeight(), paint); } private void renderRight(Canvas canvas, Paint paint) { canvas.drawRect(decorView.getRight(), 0, rootView.getMeasuredWidth(), rootView.getMeasuredHeight(), paint); } private void renderTop(Canvas canvas, Paint paint) { canvas.drawRect(0, 0, rootView.getMeasuredWidth(), decorView.getTop(), paint); } private void renderBottom(Canvas canvas, Paint paint) { canvas.drawRect(0, decorView.getBottom(), rootView.getMeasuredWidth(), rootView.getMeasuredHeight(), paint); } private void renderVertical(Canvas canvas, Paint paint) { if (decorView.getTop() > 0) { renderTop(canvas, paint); } else { renderBottom(canvas, paint); } } private void renderHorizontal(Canvas canvas, Paint paint) { if (decorView.getLeft() > 0) { renderLeft(canvas, paint); } else { renderRight(canvas, paint); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/customviews/slidr/widget/SliderPanel.java ================================================ package ml.docilealligator.infinityforreddit.customviews.slidr.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.core.view.ViewGroupCompat; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrConfig; import ml.docilealligator.infinityforreddit.customviews.slidr.model.SlidrInterface; import ml.docilealligator.infinityforreddit.customviews.slidr.util.ViewDragHelper; public class SliderPanel extends FrameLayout { private static final int MIN_FLING_VELOCITY = 400; // dips per second private int screenWidth; private int screenHeight; private View decorView; private ViewDragHelper dragHelper; private OnPanelSlideListener listener; private Paint scrimPaint; private ScrimRenderer scrimRenderer; private boolean isLocked = false; private boolean isEdgeTouched = false; private int edgePosition; private SlidrConfig config; public SliderPanel(Context context) { super(context); } public SliderPanel(Context context, View decorView, @Nullable SlidrConfig config) { super(context); this.decorView = decorView; this.config = (config == null ? new SlidrConfig.Builder().build() : config); init(); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean interceptForDrag; if (isLocked) { return false; } if (config.isEdgeOnly()) { isEdgeTouched = canDragFromEdge(ev); } // Fix for pull request #13 and issue #12 try { interceptForDrag = dragHelper.shouldInterceptTouchEvent(ev); } catch (Exception e) { interceptForDrag = false; } return interceptForDrag && !isLocked; } @Override public boolean onTouchEvent(MotionEvent event) { if (isLocked) { return false; } try { dragHelper.processTouchEvent(event); } catch (IllegalArgumentException e) { return false; } return true; } @Override public void computeScroll() { super.computeScroll(); if (dragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } @Override protected void onDraw(Canvas canvas) { scrimRenderer.render(canvas, config.getPosition(), scrimPaint); } /** * Set the panel slide listener that gets called based on slider changes * * @param listener callback implementation */ public void setOnPanelSlideListener(OnPanelSlideListener listener) { this.listener = listener; } /** * Get the default {@link SlidrInterface} from which to control the panel with after attachment */ public SlidrInterface getDefaultInterface() { return defaultSlidrInterface; } public boolean isLocked() { return isLocked; } private final SlidrInterface defaultSlidrInterface = new SlidrInterface() { @Override public void lock() { SliderPanel.this.lock(); } @Override public void unlock() { SliderPanel.this.unlock(); } }; /** * The drag helper callback interface for the Left position */ private final ViewDragHelper.Callback leftCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { boolean edgeCase = !config.isEdgeOnly() || dragHelper.isEdgeTouched(edgePosition, pointerId); return child.getId() == decorView.getId() && edgeCase; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return clamp(left, 0, screenWidth); } @Override public int getViewHorizontalDragRange(View child) { return screenWidth; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); int left = releasedChild.getLeft(); int settleLeft = 0; int leftThreshold = (int) (getWidth() * config.getDistanceThreshold()); boolean isVerticalSwiping = Math.abs(yvel) > config.getVelocityThreshold(); if (xvel > 0) { if (Math.abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) { settleLeft = screenWidth; } else if (left > leftThreshold) { settleLeft = screenWidth; } } else if (xvel == 0) { if (left > leftThreshold) { settleLeft = screenWidth; } } dragHelper.settleCapturedViewAt(settleLeft, releasedChild.getTop()); invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); float percent = 1f - ((float) left / (float) screenWidth); if (listener != null) listener.onSlideChange(percent); // Update the dimmer alpha applyScrim(percent); } @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); if (listener != null) listener.onStateChanged(state); switch (state) { case ViewDragHelper.STATE_IDLE: if (decorView.getLeft() == 0) { // State Open if (listener != null) listener.onOpened(); } else { // State Closed if (listener != null) listener.onClosed(); } break; case ViewDragHelper.STATE_DRAGGING: break; case ViewDragHelper.STATE_SETTLING: break; } } }; /** * The drag helper callbacks for dragging the slidr attachment from the right of the screen */ private final ViewDragHelper.Callback rightCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { boolean edgeCase = !config.isEdgeOnly() || dragHelper.isEdgeTouched(edgePosition, pointerId); return child.getId() == decorView.getId() && edgeCase; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return clamp(left, -screenWidth, 0); } @Override public int getViewHorizontalDragRange(View child) { return screenWidth; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); int left = releasedChild.getLeft(); int settleLeft = 0; int leftThreshold = (int) (getWidth() * config.getDistanceThreshold()); boolean isVerticalSwiping = Math.abs(yvel) > config.getVelocityThreshold(); if (xvel < 0) { if (Math.abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) { settleLeft = -screenWidth; } else if (left < -leftThreshold) { settleLeft = -screenWidth; } } else if (xvel == 0) { if (left < -leftThreshold) { settleLeft = -screenWidth; } } dragHelper.settleCapturedViewAt(settleLeft, releasedChild.getTop()); invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); float percent = 1f - ((float) Math.abs(left) / (float) screenWidth); if (listener != null) listener.onSlideChange(percent); // Update the dimmer alpha applyScrim(percent); } @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); if (listener != null) listener.onStateChanged(state); switch (state) { case ViewDragHelper.STATE_IDLE: if (decorView.getLeft() == 0) { // State Open if (listener != null) listener.onOpened(); } else { // State Closed if (listener != null) listener.onClosed(); } break; case ViewDragHelper.STATE_DRAGGING: break; case ViewDragHelper.STATE_SETTLING: break; } } }; /** * The drag helper callbacks for dragging the slidr attachment from the top of the screen */ private final ViewDragHelper.Callback topCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return child.getId() == decorView.getId() && (!config.isEdgeOnly() || isEdgeTouched); } @Override public int clampViewPositionVertical(View child, int top, int dy) { return clamp(top, 0, screenHeight); } @Override public int getViewVerticalDragRange(View child) { return screenHeight; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); int top = releasedChild.getTop(); int settleTop = 0; int topThreshold = (int) (getHeight() * config.getDistanceThreshold()); boolean isSideSwiping = Math.abs(xvel) > config.getVelocityThreshold(); if (yvel > 0) { if (Math.abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) { settleTop = screenHeight; } else if (top > topThreshold) { settleTop = screenHeight; } } else if (yvel == 0) { if (top > topThreshold) { settleTop = screenHeight; } } dragHelper.settleCapturedViewAt(releasedChild.getLeft(), settleTop); invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); float percent = 1f - ((float) Math.abs(top) / (float) screenHeight); if (listener != null) listener.onSlideChange(percent); // Update the dimmer alpha applyScrim(percent); } @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); if (listener != null) listener.onStateChanged(state); switch (state) { case ViewDragHelper.STATE_IDLE: if (decorView.getTop() == 0) { // State Open if (listener != null) listener.onOpened(); } else { // State Closed if (listener != null) listener.onClosed(); } break; case ViewDragHelper.STATE_DRAGGING: break; case ViewDragHelper.STATE_SETTLING: break; } } }; /** * The drag helper callbacks for dragging the slidr attachment from the bottom of the screen */ private final ViewDragHelper.Callback bottomCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return child.getId() == decorView.getId() && (!config.isEdgeOnly() || isEdgeTouched); } @Override public int clampViewPositionVertical(View child, int top, int dy) { return clamp(top, -screenHeight, 0); } @Override public int getViewVerticalDragRange(View child) { return screenHeight; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); int top = releasedChild.getTop(); int settleTop = 0; int topThreshold = (int) (getHeight() * config.getDistanceThreshold()); boolean isSideSwiping = Math.abs(xvel) > config.getVelocityThreshold(); if (yvel < 0) { if (Math.abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) { settleTop = -screenHeight; } else if (top < -topThreshold) { settleTop = -screenHeight; } } else if (yvel == 0) { if (top < -topThreshold) { settleTop = -screenHeight; } } dragHelper.settleCapturedViewAt(releasedChild.getLeft(), settleTop); invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); float percent = 1f - ((float) Math.abs(top) / (float) screenHeight); if (listener != null) listener.onSlideChange(percent); // Update the dimmer alpha applyScrim(percent); } @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); if (listener != null) listener.onStateChanged(state); switch (state) { case ViewDragHelper.STATE_IDLE: if (decorView.getTop() == 0) { // State Open if (listener != null) listener.onOpened(); } else { // State Closed if (listener != null) listener.onClosed(); } break; case ViewDragHelper.STATE_DRAGGING: break; case ViewDragHelper.STATE_SETTLING: break; } } }; /** * The drag helper callbacks for dragging the slidr attachment in both vertical directions */ private final ViewDragHelper.Callback verticalCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return child.getId() == decorView.getId() && (!config.isEdgeOnly() || isEdgeTouched); } @Override public int clampViewPositionVertical(View child, int top, int dy) { return clamp(top, -screenHeight, screenHeight); } @Override public int getViewVerticalDragRange(View child) { return screenHeight; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); int top = releasedChild.getTop(); int settleTop = 0; int topThreshold = (int) (getHeight() * config.getDistanceThreshold()); boolean isSideSwiping = Math.abs(xvel) > config.getVelocityThreshold(); if (yvel > 0) { // Being slinged down if (Math.abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) { settleTop = screenHeight; } else if (top > topThreshold) { settleTop = screenHeight; } } else if (yvel < 0) { // Being slinged up if (Math.abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) { settleTop = -screenHeight; } else if (top < -topThreshold) { settleTop = -screenHeight; } } else { if (top > topThreshold) { settleTop = screenHeight; } else if (top < -topThreshold) { settleTop = -screenHeight; } } dragHelper.settleCapturedViewAt(releasedChild.getLeft(), settleTop); invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); float percent = 1f - ((float) Math.abs(top) / (float) screenHeight); if (listener != null) listener.onSlideChange(percent); // Update the dimmer alpha applyScrim(percent); } @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); if (listener != null) listener.onStateChanged(state); switch (state) { case ViewDragHelper.STATE_IDLE: if (decorView.getTop() == 0) { // State Open if (listener != null) listener.onOpened(); } else { // State Closed if (listener != null) listener.onClosed(); } break; case ViewDragHelper.STATE_DRAGGING: break; case ViewDragHelper.STATE_SETTLING: break; } } }; /** * The drag helper callbacks for dragging the slidr attachment in both horizontal directions */ private final ViewDragHelper.Callback horizontalCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { boolean edgeCase = !config.isEdgeOnly() || dragHelper.isEdgeTouched(edgePosition, pointerId); return child.getId() == decorView.getId() && edgeCase; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return clamp(left, -screenWidth, screenWidth); } @Override public int getViewHorizontalDragRange(View child) { return screenWidth; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); int left = releasedChild.getLeft(); int settleLeft = 0; int leftThreshold = (int) (getWidth() * config.getDistanceThreshold()); boolean isVerticalSwiping = Math.abs(yvel) > config.getVelocityThreshold(); if (xvel > 0) { if (Math.abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) { settleLeft = screenWidth; } else if (left > leftThreshold) { settleLeft = screenWidth; } } else if (xvel < 0) { if (Math.abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) { settleLeft = -screenWidth; } else if (left < -leftThreshold) { settleLeft = -screenWidth; } } else { if (left > leftThreshold) { settleLeft = screenWidth; } else if (left < -leftThreshold) { settleLeft = -screenWidth; } } dragHelper.settleCapturedViewAt(settleLeft, releasedChild.getTop()); invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); float percent = 1f - ((float) Math.abs(left) / (float) screenWidth); if (listener != null) listener.onSlideChange(percent); // Update the dimmer alpha applyScrim(percent); } @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); if (listener != null) listener.onStateChanged(state); switch (state) { case ViewDragHelper.STATE_IDLE: if (decorView.getLeft() == 0) { // State Open if (listener != null) listener.onOpened(); } else { // State Closed if (listener != null) listener.onClosed(); } break; case ViewDragHelper.STATE_DRAGGING: break; case ViewDragHelper.STATE_SETTLING: break; } } }; private void init() { setWillNotDraw(false); screenWidth = getResources().getDisplayMetrics().widthPixels; final float density = getResources().getDisplayMetrics().density; final float minVel = MIN_FLING_VELOCITY * density; ViewDragHelper.Callback callback; switch (config.getPosition()) { case LEFT: callback = leftCallback; edgePosition = ViewDragHelper.EDGE_LEFT; break; case RIGHT: callback = rightCallback; edgePosition = ViewDragHelper.EDGE_RIGHT; break; case TOP: callback = topCallback; edgePosition = ViewDragHelper.EDGE_TOP; break; case BOTTOM: callback = bottomCallback; edgePosition = ViewDragHelper.EDGE_BOTTOM; break; case VERTICAL: callback = verticalCallback; edgePosition = ViewDragHelper.EDGE_TOP | ViewDragHelper.EDGE_BOTTOM; break; case HORIZONTAL: callback = horizontalCallback; edgePosition = ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_RIGHT; break; default: callback = leftCallback; edgePosition = ViewDragHelper.EDGE_LEFT; } dragHelper = ViewDragHelper.create(this, config.getSensitivity(), callback); dragHelper.setMinVelocity(minVel); dragHelper.setEdgeTrackingEnabled(edgePosition); ViewGroupCompat.setMotionEventSplittingEnabled(this, false); // Setup the dimmer view scrimPaint = new Paint(); scrimPaint.setColor(config.getScrimColor()); scrimPaint.setAlpha(toAlpha(config.getScrimStartAlpha())); scrimRenderer = new ScrimRenderer(this, decorView); /* * This is so we can get the height of the view and * ignore the system navigation that would be included if we * retrieved this value from the DisplayMetrics */ post(() -> screenHeight = getHeight()); } public void lock() { dragHelper.abort(); isLocked = true; } public void unlock() { dragHelper.abort(); isLocked = false; } private boolean canDragFromEdge(MotionEvent ev) { float x = ev.getX(); float y = ev.getY(); switch (config.getPosition()) { case LEFT: return x < config.getEdgeSize(getWidth()); case RIGHT: return x > getWidth() - config.getEdgeSize(getWidth()); case BOTTOM: return y > getHeight() - config.getEdgeSize(getHeight()); case TOP: return y < config.getEdgeSize(getHeight()); case HORIZONTAL: return x < config.getEdgeSize(getWidth()) || x > getWidth() - config.getEdgeSize(getWidth()); case VERTICAL: return y < config.getEdgeSize(getHeight()) || y > getHeight() - config.getEdgeSize(getHeight()); } return false; } private void applyScrim(float percent) { float alpha = (percent * (config.getScrimStartAlpha() - config.getScrimEndAlpha())) + config.getScrimEndAlpha(); scrimPaint.setAlpha(toAlpha(alpha)); invalidate(scrimRenderer.getDirtyRect(config.getPosition())); } private static int clamp(int value, int min, int max) { return Math.max(min, Math.min(max, value)); } private static int toAlpha(float percentage) { return (int) (percentage * 255); } /** * The panel sliding interface that gets called * whenever the panel is closed or opened */ public interface OnPanelSlideListener { void onStateChanged(int state); void onClosed(); void onOpened(); void onSlideChange(float percent); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeAppLockEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeAppLockEvent { public boolean appLock; public long appLockTimeout; public ChangeAppLockEvent(boolean appLock, long appLockTimeout) { this.appLock = appLock; this.appLockTimeout = appLockTimeout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeAutoplayNsfwVideosEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeAutoplayNsfwVideosEvent { public boolean autoplayNsfwVideos; public ChangeAutoplayNsfwVideosEvent(boolean autoplayNsfwVideos) { this.autoplayNsfwVideos = autoplayNsfwVideos; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeCompactLayoutToolbarHiddenByDefaultEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeCompactLayoutToolbarHiddenByDefaultEvent { public boolean compactLayoutToolbarHiddenByDefault; public ChangeCompactLayoutToolbarHiddenByDefaultEvent(boolean compactLayoutToolbarHiddenByDefault) { this.compactLayoutToolbarHiddenByDefault = compactLayoutToolbarHiddenByDefault; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeDataSavingModeEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeDataSavingModeEvent { public String dataSavingMode; public ChangeDataSavingModeEvent(String dataSavingMode) { this.dataSavingMode = dataSavingMode; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeDefaultLinkPostLayoutEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeDefaultLinkPostLayoutEvent { public int defaultLinkPostLayout; public ChangeDefaultLinkPostLayoutEvent(int defaultLinkPostLayout) { this.defaultLinkPostLayout = defaultLinkPostLayout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeDefaultPostLayoutEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeDefaultPostLayoutEvent { public int defaultPostLayout; public ChangeDefaultPostLayoutEvent(int defaultPostLayout) { this.defaultPostLayout = defaultPostLayout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeDefaultPostLayoutUnfoldedEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeDefaultPostLayoutUnfoldedEvent { public int defaultPostLayoutUnfolded; public ChangeDefaultPostLayoutUnfoldedEvent(int defaultPostLayoutUnfolded) { this.defaultPostLayoutUnfolded = defaultPostLayoutUnfolded; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeDisableImagePreviewEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeDisableImagePreviewEvent { public boolean disableImagePreview; public ChangeDisableImagePreviewEvent(boolean disableImagePreview) { this.disableImagePreview = disableImagePreview; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeDisableSwipingBetweenTabsEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeDisableSwipingBetweenTabsEvent { public boolean disableSwipingBetweenTabs; public ChangeDisableSwipingBetweenTabsEvent(boolean disableSwipingBetweenTabs) { this.disableSwipingBetweenTabs = disableSwipingBetweenTabs; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeEasierToWatchInFullScreenEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeEasierToWatchInFullScreenEvent { public boolean easierToWatchInFullScreen; public ChangeEasierToWatchInFullScreenEvent(boolean easierToWatchInFullScreen) { this.easierToWatchInFullScreen = easierToWatchInFullScreen; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeEnableSwipeActionSwitchEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeEnableSwipeActionSwitchEvent { public boolean enableSwipeAction; public ChangeEnableSwipeActionSwitchEvent(boolean enableSwipeAction) { this.enableSwipeAction = enableSwipeAction; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeFixedHeightPreviewInCardEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeFixedHeightPreviewInCardEvent { public boolean fixedHeightPreviewInCard; public ChangeFixedHeightPreviewInCardEvent(boolean fixedHeightPreviewInCard) { this.fixedHeightPreviewInCard = fixedHeightPreviewInCard; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHideFabInPostFeedEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHideFabInPostFeedEvent { public boolean hideFabInPostFeed; public ChangeHideFabInPostFeedEvent(boolean hideFabInPostFeed) { this.hideFabInPostFeed = hideFabInPostFeed; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHideKarmaEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHideKarmaEvent { public boolean hideKarma; public ChangeHideKarmaEvent(boolean showKarma) { this.hideKarma = showKarma; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHidePostFlairEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHidePostFlairEvent { public boolean hidePostFlair; public ChangeHidePostFlairEvent(boolean hidePostFlair) { this.hidePostFlair = hidePostFlair; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHidePostTypeEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHidePostTypeEvent { public boolean hidePostType; public ChangeHidePostTypeEvent(boolean hidePostType) { this.hidePostType = hidePostType; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHideSubredditAndUserPrefixEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHideSubredditAndUserPrefixEvent { public boolean hideSubredditAndUserPrefix; public ChangeHideSubredditAndUserPrefixEvent(boolean hideSubredditAndUserPrefix) { this.hideSubredditAndUserPrefix = hideSubredditAndUserPrefix; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHideTextPostContent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHideTextPostContent { public boolean hideTextPostContent; public ChangeHideTextPostContent(boolean hideTextPostContent) { this.hideTextPostContent = hideTextPostContent; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHideTheNumberOfCommentsEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHideTheNumberOfCommentsEvent { public boolean hideTheNumberOfComments; public ChangeHideTheNumberOfCommentsEvent(boolean hideTheNumberOfComments) { this.hideTheNumberOfComments = hideTheNumberOfComments; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeHideTheNumberOfVotesEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeHideTheNumberOfVotesEvent { public boolean hideTheNumberOfVotes; public ChangeHideTheNumberOfVotesEvent(boolean hideTheNumberOfVotes) { this.hideTheNumberOfVotes = hideTheNumberOfVotes; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeInboxCountEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeInboxCountEvent { public int inboxCount; public ChangeInboxCountEvent(int inboxCount) { this.inboxCount = inboxCount; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeLockBottomAppBarEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeLockBottomAppBarEvent { public boolean lockBottomAppBar; public ChangeLockBottomAppBarEvent(boolean lockBottomAppBar) { this.lockBottomAppBar = lockBottomAppBar; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeLongPressToHideToolbarInCompactLayoutEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeLongPressToHideToolbarInCompactLayoutEvent { public boolean longPressToHideToolbarInCompactLayout; public ChangeLongPressToHideToolbarInCompactLayoutEvent(boolean longPressToHideToolbarInCompactLayout) { this.longPressToHideToolbarInCompactLayout = longPressToHideToolbarInCompactLayout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeMuteAutoplayingVideosEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeMuteAutoplayingVideosEvent { public boolean muteAutoplayingVideos; public ChangeMuteAutoplayingVideosEvent(boolean muteAutoplayingVideos) { this.muteAutoplayingVideos = muteAutoplayingVideos; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeMuteNSFWVideoEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeMuteNSFWVideoEvent { public boolean muteNSFWVideo; public ChangeMuteNSFWVideoEvent(boolean muteNSFWVideo) { this.muteNSFWVideo = muteNSFWVideo; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeNSFWBlurEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeNSFWBlurEvent { public boolean needBlurNSFW; public boolean doNotBlurNsfwInNsfwSubreddits; public ChangeNSFWBlurEvent(boolean needBlurNSFW, boolean doNotBlurNsfwInNsfwSubreddits) { this.needBlurNSFW = needBlurNSFW; this.doNotBlurNsfwInNsfwSubreddits = doNotBlurNsfwInNsfwSubreddits; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeNSFWEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeNSFWEvent { public boolean nsfw; public ChangeNSFWEvent(boolean nsfw) { this.nsfw = nsfw; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeNetworkStatusEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeNetworkStatusEvent { public int connectedNetwork; public ChangeNetworkStatusEvent(int connectedNetwork) { this.connectedNetwork = connectedNetwork; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeOnlyDisablePreviewInVideoAndGifPostsEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeOnlyDisablePreviewInVideoAndGifPostsEvent { public boolean onlyDisablePreviewInVideoAndGifPosts; public ChangeOnlyDisablePreviewInVideoAndGifPostsEvent(boolean onlyDisablePreviewInVideoAndGifPosts) { this.onlyDisablePreviewInVideoAndGifPosts = onlyDisablePreviewInVideoAndGifPosts; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangePostFeedMaxResolutionEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangePostFeedMaxResolutionEvent { public int postFeedMaxResolution; public ChangePostFeedMaxResolutionEvent(int postFeedMaxResolution) { this.postFeedMaxResolution = postFeedMaxResolution; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangePostLayoutEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangePostLayoutEvent { public int postLayout; public ChangePostLayoutEvent(int postLayout) { this.postLayout = postLayout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangePullToRefreshEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangePullToRefreshEvent { public boolean pullToRefresh; public ChangePullToRefreshEvent(boolean pullToRefresh) { this.pullToRefresh = pullToRefresh; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeRememberMutingOptionInPostFeedEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeRememberMutingOptionInPostFeedEvent { public boolean rememberMutingOptionInPostFeedEvent; public ChangeRememberMutingOptionInPostFeedEvent(boolean rememberMutingOptionInPostFeedEvent) { this.rememberMutingOptionInPostFeedEvent = rememberMutingOptionInPostFeedEvent; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeRequireAuthToAccountSectionEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeRequireAuthToAccountSectionEvent { public boolean requireAuthToAccountSection; public ChangeRequireAuthToAccountSectionEvent(boolean requireAuthToAccountSection) { this.requireAuthToAccountSection = requireAuthToAccountSection; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeSavePostFeedScrolledPositionEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeSavePostFeedScrolledPositionEvent { public boolean savePostFeedScrolledPosition; public ChangeSavePostFeedScrolledPositionEvent(boolean savePostFeedScrolledPosition) { this.savePostFeedScrolledPosition = savePostFeedScrolledPosition; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeShowAbsoluteNumberOfVotesEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeShowAbsoluteNumberOfVotesEvent { public boolean showAbsoluteNumberOfVotes; public ChangeShowAbsoluteNumberOfVotesEvent(boolean showAbsoluteNumberOfVotes) { this.showAbsoluteNumberOfVotes = showAbsoluteNumberOfVotes; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent { public boolean showAvatarOnTheRightInTheNavigationDrawer; public ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent(boolean showAvatarOnTheRightInTheNavigationDrawer) { this.showAvatarOnTheRightInTheNavigationDrawer = showAvatarOnTheRightInTheNavigationDrawer; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeShowElapsedTimeEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeShowElapsedTimeEvent { public boolean showElapsedTime; public ChangeShowElapsedTimeEvent(boolean showElapsedTime) { this.showElapsedTime = showElapsedTime; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeSpoilerBlurEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeSpoilerBlurEvent { public boolean needBlurSpoiler; public ChangeSpoilerBlurEvent(boolean needBlurSpoiler) { this.needBlurSpoiler = needBlurSpoiler; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeStartAutoplayVisibleAreaOffsetEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeStartAutoplayVisibleAreaOffsetEvent { public double startAutoplayVisibleAreaOffset; public ChangeStartAutoplayVisibleAreaOffsetEvent(double startAutoplayVisibleAreaOffset) { this.startAutoplayVisibleAreaOffset = startAutoplayVisibleAreaOffset; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeSwipeActionEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeSwipeActionEvent { public int swipeLeftAction; public int swipeRightAction; public ChangeSwipeActionEvent(int swipeLeftAction, int swipeRightAction) { this.swipeLeftAction = swipeLeftAction; this.swipeRightAction = swipeRightAction; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeSwipeActionThresholdEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeSwipeActionThresholdEvent { public float swipeActionThreshold; public ChangeSwipeActionThresholdEvent(float swipeActionThreshold) { this.swipeActionThreshold = swipeActionThreshold; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeTimeFormatEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeTimeFormatEvent { public String timeFormat; public ChangeTimeFormatEvent(String timeFormat) { this.timeFormat = timeFormat; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeVibrateWhenActionTriggeredEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeVibrateWhenActionTriggeredEvent { public boolean vibrateWhenActionTriggered; public ChangeVibrateWhenActionTriggeredEvent(boolean vibrateWhenActionTriggered) { this.vibrateWhenActionTriggered = vibrateWhenActionTriggered; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeVideoAutoplayEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeVideoAutoplayEvent { public String autoplay; public ChangeVideoAutoplayEvent(String autoplay) { this.autoplay = autoplay; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ChangeVoteButtonsPositionEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ChangeVoteButtonsPositionEvent { public boolean voteButtonsOnTheRight; public ChangeVoteButtonsPositionEvent(boolean voteButtonsOnTheRight) { this.voteButtonsOnTheRight = voteButtonsOnTheRight; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/FinishViewMediaActivityEvent.kt ================================================ package ml.docilealligator.infinityforreddit.events class FinishViewMediaActivityEvent { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/FlairSelectedEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; import ml.docilealligator.infinityforreddit.subreddit.Flair; public class FlairSelectedEvent { public long viewPostDetailFragmentId; public Flair flair; public FlairSelectedEvent(long viewPostDetailFragmentId, Flair flair) { this.viewPostDetailFragmentId = viewPostDetailFragmentId; this.flair = flair; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/GoBackToMainPageEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class GoBackToMainPageEvent { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/NeedForPostListFromPostFragmentEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class NeedForPostListFromPostFragmentEvent { public long postFragmentTimeId; public NeedForPostListFromPostFragmentEvent(long postFragmentId) { this.postFragmentTimeId = postFragmentId; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/NewUserLoggedInEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class NewUserLoggedInEvent { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/PassPrivateMessageEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; import ml.docilealligator.infinityforreddit.message.Message; public class PassPrivateMessageEvent { public Message message; public PassPrivateMessageEvent(Message message) { this.message = message; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/PassPrivateMessageIndexEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class PassPrivateMessageIndexEvent { public int privateMessageIndex; public PassPrivateMessageIndexEvent(int privateMessageIndex) { this.privateMessageIndex = privateMessageIndex; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/PostUpdateEventToPostDetailFragment.java ================================================ package ml.docilealligator.infinityforreddit.events; import ml.docilealligator.infinityforreddit.post.Post; public class PostUpdateEventToPostDetailFragment { public final Post post; public PostUpdateEventToPostDetailFragment(Post post) { this.post = post; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/PostUpdateEventToPostList.java ================================================ package ml.docilealligator.infinityforreddit.events; import ml.docilealligator.infinityforreddit.post.Post; public class PostUpdateEventToPostList { public final Post post; public final int positionInList; public PostUpdateEventToPostList(Post post, int positionInList) { this.post = post; this.positionInList = positionInList; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ProvidePostListToViewPostDetailActivityEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; public class ProvidePostListToViewPostDetailActivityEvent { public long postFragmentId; public ArrayList posts; public int postType; public String subredditName; public String concatenatedSubredditNames; public String username; public String userWhere; public String multiPath; public String query; public String trendingSource; public PostFilter postFilter; public SortType sortType; public ReadPostsListInterface readPostsList; public ProvidePostListToViewPostDetailActivityEvent(long postFragmentId, ArrayList posts, int postType, String subredditName, String concatenatedSubredditNames, String username, String userWhere, String multiPath, String query, String trendingSource, PostFilter postFilter, SortType sortType, ReadPostsListInterface readPostsList) { this.postFragmentId = postFragmentId; this.posts = posts; this.postType = postType; this.subredditName = subredditName; this.concatenatedSubredditNames = concatenatedSubredditNames; this.username = username; this.userWhere = userWhere; this.multiPath = multiPath; this.query = query; this.trendingSource = trendingSource; this.postFilter = postFilter; this.sortType = sortType; this.readPostsList = readPostsList; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/RecreateActivityEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class RecreateActivityEvent { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/RefreshMultiRedditsEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class RefreshMultiRedditsEvent { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/RepliedToPrivateMessageEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; import ml.docilealligator.infinityforreddit.message.Message; public class RepliedToPrivateMessageEvent { public Message newReply; public int messagePosition; public RepliedToPrivateMessageEvent(Message newReply, int messagePosition) { this.newReply = newReply; this.messagePosition = messagePosition; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ShowDividerInCompactLayoutPreferenceEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ShowDividerInCompactLayoutPreferenceEvent { public boolean showDividerInCompactLayout; public ShowDividerInCompactLayoutPreferenceEvent(boolean showDividerInCompactLayout) { this.showDividerInCompactLayout = showDividerInCompactLayout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ShowThumbnailOnTheLeftInCompactLayoutEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ShowThumbnailOnTheLeftInCompactLayoutEvent { public boolean showThumbnailOnTheLeftInCompactLayout; public ShowThumbnailOnTheLeftInCompactLayoutEvent(boolean showThumbnailOnTheLeftInCompactLayout) { this.showThumbnailOnTheLeftInCompactLayout = showThumbnailOnTheLeftInCompactLayout; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitChangeAvatarEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SubmitChangeAvatarEvent { public final boolean isSuccess; public final String errorMessage; public SubmitChangeAvatarEvent(boolean isSuccess, String errorMessage) { this.isSuccess = isSuccess; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitChangeBannerEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SubmitChangeBannerEvent { public final boolean isSuccess; public final String errorMessage; public SubmitChangeBannerEvent(boolean isSuccess, String errorMessage) { this.isSuccess = isSuccess; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitCrosspostEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; import ml.docilealligator.infinityforreddit.post.Post; public class SubmitCrosspostEvent { public boolean postSuccess; public Post post; public String errorMessage; public SubmitCrosspostEvent(boolean postSuccess, Post post, String errorMessage) { this.postSuccess = postSuccess; this.post = post; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitGalleryPostEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SubmitGalleryPostEvent { public boolean postSuccess; public String postUrl; public String errorMessage; public SubmitGalleryPostEvent(boolean postSuccess, String postUrl, String errorMessage) { this.postSuccess = postSuccess; this.postUrl = postUrl; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitImagePostEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SubmitImagePostEvent { public boolean postSuccess; public String errorMessage; public SubmitImagePostEvent(boolean postSuccess, String errorMessage) { this.postSuccess = postSuccess; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitPollPostEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SubmitPollPostEvent { public boolean postSuccess; public String postUrl; public String errorMessage; public SubmitPollPostEvent(boolean postSuccess, String postUrl, String errorMessage) { this.postSuccess = postSuccess; this.postUrl = postUrl; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitSaveProfileEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SubmitSaveProfileEvent { public final boolean isSuccess; public final String errorMessage; public SubmitSaveProfileEvent(boolean isSuccess, String errorMessage) { this.isSuccess = isSuccess; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitTextOrLinkPostEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; import ml.docilealligator.infinityforreddit.post.Post; public class SubmitTextOrLinkPostEvent { public boolean postSuccess; public Post post; public String errorMessage; public SubmitTextOrLinkPostEvent(boolean postSuccess, Post post, String errorMessage) { this.postSuccess = postSuccess; this.post = post; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SubmitVideoOrGifPostEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SubmitVideoOrGifPostEvent { public boolean postSuccess; public boolean errorProcessingVideoOrGif; public String errorMessage; public SubmitVideoOrGifPostEvent(boolean postSuccess, boolean errorProcessingVideoOrGif, String errorMessage) { this.postSuccess = postSuccess; this.errorProcessingVideoOrGif = errorProcessingVideoOrGif; this.errorMessage = errorMessage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/SwitchAccountEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class SwitchAccountEvent { public String excludeActivityClassName; public SwitchAccountEvent() { } public SwitchAccountEvent(String excludeActivityClassName) { this.excludeActivityClassName = excludeActivityClassName; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/events/ToggleSecureModeEvent.java ================================================ package ml.docilealligator.infinityforreddit.events; public class ToggleSecureModeEvent { public boolean isSecureMode; public ToggleSecureModeEvent(boolean isSecureMode) { this.isSecureMode = isSecureMode; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/font/ContentFontFamily.java ================================================ package ml.docilealligator.infinityforreddit.font; import ml.docilealligator.infinityforreddit.R; public enum ContentFontFamily { Default(R.style.ContentFontFamily, "Default"), BalsamiqSans(R.style.ContentFontFamily_BalsamiqSans, "BalsamiqSansBold"), BalsamiqSansBold(R.style.ContentFontFamily_BalsamiqSansBold, "BalsamiqSansBold"), NotoSans(R.style.ContentFontFamily_NotoSans, "NotoSans"), NotoSansBold(R.style.ContentFontFamily_NotoSansBold, "NotoSansBold"), RobotoCondensed(R.style.ContentFontFamily_RobotoCondensed, "RobotoCondensed"), RobotoCondensedBold(R.style.ContentFontFamily_RobotoCondensedBold, "RobotoCondensedBold"), HarmoniaSans(R.style.ContentFontFamily_HarmoniaSans, "HarmoniaSans"), HarmoniaSansBold(R.style.ContentFontFamily_HarmoniaSansBold, "HarmoniaSansBold"), Inter(R.style.ContentFontFamily_Inter, "Inter"), InterBold(R.style.ContentFontFamily_InterBold, "InterBold"), Manrope(R.style.ContentFontFamily_Manrope, "Manrope"), ManropeBold(R.style.ContentFontFamily_ManropeBold, "ManropeBold"), Sriracha(R.style.ContentFontFamily_Sriracha, "Sriracha"), AtkinsonHyperlegible(R.style.ContentFontFamily_AtkinsonHyperlegible, "AtkinsonHyperlegible"), AtkinsonHyperlegibleBold(R.style.ContentFontFamily_AtkinsonHyperlegibleBold, "AtkinsonHyperlegibleBold"), Custom(R.style.ContentFontFamily, "Custom"); private final int resId; private final String title; ContentFontFamily(int resId, String title) { this.resId = resId; this.title = title; } public int getResId() { return resId; } public String getTitle() { return title; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/font/ContentFontStyle.java ================================================ package ml.docilealligator.infinityforreddit.font; import ml.docilealligator.infinityforreddit.R; public enum ContentFontStyle { XSmall(R.style.ContentFontStyle_XSmall, "XSmall"), Small(R.style.ContentFontStyle_Small, "Small"), Normal(R.style.ContentFontStyle_Normal, "Normal"), Large(R.style.ContentFontStyle_Large, "Large"), XLarge(R.style.ContentFontStyle_XLarge, "XLarge"), XXLarge(R.style.ContentFontStyle_XXLarge, "XXLarge"); private final int resId; private final String title; ContentFontStyle(int resId, String title) { this.resId = resId; this.title = title; } public int getResId() { return resId; } public String getTitle() { return title; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/font/FontFamily.java ================================================ package ml.docilealligator.infinityforreddit.font; import ml.docilealligator.infinityforreddit.R; public enum FontFamily { Default(R.style.FontFamily, "Default"), BalsamiqSans(R.style.FontFamily_BalsamiqSans, "BalsamiqSans"), BalsamiqSansBold(R.style.FontFamily_BalsamiqSansBold, "BalsamiqSansBold"), NotoSans(R.style.FontFamily_NotoSans, "NotoSans"), NotoSansBold(R.style.FontFamily_NotoSansBold, "NotoSansBold"), RobotoCondensed(R.style.FontFamily_RobotoCondensed, "RobotoCondensed"), RobotoCondensedBold(R.style.FontFamily_RobotoCondensedBold, "RobotoCondensedBold"), HarmoniaSans(R.style.FontFamily_HarmoniaSans, "HarmoniaSans"), HarmoniaSansBold(R.style.FontFamily_HarmoniaSansBold, "HarmoniaSansBold"), Inter(R.style.FontFamily_Inter, "Inter"), InterBold(R.style.FontFamily_InterBold, "InterBold"), Manrope(R.style.FontFamily_Manrope, "Manrope"), ManropeBold(R.style.FontFamily_ManropeBold, "ManropeBold"), Sriracha(R.style.FontFamily_Sriracha, "Sriracha"), AtkinsonHyperlegible(R.style.FontFamily_AtkinsonHyperlegible, "AtkinsonHyperlegible"), AtkinsonHyperlegibleBold(R.style.FontFamily_AtkinsonHyperlegibleBold, "AtkinsonHyperlegibleBold"), Custom(R.style.FontFamily, "Custom"); private final int resId; private final String title; FontFamily(int resId, String title) { this.resId = resId; this.title = title; } public int getResId() { return resId; } public String getTitle() { return title; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/font/FontStyle.java ================================================ package ml.docilealligator.infinityforreddit.font; import ml.docilealligator.infinityforreddit.R; public enum FontStyle { XSmall(R.style.FontStyle_XSmall, "XSmall"), Small(R.style.FontStyle_Small, "Small"), Normal(R.style.FontStyle_Normal, "Normal"), Large(R.style.FontStyle_Large, "Large"), XLarge(R.style.FontStyle_XLarge, "XLarge"); private final int resId; private final String title; FontStyle(int resId, String title) { this.resId = resId; this.title = title; } public int getResId() { return resId; } public String getTitle() { return title; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/font/TitleFontFamily.java ================================================ package ml.docilealligator.infinityforreddit.font; import ml.docilealligator.infinityforreddit.R; public enum TitleFontFamily { Default(R.style.TitleFontFamily, "Default"), BalsamiqSans(R.style.TitleFontFamily_BalsamiqSans, "BalsamiqSans"), BalsamiqSansBold(R.style.TitleFontFamily_BalsamiqSansBold, "BalsamiqSansBold"), NotoSans(R.style.TitleFontFamily_NotoSans, "NotoSans"), NotoSansBold(R.style.TitleFontFamily_NotoSansBold, "NotoSansBold"), RobotoCondensed(R.style.TitleFontFamily_RobotoCondensed, "RobotoCondensed"), RobotoCondensedBold(R.style.TitleFontFamily_RobotoCondensedBold, "RobotoCondensedBold"), HarmoniaSans(R.style.TitleFontFamily_HarmoniaSans, "HarmoniaSans"), HarmoniaSansBold(R.style.TitleFontFamily_HarmoniaSansBold, "HarmoniaSansBold"), Inter(R.style.TitleFontFamily_Inter, "Inter"), InterBold(R.style.TitleFontFamily_InterBold, "InterBold"), Manrope(R.style.TitleFontFamily_Manrope, "Manrope"), ManropeBold(R.style.TitleFontFamily_ManropeBold, "ManropeBold"), Sriracha(R.style.TitleFontFamily_Sriracha, "Sriracha"), AtkinsonHyperlegible(R.style.TitleFontFamily_AtkinsonHyperlegible, "AtkinsonHyperlegible"), AtkinsonHyperlegibleBold(R.style.TitleFontFamily_AtkinsonHyperlegibleBold, "AtkinsonHyperlegibleBold"), Custom(R.style.TitleFontFamily, "Custom"); private final int resId; private final String title; TitleFontFamily(int resId, String title) { this.resId = resId; this.title = title; } public int getResId() { return resId; } public String getTitle() { return title; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/font/TitleFontStyle.java ================================================ package ml.docilealligator.infinityforreddit.font; import ml.docilealligator.infinityforreddit.R; public enum TitleFontStyle { XSmall(R.style.TitleFontStyle_XSmall, "XSmall"), Small(R.style.TitleFontStyle_Small, "Small"), Normal(R.style.TitleFontStyle_Normal, "Normal"), Large(R.style.TitleFontStyle_Large, "Large"), XLarge(R.style.TitleFontStyle_XLarge, "XLarge"); private final int resId; private final String title; TitleFontStyle(int resId, String title) { this.resId = resId; this.title = title; } public int getResId() { return resId; } public String getTitle() { return title; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/CommentsListingFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Canvas; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.CommentModerationActionHandler; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.CommentsListingRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.comment.CommentViewModel; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.AdjustableTouchSlopItemTouchHelper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentCommentsListingBinding; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.thing.ReplyNotificationsToggle; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; /** * A simple {@link Fragment} subclass. */ public class CommentsListingFragment extends Fragment implements FragmentCommunicator, CommentModerationActionHandler { public static final String EXTRA_USERNAME = "EN"; public static final String EXTRA_ARE_SAVED_COMMENTS = "EISC"; CommentViewModel mCommentViewModel; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor mExecutor; private RequestManager mGlide; private BaseActivity mActivity; private LinearLayoutManagerBugFixed mLinearLayoutManager; private CommentsListingRecyclerViewAdapter mAdapter; private SortType sortType; private ColorDrawable backgroundSwipeRight; private ColorDrawable backgroundSwipeLeft; private Drawable drawableSwipeRight; private Drawable drawableSwipeLeft; private int swipeLeftAction; private int swipeRightAction; private float swipeActionThreshold; private AdjustableTouchSlopItemTouchHelper touchHelper; private boolean shouldSwipeBack; private FragmentCommentsListingBinding binding; public CommentsListingFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentCommentsListingBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); EventBus.getDefault().register(this); applyTheme(); mGlide = Glide.with(mActivity); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewCommentsListingFragment.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); //binding.recyclerViewCommentsListingFragment.setPadding(0, 0, 0, mActivity.getNavBarHeight()); }/* else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { binding.recyclerViewCommentsListingFragment.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); } }*/ boolean enableSwipeAction = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SWIPE_ACTION, false); boolean vibrateWhenActionTriggered = mSharedPreferences.getBoolean(SharedPreferencesUtils.VIBRATE_WHEN_ACTION_TRIGGERED, true); swipeActionThreshold = Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_ACTION_THRESHOLD, "0.3")); swipeRightAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_RIGHT_ACTION, "1")); swipeLeftAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_LEFT_ACTION, "0")); initializeSwipeActionDrawable(); touchHelper = new AdjustableTouchSlopItemTouchHelper(new AdjustableTouchSlopItemTouchHelper.Callback() { boolean exceedThreshold = false; @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { if (!(viewHolder instanceof CommentsListingRecyclerViewAdapter.CommentBaseViewHolder)) { return makeMovementFlags(0, 0); } int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; return makeMovementFlags(0, swipeFlags); } @Override public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { return false; } @Override public boolean isItemViewSwipeEnabled() { return true; } @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {} @Override public int convertToAbsoluteDirection(int flags, int layoutDirection) { if (shouldSwipeBack) { shouldSwipeBack = false; return 0; } return super.convertToAbsoluteDirection(flags, layoutDirection); } @Override public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { View itemView = viewHolder.itemView; int horizontalOffset = (int) Utils.convertDpToPixel(16, mActivity); if (dX > 0) { if (dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) { dX = (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold; if (!exceedThreshold && isCurrentlyActive) { exceedThreshold = true; if (vibrateWhenActionTriggered) { itemView.setHapticFeedbackEnabled(true); itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } backgroundSwipeRight.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom()); } else { exceedThreshold = false; backgroundSwipeRight.setBounds(0, 0, 0, 0); } drawableSwipeRight.setBounds(itemView.getLeft() + ((int) dX) - horizontalOffset - drawableSwipeRight.getIntrinsicWidth(), (itemView.getBottom() + itemView.getTop() - drawableSwipeRight.getIntrinsicHeight()) / 2, itemView.getLeft() + ((int) dX) - horizontalOffset, (itemView.getBottom() + itemView.getTop() + drawableSwipeRight.getIntrinsicHeight()) / 2); backgroundSwipeRight.draw(c); drawableSwipeRight.draw(c); } else if (dX < 0) { if (-dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) { dX = -(itemView.getRight() - itemView.getLeft()) * swipeActionThreshold; if (!exceedThreshold && isCurrentlyActive) { exceedThreshold = true; if (vibrateWhenActionTriggered) { itemView.setHapticFeedbackEnabled(true); itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } backgroundSwipeLeft.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom()); } else { exceedThreshold = false; backgroundSwipeLeft.setBounds(0, 0, 0, 0); } drawableSwipeLeft.setBounds(itemView.getRight() + ((int) dX) + horizontalOffset, (itemView.getBottom() + itemView.getTop() - drawableSwipeLeft.getIntrinsicHeight()) / 2, itemView.getRight() + ((int) dX) + horizontalOffset + drawableSwipeLeft.getIntrinsicWidth(), (itemView.getBottom() + itemView.getTop() + drawableSwipeLeft.getIntrinsicHeight()) / 2); backgroundSwipeLeft.draw(c); drawableSwipeLeft.draw(c); } if (!isCurrentlyActive && exceedThreshold) { mAdapter.onItemSwipe(viewHolder, dX > 0 ? ItemTouchHelper.END : ItemTouchHelper.START, swipeLeftAction, swipeRightAction); exceedThreshold = false; } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } @Override public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return 100; } }); binding.recyclerViewCommentsListingFragment.setOnTouchListener((view, motionEvent) -> { shouldSwipeBack = motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP; return false; }); if (enableSwipeAction) { touchHelper.attachToRecyclerView( binding.recyclerViewCommentsListingFragment, Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_ACTION_SENSITIVITY_IN_COMMENTS, "5")) ); } new Handler().postDelayed(this::bindView, 0); return binding.getRoot(); } private void bindView() { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed()) { mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewCommentsListingFragment.setLayoutManager(mLinearLayoutManager); String username = getArguments().getString(EXTRA_USERNAME); String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_USER_COMMENT, SortType.Type.NEW.name()); if (sort.equals(SortType.Type.CONTROVERSIAL.name()) || sort.equals(SortType.Type.TOP.name())) { String sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_USER_COMMENT, SortType.Time.ALL.name()); sortType = new SortType(SortType.Type.valueOf(sort.toUpperCase()), SortType.Time.valueOf(sortTime.toUpperCase())); } else { sortType = new SortType(SortType.Type.valueOf(sort.toUpperCase())); } mAdapter = new CommentsListingRecyclerViewAdapter(mActivity, this, mOauthRetrofit, customThemeWrapper, getResources().getConfiguration().locale, mSharedPreferences, mActivity.accessToken, mActivity.accountName, username, () -> mCommentViewModel.retryLoadingMore()); binding.recyclerViewCommentsListingFragment.setAdapter(mAdapter); if (mActivity instanceof RecyclerViewContentScrollingInterface) { binding.recyclerViewCommentsListingFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); } else if (dy < 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); } } }); } CommentViewModel.Factory factory; if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { factory = new CommentViewModel.Factory(mExecutor, mActivity.mHandler, mRetrofit, null, mActivity.accountName, username, sortType, getArguments().getBoolean(EXTRA_ARE_SAVED_COMMENTS)); } else { factory = new CommentViewModel.Factory(mExecutor, mActivity.mHandler, mOauthRetrofit, mActivity.accessToken, mActivity.accountName, username, sortType, getArguments().getBoolean(EXTRA_ARE_SAVED_COMMENTS)); } mCommentViewModel = new ViewModelProvider(this, factory).get(CommentViewModel.class); mCommentViewModel.getComments().observe(getViewLifecycleOwner(), comments -> mAdapter.submitList(comments)); mCommentViewModel.hasComment().observe(getViewLifecycleOwner(), hasComment -> { binding.swipeRefreshLayoutViewCommentsListingFragment.setRefreshing(false); if (hasComment) { binding.fetchCommentsInfoLinearLayoutCommentsListingFragment.setVisibility(View.GONE); } else { binding.fetchCommentsInfoLinearLayoutCommentsListingFragment.setOnClickListener(null); showErrorView(R.string.no_comments); } }); mCommentViewModel.getInitialLoadingState().observe(getViewLifecycleOwner(), networkState -> { if (networkState.getStatus().equals(NetworkState.Status.SUCCESS)) { binding.swipeRefreshLayoutViewCommentsListingFragment.setRefreshing(false); } else if (networkState.getStatus().equals(NetworkState.Status.FAILED)) { binding.swipeRefreshLayoutViewCommentsListingFragment.setRefreshing(false); binding.fetchCommentsInfoLinearLayoutCommentsListingFragment.setOnClickListener(view -> refresh()); showErrorView(R.string.load_comments_failed); } else { binding.swipeRefreshLayoutViewCommentsListingFragment.setRefreshing(true); } }); mCommentViewModel.getPaginationNetworkState().observe(getViewLifecycleOwner(), networkState -> mAdapter.setNetworkState(networkState)); mCommentViewModel.commentModerationEventLiveData.observe(getViewLifecycleOwner(), moderationEvent -> { if (mAdapter != null) { mAdapter.updateModdedStatus(moderationEvent.getPosition()); } Toast.makeText(mActivity, moderationEvent.getToastMessageResId(), Toast.LENGTH_SHORT).show(); }); binding.swipeRefreshLayoutViewCommentsListingFragment.setOnRefreshListener(() -> mCommentViewModel.refresh()); } } @Override public void onResume() { super.onResume(); if (mAdapter != null) { mAdapter.setCanStartActivity(true); } } @Override public void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } public void changeSortType(SortType sortType) { mCommentViewModel.changeSortType(sortType); this.sortType = sortType; } private void initializeSwipeActionDrawable() { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { backgroundSwipeRight = new ColorDrawable(customThemeWrapper.getDownvoted()); drawableSwipeRight = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_downward_day_night_24dp, null); } else { backgroundSwipeRight = new ColorDrawable(customThemeWrapper.getUpvoted()); drawableSwipeRight = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_upward_day_night_24dp, null); } if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { backgroundSwipeLeft = new ColorDrawable(customThemeWrapper.getUpvoted()); drawableSwipeLeft = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_upward_day_night_24dp, null); } else { backgroundSwipeLeft = new ColorDrawable(customThemeWrapper.getDownvoted()); drawableSwipeLeft = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_downward_day_night_24dp, null); } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.mActivity = (BaseActivity) context; } @Override public void refresh() { binding.fetchCommentsInfoLinearLayoutCommentsListingFragment.setVisibility(View.GONE); mCommentViewModel.refresh(); mAdapter.setNetworkState(null); } @Override public void applyTheme() { binding.swipeRefreshLayoutViewCommentsListingFragment.setProgressBackgroundColorSchemeColor(customThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutViewCommentsListingFragment.setColorSchemeColors(customThemeWrapper.getColorAccent()); binding.fetchCommentsInfoTextViewCommentsListingFragment.setTextColor(customThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchCommentsInfoTextViewCommentsListingFragment.setTypeface(mActivity.typeface); } } private void showErrorView(int stringResId) { if (mActivity != null && isAdded()) { binding.swipeRefreshLayoutViewCommentsListingFragment.setRefreshing(false); binding.fetchCommentsInfoLinearLayoutCommentsListingFragment.setVisibility(View.VISIBLE); binding.fetchCommentsInfoTextViewCommentsListingFragment.setText(stringResId); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } public SortType getSortType() { return sortType; } public void editComment(Comment comment, int position) { if (mAdapter != null) { mAdapter.editComment(comment, position); } } public void editComment(String commentMarkdown, int position) { if (mAdapter != null) { mAdapter.editComment(commentMarkdown, position); } } public void toggleReplyNotifications(Comment comment, int position) { ReplyNotificationsToggle.toggleEnableNotification(new Handler(Looper.getMainLooper()), mOauthRetrofit, mActivity.accessToken, comment, new ReplyNotificationsToggle.SendNotificationListener() { @Override public void onSuccess() { Toast.makeText(mActivity, comment.isSendReplies() ? R.string.reply_notifications_disabled : R.string.reply_notifications_enabled, Toast.LENGTH_SHORT).show(); mAdapter.toggleReplyNotifications(position); } @Override public void onError() { Toast.makeText(mActivity, R.string.toggle_reply_notifications_failed, Toast.LENGTH_SHORT).show(); } }); } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { if (mAdapter != null) { String dataSavingMode = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { mAdapter.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); refreshAdapter(binding.recyclerViewCommentsListingFragment, mAdapter); } } } private void refreshAdapter(RecyclerView recyclerView, RecyclerView.Adapter adapter) { int previousPosition = -1; if (recyclerView.getLayoutManager() != null) { previousPosition = ((LinearLayoutManagerBugFixed) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); } RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); recyclerView.setAdapter(null); recyclerView.setLayoutManager(null); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(layoutManager); if (previousPosition > 0) { recyclerView.scrollToPosition(previousPosition); } } @Override public void approveComment(@NonNull Comment comment, int position) { mCommentViewModel.approveComment(comment, position); } @Override public void removeComment(@NonNull Comment comment, int position, boolean isSpam) { mCommentViewModel.removeComment(comment, position, isSpam); } @Override public void toggleLock(@NonNull Comment comment, int position) { mCommentViewModel.toggleLock(comment, position); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/CustomThemeListingFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity; import ml.docilealligator.infinityforreddit.adapters.CustomThemeListingRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.adapters.OnlineCustomThemeListingRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeViewModel; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.FragmentCustomThemeListingBinding; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class CustomThemeListingFragment extends Fragment { public static final String EXTRA_IS_ONLINE = "EIO"; @Inject @Named("online_custom_themes") Retrofit onlineCustomThemesRetrofit; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; @Inject @Named("light_theme") SharedPreferences lightThemeSharedPreferences; @Inject @Named("dark_theme") SharedPreferences darkThemeSharedPreferences; @Inject @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences; @Inject Executor executor; public CustomThemeViewModel customThemeViewModel; private BaseActivity mActivity; private FragmentCustomThemeListingBinding binding; private boolean isOnline; @Nullable private ActivityResultLauncher customizeThemeActivityResultLauncher; public CustomThemeListingFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); // Inflate the layout for this fragment binding = FragmentCustomThemeListingBinding.inflate(inflater, container, false); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewCustomizeThemeListingActivity.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } if (getArguments() != null) { isOnline = getArguments().getBoolean(EXTRA_IS_ONLINE); } binding.recyclerViewCustomizeThemeListingActivity.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); } else if (dy < 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); } } }); if (isOnline) { OnlineCustomThemeListingRecyclerViewAdapter adapter = new OnlineCustomThemeListingRecyclerViewAdapter(mActivity); binding.recyclerViewCustomizeThemeListingActivity.setAdapter(adapter); customThemeViewModel = new ViewModelProvider(this, new CustomThemeViewModel.Factory(executor, onlineCustomThemesRetrofit, redditDataRoomDatabase)) .get(CustomThemeViewModel.class); customThemeViewModel.getOnlineCustomThemeMetadata().observe(getViewLifecycleOwner(), customThemePagingData -> adapter.submitData(getViewLifecycleOwner().getLifecycle(), customThemePagingData)); customizeThemeActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> { if (activityResult.getResultCode() == Activity.RESULT_OK) { Intent data = activityResult.getData(); int index = data.getIntExtra(CustomizeThemeActivity.RETURN_EXTRA_INDEX_IN_THEME_LIST, -1); String themeName = data.getStringExtra(CustomizeThemeActivity.RETURN_EXTRA_THEME_NAME); String primaryColorHex = data.getStringExtra(CustomizeThemeActivity.RETURN_EXTRA_PRIMARY_COLOR); adapter.updateMetadata(index, themeName, primaryColorHex); } }); } else { CustomThemeListingRecyclerViewAdapter adapter = new CustomThemeListingRecyclerViewAdapter(mActivity, CustomThemeWrapper.getPredefinedThemes(mActivity)); binding.recyclerViewCustomizeThemeListingActivity.setAdapter(adapter); customThemeViewModel = new ViewModelProvider(this, new CustomThemeViewModel.Factory(redditDataRoomDatabase)) .get(CustomThemeViewModel.class); customThemeViewModel.getAllCustomThemes().observe(getViewLifecycleOwner(), adapter::setUserThemes); } return binding.getRoot(); } @Nullable public ActivityResultLauncher getCustomizeThemeActivityResultLauncher() { return customizeThemeActivityResultLauncher; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/FollowedUsersListingFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import me.zhanghai.android.fastscroll.FastScrollerBuilder; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.SubscribedThingListingActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.adapters.FollowedUsersRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentFollowedUsersListingBinding; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserViewModel; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; /** * A simple {@link Fragment} subclass. */ public class FollowedUsersListingFragment extends Fragment implements FragmentCommunicator { public static final String EXTRA_IS_USER_SELECTION = "EIUS"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; SubscribedUserViewModel mSubscribedUserViewModel; private BaseActivity mActivity; private RequestManager mGlide; private LinearLayoutManagerBugFixed mLinearLayoutManager; private FragmentFollowedUsersListingBinding binding; public FollowedUsersListingFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentFollowedUsersListingBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); applyTheme(); Resources resources = getResources(); if ((mActivity instanceof BaseActivity && mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge())) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewFollowedUsersListingFragment.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); //binding.recyclerViewFollowedUsersListingFragment.setPadding(0, 0, 0, mActivity.getNavBarHeight()); }/* else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { binding.recyclerViewFollowedUsersListingFragment.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); } }*/ mGlide = Glide.with(this); if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.swipeRefreshLayoutFollowedUsersListingFragment.setEnabled(false); } mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewFollowedUsersListingFragment.setLayoutManager(mLinearLayoutManager); FollowedUsersRecyclerViewAdapter adapter = new FollowedUsersRecyclerViewAdapter(mActivity, mExecutor, mOauthRetrofit, mRedditDataRoomDatabase, mCustomThemeWrapper, mActivity.accessToken, mActivity.accountName, subscribedUserData -> { if (getArguments().getBoolean(EXTRA_IS_USER_SELECTION)) { Intent returnIntent = new Intent(); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME, subscribedUserData.getName()); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.USER); mActivity.setResult(Activity.RESULT_OK, returnIntent); mActivity.finish(); } else { Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, subscribedUserData.getName()); mActivity.startActivity(intent); } }); binding.recyclerViewFollowedUsersListingFragment.setAdapter(adapter); new FastScrollerBuilder(binding.recyclerViewFollowedUsersListingFragment).useMd2Style().build(); mSubscribedUserViewModel = new ViewModelProvider(this, new SubscribedUserViewModel.Factory(mRedditDataRoomDatabase, mActivity.accountName)) .get(SubscribedUserViewModel.class); mSubscribedUserViewModel.getAllSubscribedUsers().observe(getViewLifecycleOwner(), subscribedUserData -> { binding.swipeRefreshLayoutFollowedUsersListingFragment.setRefreshing(false); if (subscribedUserData == null || subscribedUserData.size() == 0) { binding.recyclerViewFollowedUsersListingFragment.setVisibility(View.GONE); binding.noSubscriptionsLinearLayoutFollowedUsersListingFragment.setVisibility(View.VISIBLE); } else { binding.noSubscriptionsLinearLayoutFollowedUsersListingFragment.setVisibility(View.GONE); binding.recyclerViewFollowedUsersListingFragment.setVisibility(View.VISIBLE); mGlide.clear(binding.noSubscriptionsImageViewFollowedUsersListingFragment); } adapter.setSubscribedUsers(subscribedUserData); }); mSubscribedUserViewModel.getAllFavoriteSubscribedUsers().observe(getViewLifecycleOwner(), favoriteSubscribedUserData -> { binding.swipeRefreshLayoutFollowedUsersListingFragment.setRefreshing(false); if (favoriteSubscribedUserData != null && favoriteSubscribedUserData.size() > 0) { binding.noSubscriptionsLinearLayoutFollowedUsersListingFragment.setVisibility(View.GONE); binding.recyclerViewFollowedUsersListingFragment.setVisibility(View.VISIBLE); mGlide.clear(binding.noSubscriptionsImageViewFollowedUsersListingFragment); } adapter.setFavoriteSubscribedUsers(favoriteSubscribedUserData); }); return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } @Override public void stopRefreshProgressbar() { binding.swipeRefreshLayoutFollowedUsersListingFragment.setRefreshing(false); } @Override public void applyTheme() { if (mActivity instanceof SubscribedThingListingActivity) { binding.swipeRefreshLayoutFollowedUsersListingFragment.setOnRefreshListener(() -> ((SubscribedThingListingActivity) mActivity).loadSubscriptions(true)); binding.swipeRefreshLayoutFollowedUsersListingFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutFollowedUsersListingFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); } else { binding.swipeRefreshLayoutFollowedUsersListingFragment.setEnabled(false); } binding.errorTextViewFollowedUsersListingFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.errorTextViewFollowedUsersListingFragment.setTypeface(mActivity.typeface); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } public void changeSearchQuery(String searchQuery) { mSubscribedUserViewModel.setSearchQuery(searchQuery); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/FragmentCommunicator.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; public interface FragmentCommunicator { default void refresh() { } default void changeNSFW(boolean nsfw) { } default void stopRefreshProgressbar() { } void applyTheme(); default void hideReadPosts() { } default void changePostFilter(PostFilter postFilter) { } default PostFilter getPostFilter() { return null; } default void filterPosts() { } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import androidx.paging.LoadState; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.transition.AutoTransition; import androidx.transition.TransitionManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayList; import java.util.Locale; import java.util.Random; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import ml.docilealligator.infinityforreddit.FetchPostFilterAndConcatenatedSubredditNames; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.adapters.Paging3LoadingStateAdapter; import ml.docilealligator.infinityforreddit.adapters.PostRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentHistoryPostBinding; import ml.docilealligator.infinityforreddit.events.ChangeDefaultPostLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangeDefaultPostLayoutUnfoldedEvent; import ml.docilealligator.infinityforreddit.events.NeedForPostListFromPostFragmentEvent; import ml.docilealligator.infinityforreddit.events.ProvidePostListToViewPostDetailActivityEvent; import ml.docilealligator.infinityforreddit.post.HistoryPostPagingSource; import ml.docilealligator.infinityforreddit.post.HistoryPostViewModel; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesLiveDataKt; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; import retrofit2.Retrofit; public class HistoryPostFragment extends PostFragmentBase implements FragmentCommunicator { public static final String EXTRA_HISTORY_TYPE = "EHT"; public static final String EXTRA_FILTER = "EF"; public static final int HISTORY_TYPE_READ_POSTS = 1; private static final String IS_IN_LAZY_MODE_STATE = "IILMS"; private static final String RECYCLER_VIEW_POSITION_STATE = "RVPS"; private static final String READ_POST_LIST_STATE = "RPLS"; private static final String POST_FILTER_STATE = "PFS"; private static final String POST_FRAGMENT_ID_STATE = "PFIS"; HistoryPostViewModel mHistoryPostViewModel; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("redgifs") Retrofit mRedgifsRetrofit; @Inject Provider mStreamableApiProvider; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_feed_scrolled_position_cache") SharedPreferences mPostFeedScrolledPositionSharedPreferences; @Inject ExoCreator mExoCreator; @Inject Executor mExecutor; private int postType; private PostRecyclerViewAdapter mAdapter; private int maxPosition = -1; private PostFilter postFilter; private int historyType; private FragmentHistoryPostBinding binding; public HistoryPostFragment() { // Required empty public constructor } public static HistoryPostFragment newInstance() { HistoryPostFragment fragment = new HistoryPostFragment(); Bundle args = new Bundle(); fragment.setArguments(args); return fragment; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentHistoryPostBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); super.onCreateView(inflater, container, savedInstanceState); setHasOptionsMenu(true); applyTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); getPostRecyclerView().setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } binding.recyclerViewHistoryPostFragment.addOnWindowFocusChangedListener(this::onWindowFocusChanged); Resources resources = getResources(); /*if (activity.isImmersiveInterface()) { binding.recyclerViewHistoryPostFragment.setPadding(0, 0, 0, activity.getNavBarHeight()); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { binding.recyclerViewHistoryPostFragment.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); } }*/ binding.swipeRefreshLayoutHistoryPostFragment.setEnabled(mSharedPreferences.getBoolean(SharedPreferencesUtils.PULL_TO_REFRESH, true)); binding.swipeRefreshLayoutHistoryPostFragment.setOnRefreshListener(this::refresh); int recyclerViewPosition = 0; if (savedInstanceState != null) { recyclerViewPosition = savedInstanceState.getInt(RECYCLER_VIEW_POSITION_STATE); isInLazyMode = savedInstanceState.getBoolean(IS_IN_LAZY_MODE_STATE); postFilter = savedInstanceState.getParcelable(POST_FILTER_STATE); postFragmentId = savedInstanceState.getLong(POST_FRAGMENT_ID_STATE); } else { postFilter = getArguments().getParcelable(EXTRA_FILTER); postFragmentId = System.currentTimeMillis() + new Random().nextInt(1000); } if (mActivity instanceof RecyclerViewContentScrollingInterface) { binding.recyclerViewHistoryPostFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); } else if (dy < 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); } } }); } historyType = getArguments().getInt(EXTRA_HISTORY_TYPE, HISTORY_TYPE_READ_POSTS); int defaultPostLayout; boolean foldEnabled = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_FOLD_SUPPORT, false); boolean isTablet = getResources().getBoolean(R.bool.isTablet); if (foldEnabled && isTablet) { defaultPostLayout = Integer.parseInt(mSharedPreferences.getString( SharedPreferencesUtils.DEFAULT_POST_LAYOUT_UNFOLDED_KEY, "0")); } else { defaultPostLayout = Integer.parseInt(mSharedPreferences.getString( SharedPreferencesUtils.DEFAULT_POST_LAYOUT_KEY, "0")); } Locale locale = getResources().getConfiguration().locale; if (historyType == HISTORY_TYPE_READ_POSTS) { postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.HISTORY_POST_LAYOUT_READ_POST, defaultPostLayout); mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, true, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, null, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { /*Intent intent = new Intent(activity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, username); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_USER_WHERE, where); intent.putExtra(FilteredPostsActivity.EXTRA_FILTER, filter); startActivity(intent);*/ } @Override public void flairChipClicked(String flair) { /*Intent intent = new Intent(activity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, username); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_USER_WHERE, where); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent);*/ } @Override public void nsfwChipClicked() { /*Intent intent = new Intent(activity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, username); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_USER_WHERE, where); intent.putExtra(FilteredPostsActivity.EXTRA_FILTER, Post.NSFW_TYPE); startActivity(intent);*/ } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewHistoryPostFragment, new AutoTransition()); } }); } int nColumns = getNColumns(resources); if (nColumns == 1) { mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewHistoryPostFragment.setLayoutManager(mLinearLayoutManager); } else { mStaggeredGridLayoutManager = new StaggeredGridLayoutManager(nColumns, StaggeredGridLayoutManager.VERTICAL); binding.recyclerViewHistoryPostFragment.setLayoutManager(mStaggeredGridLayoutManager); StaggeredGridLayoutManagerItemOffsetDecoration itemDecoration = new StaggeredGridLayoutManagerItemOffsetDecoration(mActivity, R.dimen.staggeredLayoutManagerItemOffset, nColumns); binding.recyclerViewHistoryPostFragment.addItemDecoration(itemDecoration); } if (recyclerViewPosition > 0) { binding.recyclerViewHistoryPostFragment.scrollToPosition(recyclerViewPosition); } if (postFilter == null) { FetchPostFilterAndConcatenatedSubredditNames.fetchPostFilter(mRedditDataRoomDatabase, mExecutor, new Handler(), PostFilterUsage.HISTORY_TYPE, PostFilterUsage.HISTORY_TYPE_USAGE_READ_POSTS, (postFilter) -> { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed() && !isDetached()) { this.postFilter = postFilter; postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean(mActivity.accountName + SharedPreferencesUtils.NSFW_BASE, false); initializeAndBindPostViewModel(); } }); } else { initializeAndBindPostViewModel(); } if (nColumns == 1 && mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SWIPE_ACTION, false)) { swipeActionEnabled = true; touchHelper.attachToRecyclerView(binding.recyclerViewHistoryPostFragment, 1); } binding.recyclerViewHistoryPostFragment.setAdapter(mAdapter); binding.recyclerViewHistoryPostFragment.setCacheManager(mAdapter); binding.recyclerViewHistoryPostFragment.setPlayerInitializer(order -> { VolumeInfo volumeInfo = new VolumeInfo(true, 0f); return new PlaybackInfo(INDEX_UNSET, TIME_UNSET, volumeInfo); }); SharedPreferencesLiveDataKt.stringLiveData(mSharedPreferences, SharedPreferencesUtils.SIMULTANEOUS_AUTOPLAY_LIMIT, "1").observe(getViewLifecycleOwner(), limit -> { if (getPostAdapter() != null) { getPostAdapter().setSimultaneousAutoplayLimit(Integer.parseInt(limit)); } }); return binding.getRoot(); } @Override public void onResume() { super.onResume(); if (mAdapter != null) { mAdapter.setCanStartActivity(true); } if (isInLazyMode) { resumeLazyMode(false); } if (mAdapter != null && binding.recyclerViewHistoryPostFragment != null) { binding.recyclerViewHistoryPostFragment.onWindowVisibilityChanged(View.VISIBLE); } } @Override protected boolean scrollPostsByCount(int count) { if (mLinearLayoutManager != null) { int pos = mLinearLayoutManager.findFirstVisibleItemPosition(); int targetPosition = pos + count; mLinearLayoutManager.scrollToPositionWithOffset(targetPosition, 0); return true; } else { return false; } } private void initializeAndBindPostViewModel() { if (postType == HistoryPostPagingSource.TYPE_READ_POSTS) { mHistoryPostViewModel = new ViewModelProvider(HistoryPostFragment.this, new HistoryPostViewModel.Factory(mExecutor, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, mRedditDataRoomDatabase, mActivity.accessToken, mActivity.accountName, mSharedPreferences, HistoryPostPagingSource.TYPE_READ_POSTS, postFilter)).get(HistoryPostViewModel.class); } else { mHistoryPostViewModel = new ViewModelProvider(HistoryPostFragment.this, new HistoryPostViewModel.Factory(mExecutor, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, mRedditDataRoomDatabase, mActivity.accessToken, mActivity.accountName, mSharedPreferences, HistoryPostPagingSource.TYPE_READ_POSTS, postFilter)).get(HistoryPostViewModel.class); } mHistoryPostViewModel = new ViewModelProvider(HistoryPostFragment.this, new HistoryPostViewModel.Factory(mExecutor, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, mRedditDataRoomDatabase, mActivity.accessToken, mActivity.accountName, mSharedPreferences, HistoryPostPagingSource.TYPE_READ_POSTS, postFilter)).get(HistoryPostViewModel.class); bindPostViewModel(); } private void bindPostViewModel() { mHistoryPostViewModel.getPosts().observe(getViewLifecycleOwner(), posts -> mAdapter.submitData(getViewLifecycleOwner().getLifecycle(), posts)); mAdapter.addLoadStateListener(combinedLoadStates -> { LoadState refreshLoadState = combinedLoadStates.getRefresh(); LoadState appendLoadState = combinedLoadStates.getAppend(); binding.swipeRefreshLayoutHistoryPostFragment.setRefreshing(refreshLoadState instanceof LoadState.Loading); if (refreshLoadState instanceof LoadState.NotLoading) { if (refreshLoadState.getEndOfPaginationReached() && mAdapter.getItemCount() < 1) { noPostFound(); } else { hasPost = true; } } else if (refreshLoadState instanceof LoadState.Error) { binding.fetchPostInfoLinearLayoutHistoryPostFragment.setOnClickListener(view -> refresh()); showErrorView(R.string.load_posts_error); } if (!(refreshLoadState instanceof LoadState.Loading) && appendLoadState instanceof LoadState.NotLoading) { if (appendLoadState.getEndOfPaginationReached() && mAdapter.getItemCount() < 1) { noPostFound(); } } return null; }); binding.recyclerViewHistoryPostFragment.setAdapter(mAdapter.withLoadStateFooter(new Paging3LoadingStateAdapter(mActivity, mCustomThemeWrapper, R.string.load_more_posts_error, view -> mAdapter.retry()))); } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.history_post_fragment, menu); for (int i = 0; i < menu.size(); i++) { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, menu.getItem(i), null); } lazyModeItem = menu.findItem(R.id.action_lazy_mode_history_post_fragment); if (isInLazyMode) { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, lazyModeItem, getString(R.string.action_stop_lazy_mode)); } else { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, lazyModeItem, getString(R.string.action_start_lazy_mode)); } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_lazy_mode_history_post_fragment) { if (isInLazyMode) { stopLazyMode(); } else { startLazyMode(); } return true; } return false; } private void noPostFound() { hasPost = false; if (isInLazyMode) { stopLazyMode(); } binding.fetchPostInfoLinearLayoutHistoryPostFragment.setOnClickListener(null); showErrorView(R.string.no_posts); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(IS_IN_LAZY_MODE_STATE, isInLazyMode); if (mLinearLayoutManager != null) { outState.putInt(RECYCLER_VIEW_POSITION_STATE, mLinearLayoutManager.findFirstVisibleItemPosition()); } else if (mStaggeredGridLayoutManager != null) { int[] into = new int[mStaggeredGridLayoutManager.getSpanCount()]; outState.putInt(RECYCLER_VIEW_POSITION_STATE, mStaggeredGridLayoutManager.findFirstVisibleItemPositions(into)[0]); } outState.putParcelable(POST_FILTER_STATE, postFilter); outState.putLong(POST_FRAGMENT_ID_STATE, postFragmentId); } @Override public void refresh() { binding.fetchPostInfoLinearLayoutHistoryPostFragment.setVisibility(View.GONE); hasPost = false; if (isInLazyMode) { stopLazyMode(); } mAdapter.refresh(); goBackToTop(); } @Override protected void showErrorView(int stringResId) { if (mActivity != null && isAdded()) { binding.swipeRefreshLayoutHistoryPostFragment.setRefreshing(false); binding.fetchPostInfoLinearLayoutHistoryPostFragment.setVisibility(View.VISIBLE); binding.fetchPostInfoTextViewHistoryPostFragment.setText(stringResId); } } @NonNull @Override protected SwipeRefreshLayout getSwipeRefreshLayout() { return binding.swipeRefreshLayoutHistoryPostFragment; } @NonNull @Override protected RecyclerView getPostRecyclerView() { return binding.recyclerViewHistoryPostFragment; } @Nullable @Override protected PostRecyclerViewAdapter getPostAdapter() { return mAdapter; } @Override public void changePostLayout(int postLayout, boolean temporary) { this.postLayout = postLayout; if (!temporary) { switch (postType) { case HistoryPostPagingSource.TYPE_READ_POSTS: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.HISTORY_POST_LAYOUT_READ_POST, postLayout).apply(); } } int previousPosition = -1; if (mLinearLayoutManager != null) { previousPosition = mLinearLayoutManager.findFirstVisibleItemPosition(); } else if (mStaggeredGridLayoutManager != null) { int[] into = new int[mStaggeredGridLayoutManager.getSpanCount()]; previousPosition = mStaggeredGridLayoutManager.findFirstVisibleItemPositions(into)[0]; } int nColumns = getNColumns(getResources()); if (nColumns == 1) { mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); if (binding.recyclerViewHistoryPostFragment.getItemDecorationCount() > 0) { binding.recyclerViewHistoryPostFragment.removeItemDecorationAt(0); } binding.recyclerViewHistoryPostFragment.setLayoutManager(mLinearLayoutManager); mStaggeredGridLayoutManager = null; } else { mStaggeredGridLayoutManager = new StaggeredGridLayoutManager(nColumns, StaggeredGridLayoutManager.VERTICAL); if (binding.recyclerViewHistoryPostFragment.getItemDecorationCount() > 0) { binding.recyclerViewHistoryPostFragment.removeItemDecorationAt(0); } binding.recyclerViewHistoryPostFragment.setLayoutManager(mStaggeredGridLayoutManager); StaggeredGridLayoutManagerItemOffsetDecoration itemDecoration = new StaggeredGridLayoutManagerItemOffsetDecoration(mActivity, R.dimen.staggeredLayoutManagerItemOffset, nColumns); binding.recyclerViewHistoryPostFragment.addItemDecoration(itemDecoration); mLinearLayoutManager = null; } if (previousPosition > 0) { binding.recyclerViewHistoryPostFragment.scrollToPosition(previousPosition); } if (mAdapter != null) { mAdapter.setPostLayout(postLayout); refreshAdapter(); } } @Override public void applyTheme() { binding.swipeRefreshLayoutHistoryPostFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutHistoryPostFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); binding.fetchPostInfoTextViewHistoryPostFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchPostInfoTextViewHistoryPostFragment.setTypeface(mActivity.typeface); } } @Override protected void refreshAdapter() { int previousPosition = -1; if (mLinearLayoutManager != null) { previousPosition = mLinearLayoutManager.findFirstVisibleItemPosition(); } else if (mStaggeredGridLayoutManager != null) { int[] into = new int[mStaggeredGridLayoutManager.getSpanCount()]; previousPosition = mStaggeredGridLayoutManager.findFirstVisibleItemPositions(into)[0]; } RecyclerView.LayoutManager layoutManager = binding.recyclerViewHistoryPostFragment.getLayoutManager(); binding.recyclerViewHistoryPostFragment.setAdapter(null); binding.recyclerViewHistoryPostFragment.setLayoutManager(null); binding.recyclerViewHistoryPostFragment.setAdapter(mAdapter); binding.recyclerViewHistoryPostFragment.setLayoutManager(layoutManager); if (previousPosition > 0) { binding.recyclerViewHistoryPostFragment.scrollToPosition(previousPosition); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); if (isInLazyMode) { lazyModeRunnable.resetOldPosition(); } } else if (mStaggeredGridLayoutManager != null) { mStaggeredGridLayoutManager.scrollToPositionWithOffset(0, 0); if (isInLazyMode) { lazyModeRunnable.resetOldPosition(); } } } @Override public void onPause() { super.onPause(); if (isInLazyMode) { pauseLazyMode(false); } if (mAdapter != null) { binding.recyclerViewHistoryPostFragment.onWindowVisibilityChanged(View.GONE); } } @Override public void onDestroy() { binding.recyclerViewHistoryPostFragment.addOnWindowFocusChangedListener(null); super.onDestroy(); } private void onWindowFocusChanged(boolean hasWindowsFocus) { if (mAdapter != null) { mAdapter.setCanPlayVideo(hasWindowsFocus); } } @Subscribe public void onChangeDefaultPostLayoutEvent(ChangeDefaultPostLayoutEvent changeDefaultPostLayoutEvent) { Bundle bundle = getArguments(); if (bundle != null) { switch (postType) { case HistoryPostPagingSource.TYPE_READ_POSTS: if (mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.HISTORY_POST_LAYOUT_READ_POST)) { changePostLayout(changeDefaultPostLayoutEvent.defaultPostLayout, true); } break; } } } @Subscribe public void onChangeDefaultPostLayoutUnfoldedEvent(ChangeDefaultPostLayoutUnfoldedEvent event) { boolean foldEnabled = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_FOLD_SUPPORT, false); boolean isTablet = getResources().getBoolean(R.bool.isTablet); if (foldEnabled && isTablet) { Bundle bundle = getArguments(); if (bundle != null) { switch (postType) { case HistoryPostPagingSource.TYPE_READ_POSTS: if (mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.HISTORY_POST_LAYOUT_READ_POST)) { changePostLayout(event.defaultPostLayoutUnfolded, true); } break; } } } } @Subscribe public void onNeedForPostListFromPostRecyclerViewAdapterEvent(NeedForPostListFromPostFragmentEvent event) { if (postFragmentId == event.postFragmentTimeId && mAdapter != null) { EventBus.getDefault().post(new ProvidePostListToViewPostDetailActivityEvent(postFragmentId, new ArrayList<>(mAdapter.snapshot()), HistoryPostPagingSource.TYPE_READ_POSTS, null, null, null, null, null, null, null, postFilter, null, null)); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/InboxFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.paging.PagedList; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.MessageRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentInboxBinding; import ml.docilealligator.infinityforreddit.events.RepliedToPrivateMessageEvent; import ml.docilealligator.infinityforreddit.message.FetchMessage; import ml.docilealligator.infinityforreddit.message.Message; import ml.docilealligator.infinityforreddit.message.MessageViewModel; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class InboxFragment extends Fragment implements FragmentCommunicator { public static final String EXTRA_MESSAGE_WHERE = "EMT"; MessageViewModel mMessageViewModel; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private String mWhere; private MessageRecyclerViewAdapter mAdapter; private RequestManager mGlide; private LinearLayoutManagerBugFixed mLinearLayoutManager; private BaseActivity mActivity; private FragmentInboxBinding binding; public InboxFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentInboxBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); EventBus.getDefault().register(this); applyTheme(); Bundle arguments = getArguments(); if (arguments == null) { return binding.getRoot(); } mGlide = Glide.with(this); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewInboxFragment.setPadding(0, 0, 0, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); //binding.recyclerViewInboxFragment.setPadding(0, 0, 0, mActivity.getNavBarHeight()); } mWhere = arguments.getString(EXTRA_MESSAGE_WHERE, FetchMessage.WHERE_INBOX); mAdapter = new MessageRecyclerViewAdapter(mActivity, mOauthRetrofit, mCustomThemeWrapper, mActivity.accessToken, mActivity.accountName, mWhere, () -> mMessageViewModel.retryLoadingMore()); mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewInboxFragment.setLayoutManager(mLinearLayoutManager); binding.recyclerViewInboxFragment.setAdapter(mAdapter); DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mActivity, mLinearLayoutManager.getOrientation()); binding.recyclerViewInboxFragment.addItemDecoration(dividerItemDecoration); if (mActivity instanceof RecyclerViewContentScrollingInterface) { binding.recyclerViewInboxFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); } else if (dy < 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); } } }); } MessageViewModel.Factory factory = new MessageViewModel.Factory(mExecutor, mActivity.mHandler, mOauthRetrofit, getResources().getConfiguration().locale, mActivity.accessToken, mWhere); mMessageViewModel = new ViewModelProvider(this, factory).get(MessageViewModel.class); mMessageViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> mAdapter.submitList(messages)); mMessageViewModel.hasMessage().observe(getViewLifecycleOwner(), hasMessage -> { binding.swipeRefreshLayoutInboxFragment.setRefreshing(false); if (hasMessage) { binding.fetchMessagesInfoLinearLayoutInboxFragment.setVisibility(View.GONE); } else { binding.fetchMessagesInfoLinearLayoutInboxFragment.setOnClickListener(null); showErrorView(R.string.no_messages); } }); mMessageViewModel.getInitialLoadingState().observe(getViewLifecycleOwner(), networkState -> { if (networkState.getStatus().equals(NetworkState.Status.SUCCESS)) { binding.swipeRefreshLayoutInboxFragment.setRefreshing(false); } else if (networkState.getStatus().equals(NetworkState.Status.FAILED)) { binding.swipeRefreshLayoutInboxFragment.setRefreshing(false); binding.fetchMessagesInfoLinearLayoutInboxFragment.setOnClickListener(view -> { binding.fetchMessagesInfoLinearLayoutInboxFragment.setVisibility(View.GONE); mMessageViewModel.refresh(); mAdapter.setNetworkState(null); }); showErrorView(R.string.load_messages_failed); } else { binding.swipeRefreshLayoutInboxFragment.setRefreshing(true); } }); mMessageViewModel.getPaginationNetworkState().observe(getViewLifecycleOwner(), networkState -> { mAdapter.setNetworkState(networkState); }); binding.swipeRefreshLayoutInboxFragment.setOnRefreshListener(this::onRefresh); return binding.getRoot(); } private void showErrorView(int stringResId) { binding.swipeRefreshLayoutInboxFragment.setRefreshing(false); binding.fetchMessagesInfoLinearLayoutInboxFragment.setVisibility(View.VISIBLE); binding.fetchMessagesInfoTextViewInboxFragment.setText(stringResId); } @Override public void applyTheme() { binding.swipeRefreshLayoutInboxFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutInboxFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); binding.fetchMessagesInfoTextViewInboxFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchMessagesInfoTextViewInboxFragment.setTypeface(mActivity.typeface); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } public void markAllMessagesRead() { if (mAdapter != null) { mAdapter.setMarkAllMessagesAsRead(true); int previousPosition = -1; if (mLinearLayoutManager != null) { previousPosition = mLinearLayoutManager.findFirstVisibleItemPosition(); } RecyclerView.LayoutManager layoutManager = binding.recyclerViewInboxFragment.getLayoutManager(); binding.recyclerViewInboxFragment.setAdapter(null); binding.recyclerViewInboxFragment.setLayoutManager(null); binding.recyclerViewInboxFragment.setAdapter(mAdapter); binding.recyclerViewInboxFragment.setLayoutManager(layoutManager); if (previousPosition > 0) { binding.recyclerViewInboxFragment.scrollToPosition(previousPosition); } } } private void onRefresh() { mMessageViewModel.refresh(); mAdapter.setNetworkState(null); } public Message getMessageByIndex(int index) { if (mMessageViewModel == null || index < 0) { return null; } PagedList messages = mMessageViewModel.getMessages().getValue(); if (messages == null) { return null; } if (index >= messages.size()) { return null; } return messages.get(index); } @Override public void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } @Subscribe public void onRepliedToPrivateMessageEvent(RepliedToPrivateMessageEvent repliedToPrivateMessageEvent) { if (mAdapter != null && mWhere.equals(FetchMessage.WHERE_MESSAGES)) { mAdapter.updateMessageReply(repliedToPrivateMessageEvent.newReply, repliedToPrivateMessageEvent.messagePosition); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/MorePostsInfoFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import javax.inject.Inject; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.post.LoadingMorePostsStatus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.FragmentMorePostsInfoBinding; public class MorePostsInfoFragment extends Fragment { public static final String EXTRA_STATUS = "ES"; @Inject CustomThemeWrapper mCustomThemeWrapper; private FragmentMorePostsInfoBinding binding; private ViewPostDetailActivity mActivity; @LoadingMorePostsStatus int status; public MorePostsInfoFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); binding = FragmentMorePostsInfoBinding.inflate(inflater, container, false); applyTheme(); setStatus(getArguments().getInt(EXTRA_STATUS, LoadingMorePostsStatus.LOADING)); binding.getRoot().setOnClickListener(view -> { if (status == LoadingMorePostsStatus.FAILED) { mActivity.fetchMorePosts(true); } }); return binding.getRoot(); } public void setStatus(@LoadingMorePostsStatus int status) { this.status = status; switch (status) { case LoadingMorePostsStatus.NOT_LOADING: binding.progressBarViewMorePostsInfoFragment.setVisibility(View.GONE); break; case LoadingMorePostsStatus.LOADING: binding.infoTextViewMorePostsInfoFragment.setText(R.string.loading); binding.progressBarViewMorePostsInfoFragment.setVisibility(View.VISIBLE); break; case LoadingMorePostsStatus.FAILED: binding.infoTextViewMorePostsInfoFragment.setText(R.string.load_more_posts_failed); binding.progressBarViewMorePostsInfoFragment.setVisibility(View.GONE); break; case LoadingMorePostsStatus.NO_MORE_POSTS: binding.infoTextViewMorePostsInfoFragment.setText(R.string.no_more_posts); binding.progressBarViewMorePostsInfoFragment.setVisibility(View.GONE); } } private void applyTheme() { binding.infoTextViewMorePostsInfoFragment.setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (ViewPostDetailActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/MultiRedditListingFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import me.zhanghai.android.fastscroll.FastScrollerBuilder; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.SubscribedThingListingActivity; import ml.docilealligator.infinityforreddit.activities.ViewMultiRedditDetailActivity; import ml.docilealligator.infinityforreddit.adapters.MultiRedditListingRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.bottomsheetfragments.MultiRedditOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentMultiRedditListingBinding; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.multireddit.MultiRedditViewModel; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class MultiRedditListingFragment extends Fragment implements FragmentCommunicator { public static final String EXTRA_IS_MULTIREDDIT_SELECTION = "EIMS"; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; public MultiRedditViewModel mMultiRedditViewModel; private BaseActivity mActivity; private RequestManager mGlide; private LinearLayoutManagerBugFixed mLinearLayoutManager; private FragmentMultiRedditListingBinding binding; public MultiRedditListingFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentMultiRedditListingBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); applyTheme(); if ((mActivity != null && mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge())) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewMultiRedditListingFragment.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); //binding.recyclerViewMultiRedditListingFragment.setPadding(0, 0, 0, mActivity.getNavBarHeight()); }/* else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { Resources resources = getResources(); int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { binding.recyclerViewMultiRedditListingFragment.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); } }*/ boolean isGettingMultiredditInfo = getArguments().getBoolean(EXTRA_IS_MULTIREDDIT_SELECTION, false); if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.swipeRefreshLayoutMultiRedditListingFragment.setEnabled(false); } mGlide = Glide.with(this); mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewMultiRedditListingFragment.setLayoutManager(mLinearLayoutManager); MultiRedditListingRecyclerViewAdapter adapter = new MultiRedditListingRecyclerViewAdapter(mActivity, mExecutor, mOauthRetrofit, mRedditDataRoomDatabase, mCustomThemeWrapper, mActivity.accessToken, mActivity.accountName, new MultiRedditListingRecyclerViewAdapter.OnItemClickListener() { @Override public void onClick(MultiReddit multiReddit) { if (isGettingMultiredditInfo) { Intent returnIntent = new Intent(); returnIntent.putExtra(SelectThingReturnKey.RETRUN_EXTRA_MULTIREDDIT, multiReddit); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.MULTIREDDIT); mActivity.setResult(Activity.RESULT_OK, returnIntent); mActivity.finish(); } else { Intent intent = new Intent(mActivity, ViewMultiRedditDetailActivity.class); intent.putExtra(ViewMultiRedditDetailActivity.EXTRA_MULTIREDDIT_DATA, multiReddit); mActivity.startActivity(intent); } } @Override public void onLongClick(MultiReddit multiReddit) { if (!isGettingMultiredditInfo) { showOptionsBottomSheetFragment(multiReddit); } } }); binding.recyclerViewMultiRedditListingFragment.setAdapter(adapter); if (mActivity instanceof SubscribedThingListingActivity) { binding.recyclerViewMultiRedditListingFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (dy > 0) { ((SubscribedThingListingActivity) mActivity).hideFabInMultiredditTab(); } else { ((SubscribedThingListingActivity) mActivity).showFabInMultiredditTab(); } } }); } new FastScrollerBuilder(binding.recyclerViewMultiRedditListingFragment).useMd2Style().build(); mMultiRedditViewModel = new ViewModelProvider(this, new MultiRedditViewModel.Factory(mRedditDataRoomDatabase, mActivity.accountName)) .get(MultiRedditViewModel.class); mMultiRedditViewModel.getAllMultiReddits().observe(getViewLifecycleOwner(), multiReddits -> { if (multiReddits == null || multiReddits.size() == 0) { binding.recyclerViewMultiRedditListingFragment.setVisibility(View.GONE); binding.fetchMultiRedditListingInfoLinearLayoutMultiRedditListingFragment.setVisibility(View.VISIBLE); } else { binding.fetchMultiRedditListingInfoLinearLayoutMultiRedditListingFragment.setVisibility(View.GONE); binding.recyclerViewMultiRedditListingFragment.setVisibility(View.VISIBLE); mGlide.clear(binding.fetchMultiRedditListingInfoImageViewMultiRedditListingFragment); } adapter.setMultiReddits(multiReddits); }); mMultiRedditViewModel.getAllFavoriteMultiReddits().observe(getViewLifecycleOwner(), favoriteMultiReddits -> { if (favoriteMultiReddits != null && favoriteMultiReddits.size() > 0) { binding.fetchMultiRedditListingInfoLinearLayoutMultiRedditListingFragment.setVisibility(View.GONE); binding.recyclerViewMultiRedditListingFragment.setVisibility(View.VISIBLE); mGlide.clear(binding.fetchMultiRedditListingInfoImageViewMultiRedditListingFragment); } adapter.setFavoriteMultiReddits(favoriteMultiReddits); }); return binding.getRoot(); } private void showOptionsBottomSheetFragment(MultiReddit multiReddit) { MultiRedditOptionsBottomSheetFragment fragment = new MultiRedditOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(MultiRedditOptionsBottomSheetFragment.EXTRA_MULTI_REDDIT, multiReddit); fragment.setArguments(bundle); fragment.show(mActivity.getSupportFragmentManager(), fragment.getTag()); } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } public void changeSearchQuery(String searchQuery) { mMultiRedditViewModel.setSearchQuery(searchQuery); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } @Override public void applyTheme() { if (mActivity instanceof SubscribedThingListingActivity) { binding.swipeRefreshLayoutMultiRedditListingFragment.setOnRefreshListener(() -> ((SubscribedThingListingActivity) mActivity).loadSubscriptions(true)); binding.swipeRefreshLayoutMultiRedditListingFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutMultiRedditListingFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); } else { binding.swipeRefreshLayoutMultiRedditListingFragment.setEnabled(false); } binding.fetchMultiRedditListingInfoTextViewMultiRedditListingFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchMultiRedditListingInfoTextViewMultiRedditListingFragment.setTypeface(mActivity.typeface); } } @Override public void stopRefreshProgressbar() { binding.swipeRefreshLayoutMultiRedditListingFragment.setRefreshing(false); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import androidx.paging.CombinedLoadStates; import androidx.paging.LoadState; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.transition.AutoTransition; import androidx.transition.TransitionManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Locale; import java.util.Objects; import java.util.Random; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import kotlin.Unit; import kotlin.jvm.functions.Function1; import ml.docilealligator.infinityforreddit.FetchPostFilterAndConcatenatedSubredditNames; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.PostModerationActionHandler; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.AccountPostsActivity; import ml.docilealligator.infinityforreddit.activities.AccountSavedThingActivity; import ml.docilealligator.infinityforreddit.activities.ActivityToolbarInterface; import ml.docilealligator.infinityforreddit.activities.CustomizePostFilterActivity; import ml.docilealligator.infinityforreddit.activities.FilteredPostsActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.adapters.Paging3LoadingStateAdapter; import ml.docilealligator.infinityforreddit.adapters.PostRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FABMoreOptionsBottomSheetFragment; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentPostBinding; import ml.docilealligator.infinityforreddit.events.ChangeDefaultPostLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangeDefaultPostLayoutUnfoldedEvent; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.events.ChangeSavePostFeedScrolledPositionEvent; import ml.docilealligator.infinityforreddit.events.NeedForPostListFromPostFragmentEvent; import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostDetailFragment; import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostList; import ml.docilealligator.infinityforreddit.events.ProvidePostListToViewPostDetailActivityEvent; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.PostPagingSource; import ml.docilealligator.infinityforreddit.post.PostViewModel; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage; import ml.docilealligator.infinityforreddit.readpost.ReadPostsList; import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesLiveDataKt; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; import retrofit2.Retrofit; /** * A simple {@link PostFragmentBase} subclass. */ public class PostFragment extends PostFragmentBase implements FragmentCommunicator, PostModerationActionHandler { public static final String EXTRA_NAME = "EN"; public static final String EXTRA_USER_NAME = "EUN"; public static final String EXTRA_USER_WHERE = "EUW"; public static final String EXTRA_QUERY = "EQ"; public static final String EXTRA_TRENDING_SOURCE = "ETS"; public static final String EXTRA_POST_TYPE = "EPT"; public static final String EXTRA_FILTER = "EF"; public static final String EXTRA_DISABLE_READ_POSTS = "EDRP"; private static final String IS_IN_LAZY_MODE_STATE = "IILMS"; private static final String RECYCLER_VIEW_POSITION_STATE = "RVPS"; private static final String POST_FILTER_STATE = "PFS"; private static final String CONCATENATED_SUBREDDIT_NAMES_STATE = "CSNS"; private static final String POST_FRAGMENT_ID_STATE = "PFIS"; PostViewModel mPostViewModel; @Inject @Named("redgifs") Retrofit mRedgifsRetrofit; @Inject Provider mStreamableApiProvider; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject @Named("post_feed_scrolled_position_cache") SharedPreferences mPostFeedScrolledPositionSharedPreferences; @Inject ExoCreator mExoCreator; private int postType; private boolean savePostFeedScrolledPosition; private PostRecyclerViewAdapter mAdapter; private String subredditName; private String username; private String query; private String trendingSource; private String where; private String multiRedditPath; private String concatenatedSubredditNames; private int maxPosition = -1; private SortType sortType; private PostFilter postFilter; private ReadPostsListInterface readPostsList; private FragmentPostBinding binding; public PostFragment() { // Required empty public constructor } @Override public void onResume() { super.onResume(); if (mAdapter != null) { mAdapter.setCanStartActivity(true); } if (isInLazyMode) { resumeLazyMode(false); } if (mAdapter != null && binding.recyclerViewPostFragment != null) { binding.recyclerViewPostFragment.onWindowVisibilityChanged(View.VISIBLE); } } @Override protected boolean scrollPostsByCount(int count) { if (mLinearLayoutManager != null) { int pos = mLinearLayoutManager.findFirstVisibleItemPosition(); int targetPosition = pos + count; mLinearLayoutManager.scrollToPositionWithOffset(targetPosition, 0); return true; } else { return false; } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentPostBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); super.onCreateView(inflater, container, savedInstanceState); setHasOptionsMenu(true); applyTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); getPostRecyclerView().setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } binding.recyclerViewPostFragment.addOnWindowFocusChangedListener(this::onWindowFocusChanged); Resources resources = getResources(); binding.swipeRefreshLayoutPostFragment.setEnabled(mSharedPreferences.getBoolean(SharedPreferencesUtils.PULL_TO_REFRESH, true)); binding.swipeRefreshLayoutPostFragment.setOnRefreshListener(this::refresh); int recyclerViewPosition; if (savedInstanceState != null) { recyclerViewPosition = savedInstanceState.getInt(RECYCLER_VIEW_POSITION_STATE); isInLazyMode = savedInstanceState.getBoolean(IS_IN_LAZY_MODE_STATE); postFilter = savedInstanceState.getParcelable(POST_FILTER_STATE); concatenatedSubredditNames = savedInstanceState.getString(CONCATENATED_SUBREDDIT_NAMES_STATE); postFragmentId = savedInstanceState.getLong(POST_FRAGMENT_ID_STATE); } else { recyclerViewPosition = 0; postFilter = getArguments().getParcelable(EXTRA_FILTER); postFragmentId = System.currentTimeMillis() + new Random().nextInt(1000); } readPostsList = new ReadPostsList(mRedditDataRoomDatabase.readPostDao(), mActivity.accountName, getArguments().getBoolean(EXTRA_DISABLE_READ_POSTS, false)); if (mActivity instanceof RecyclerViewContentScrollingInterface) { binding.recyclerViewPostFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); } else if (dy < 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); } } }); } postType = getArguments().getInt(EXTRA_POST_TYPE); int defaultPostLayout; boolean foldEnabled = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_FOLD_SUPPORT, false); boolean isTablet = getResources().getBoolean(R.bool.isTablet); if (foldEnabled && isTablet) { defaultPostLayout = Integer.parseInt(mSharedPreferences.getString( SharedPreferencesUtils.DEFAULT_POST_LAYOUT_UNFOLDED_KEY, "0")); } else { defaultPostLayout = Integer.parseInt(mSharedPreferences.getString( SharedPreferencesUtils.DEFAULT_POST_LAYOUT_KEY, "0")); } savePostFeedScrolledPosition = mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_FRONT_PAGE_SCROLLED_POSITION, false); Locale locale = resources.getConfiguration().locale; int usage; String nameOfUsage; if (postType == PostPagingSource.TYPE_SEARCH) { subredditName = getArguments().getString(EXTRA_NAME); query = getArguments().getString(EXTRA_QUERY); trendingSource = getArguments().getString(EXTRA_TRENDING_SOURCE); if (savedInstanceState == null) { postFragmentId += query.hashCode(); } usage = PostFilterUsage.SEARCH_TYPE; nameOfUsage = PostFilterUsage.NO_USAGE; String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_SEARCH_POST, SortType.Type.RELEVANCE.name()); String sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_SEARCH_POST, SortType.Time.ALL.name()); sortType = new SortType(SortType.Type.valueOf(sort), SortType.Time.valueOf(sortTime)); postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.POST_LAYOUT_SEARCH_POST, defaultPostLayout); mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, true, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_TRENDING_SOURCE, trendingSource); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, filter); startActivity(intent); } @Override public void flairChipClicked(String flair) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_TRENDING_SOURCE, trendingSource); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent); } @Override public void nsfwChipClicked() { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_TRENDING_SOURCE, trendingSource); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); startActivity(intent); } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewPostFragment, new AutoTransition()); } }); } else if (postType == PostPagingSource.TYPE_SUBREDDIT) { subredditName = getArguments().getString(EXTRA_NAME); if (savedInstanceState == null) { postFragmentId += subredditName.hashCode(); } usage = PostFilterUsage.SUBREDDIT_TYPE; nameOfUsage = subredditName; String sort; String sortTime = null; sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_SUBREDDIT_POST_BASE + subredditName, mSharedPreferences.getString(SharedPreferencesUtils.SUBREDDIT_DEFAULT_SORT_TYPE, SortType.Type.HOT.name())); if (sort.equals(SortType.Type.CONTROVERSIAL.name()) || sort.equals(SortType.Type.TOP.name())) { sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_SUBREDDIT_POST_BASE + subredditName, mSharedPreferences.getString(SharedPreferencesUtils.SUBREDDIT_DEFAULT_SORT_TIME, SortType.Time.ALL.name())); } boolean displaySubredditName = subredditName != null && (subredditName.equals("popular") || subredditName.equals("all")); postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.POST_LAYOUT_SUBREDDIT_POST_BASE + subredditName, defaultPostLayout); if (sortTime != null) { sortType = new SortType(SortType.Type.valueOf(sort), SortType.Time.valueOf(sortTime)); } else { sortType = new SortType(SortType.Type.valueOf(sort)); } mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, displaySubredditName, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, filter); startActivity(intent); } @Override public void flairChipClicked(String flair) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent); } @Override public void nsfwChipClicked() { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); startActivity(intent); } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewPostFragment, new AutoTransition()); } }); } else if (postType == PostPagingSource.TYPE_MULTI_REDDIT) { multiRedditPath = getArguments().getString(EXTRA_NAME); query = getArguments().getString(EXTRA_QUERY); if (savedInstanceState == null) { postFragmentId += multiRedditPath.hashCode() + (query == null ? 0 : query.hashCode()); } usage = PostFilterUsage.MULTIREDDIT_TYPE; nameOfUsage = multiRedditPath; String sort; String sortTime = null; sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_MULTI_REDDIT_POST_BASE + multiRedditPath, SortType.Type.HOT.name()); if (sort.equals(SortType.Type.CONTROVERSIAL.name()) || sort.equals(SortType.Type.TOP.name())) { sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_MULTI_REDDIT_POST_BASE + multiRedditPath, SortType.Time.ALL.name()); } postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.POST_LAYOUT_MULTI_REDDIT_POST_BASE + multiRedditPath, defaultPostLayout); if (sortTime != null) { sortType = new SortType(SortType.Type.valueOf(sort), SortType.Time.valueOf(sortTime)); } else { sortType = new SortType(SortType.Type.valueOf(sort)); } mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, true, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, multiRedditPath); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, filter); startActivity(intent); } @Override public void flairChipClicked(String flair) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, multiRedditPath); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent); } @Override public void nsfwChipClicked() { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, multiRedditPath); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); startActivity(intent); } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewPostFragment, new AutoTransition()); } }); } else if (postType == PostPagingSource.TYPE_USER) { username = getArguments().getString(EXTRA_USER_NAME); where = getArguments().getString(EXTRA_USER_WHERE); if (savedInstanceState == null) { postFragmentId += username.hashCode(); } usage = PostFilterUsage.USER_TYPE; nameOfUsage = username; String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_USER_POST_BASE + username, mSharedPreferences.getString(SharedPreferencesUtils.USER_DEFAULT_SORT_TYPE, SortType.Type.NEW.name())); if (sort.equals(SortType.Type.CONTROVERSIAL.name()) || sort.equals(SortType.Type.TOP.name())) { String sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_USER_POST_BASE + username, mSharedPreferences.getString(SharedPreferencesUtils.USER_DEFAULT_SORT_TIME, SortType.Time.ALL.name())); sortType = new SortType(SortType.Type.valueOf(sort), SortType.Time.valueOf(sortTime)); } else { sortType = new SortType(SortType.Type.valueOf(sort)); } postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE + username, defaultPostLayout); mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, true, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, username); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_USER_WHERE, where); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, filter); startActivity(intent); } @Override public void flairChipClicked(String flair) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, username); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_USER_WHERE, where); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent); } @Override public void nsfwChipClicked() { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, username); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_USER_WHERE, where); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); startActivity(intent); } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewPostFragment, new AutoTransition()); } }); } else if (postType == PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE) { usage = PostFilterUsage.HOME_TYPE; nameOfUsage = PostFilterUsage.NO_USAGE; String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_SUBREDDIT_POST_BASE + Account.ANONYMOUS_ACCOUNT, SortType.Type.HOT.name()); if (sort.equals(SortType.Type.CONTROVERSIAL.name()) || sort.equals(SortType.Type.TOP.name())) { String sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_SUBREDDIT_POST_BASE + Account.ANONYMOUS_ACCOUNT, SortType.Time.ALL.name()); sortType = new SortType(SortType.Type.valueOf(sort), SortType.Time.valueOf(sortTime)); } else { sortType = new SortType(SortType.Type.valueOf(sort)); } postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.POST_LAYOUT_FRONT_PAGE_POST, defaultPostLayout); mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, true, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, filter); startActivity(intent); } @Override public void flairChipClicked(String flair) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent); } @Override public void nsfwChipClicked() { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); startActivity(intent); } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewPostFragment, new AutoTransition()); } }); } else if (postType == PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT) { multiRedditPath = getArguments().getString(EXTRA_NAME); if (savedInstanceState == null) { postFragmentId += multiRedditPath.hashCode(); } usage = PostFilterUsage.MULTIREDDIT_TYPE; nameOfUsage = multiRedditPath; String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_MULTI_REDDIT_POST_BASE + multiRedditPath, SortType.Type.HOT.name()); if (sort.equals(SortType.Type.CONTROVERSIAL.name()) || sort.equals(SortType.Type.TOP.name())) { String sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_MULTI_REDDIT_POST_BASE + multiRedditPath, SortType.Time.ALL.name()); sortType = new SortType(SortType.Type.valueOf(sort), SortType.Time.valueOf(sortTime)); } else { sortType = new SortType(SortType.Type.valueOf(sort)); } postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.POST_LAYOUT_MULTI_REDDIT_POST_BASE + multiRedditPath, defaultPostLayout); mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, true, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, multiRedditPath); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, filter); startActivity(intent); } @Override public void flairChipClicked(String flair) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, multiRedditPath); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent); } @Override public void nsfwChipClicked() { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, multiRedditPath); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); startActivity(intent); } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewPostFragment, new AutoTransition()); } }); } else { usage = PostFilterUsage.HOME_TYPE; nameOfUsage = PostFilterUsage.NO_USAGE; String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_BEST_POST, SortType.Type.BEST.name()); if (sort.equals(SortType.Type.CONTROVERSIAL.name()) || sort.equals(SortType.Type.TOP.name())) { String sortTime = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TIME_BEST_POST, SortType.Time.ALL.name()); sortType = new SortType(SortType.Type.valueOf(sort), SortType.Time.valueOf(sortTime)); } else { sortType = new SortType(SortType.Type.valueOf(sort)); } postLayout = mPostLayoutSharedPreferences.getInt(SharedPreferencesUtils.POST_LAYOUT_FRONT_PAGE_POST, defaultPostLayout); mAdapter = new PostRecyclerViewAdapter(mActivity, this, mExecutor, mOauthRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mCustomThemeWrapper, locale, mActivity.accessToken, mActivity.accountName, postType, postLayout, true, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences, mExoCreator, new PostRecyclerViewAdapter.Callback() { @Override public void typeChipClicked(int filter) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, filter); startActivity(intent); } @Override public void flairChipClicked(String flair) { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_CONTAIN_FLAIR, flair); startActivity(intent); } @Override public void nsfwChipClicked() { Intent intent = new Intent(mActivity, FilteredPostsActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE_FILTER, Post.NSFW_TYPE); startActivity(intent); } @Override public void currentlyBindItem(int position) { if (maxPosition < position) { maxPosition = position; } } @Override public void delayTransition() { TransitionManager.beginDelayedTransition(binding.recyclerViewPostFragment, new AutoTransition()); } }); } int nColumns = getNColumns(resources); if (nColumns == 1) { mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewPostFragment.setLayoutManager(mLinearLayoutManager); } else { mStaggeredGridLayoutManager = new StaggeredGridLayoutManager(nColumns, StaggeredGridLayoutManager.VERTICAL); binding.recyclerViewPostFragment.setLayoutManager(mStaggeredGridLayoutManager); StaggeredGridLayoutManagerItemOffsetDecoration itemDecoration = new StaggeredGridLayoutManagerItemOffsetDecoration(mActivity, R.dimen.staggeredLayoutManagerItemOffset, nColumns); binding.recyclerViewPostFragment.addItemDecoration(itemDecoration); } if (recyclerViewPosition > 0) { mAdapter.addLoadStateListener(new Function1<>() { @Override public Unit invoke(CombinedLoadStates combinedLoadStates) { if (combinedLoadStates.getRefresh() instanceof LoadState.NotLoading && mAdapter.getItemCount() > 0) { binding.recyclerViewPostFragment.scrollToPosition(recyclerViewPosition); mAdapter.removeLoadStateListener(this); } return Unit.INSTANCE; } }); } if (mActivity instanceof ActivityToolbarInterface) { ((ActivityToolbarInterface) mActivity).displaySortType(); } where = getArguments().getString(EXTRA_USER_WHERE); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if(Objects.equals(where, PostPagingSource.USER_WHERE_UPVOTED)){ usage = PostFilterUsage.UPVOTED_TYPE; nameOfUsage = PostFilterUsage.NO_USAGE; } else if(Objects.equals(where, PostPagingSource.USER_WHERE_DOWNVOTED)){ usage = PostFilterUsage.DOWNVOTED_TYPE; nameOfUsage = PostFilterUsage.NO_USAGE; } else if(Objects.equals(where, PostPagingSource.USER_WHERE_HIDDEN)){ usage = PostFilterUsage.HIDDEN_TYPE; nameOfUsage = PostFilterUsage.NO_USAGE; } else if(Objects.equals(where, PostPagingSource.USER_WHERE_SAVED)){ usage = PostFilterUsage.SAVED_TYPE; nameOfUsage = PostFilterUsage.NO_USAGE; } } if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (postFilter == null) { FetchPostFilterAndConcatenatedSubredditNames.fetchPostFilter(mRedditDataRoomDatabase, mExecutor, new Handler(), usage, nameOfUsage, (postFilter) -> { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed() && !isDetached()) { this.postFilter = postFilter; this.postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean(mActivity.accountName + SharedPreferencesUtils.NSFW_BASE, false); initializeAndBindPostViewModel(); } }); } else { initializeAndBindPostViewModel(); } } else { if (postFilter == null) { if (postType == PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE) { if (concatenatedSubredditNames == null) { FetchPostFilterAndConcatenatedSubredditNames.fetchPostFilterAndConcatenatedSubredditNames(mRedditDataRoomDatabase, mExecutor, new Handler(), usage, nameOfUsage, (postFilter, concatenatedSubredditNames) -> { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed() && !isDetached()) { this.postFilter = postFilter; this.postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean(SharedPreferencesUtils.NSFW_BASE, false); this.concatenatedSubredditNames = concatenatedSubredditNames; if (concatenatedSubredditNames == null) { showErrorView(R.string.anonymous_front_page_no_subscriptions); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } }); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } else if (postType == PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT) { if (concatenatedSubredditNames == null) { FetchPostFilterAndConcatenatedSubredditNames.fetchPostFilterAndConcatenatedSubredditNames(mRedditDataRoomDatabase, mExecutor, new Handler(), multiRedditPath, usage, nameOfUsage, (postFilter, concatenatedSubredditNames) -> { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed() && !isDetached()) { this.postFilter = postFilter; this.postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean(SharedPreferencesUtils.NSFW_BASE, false); this.concatenatedSubredditNames = concatenatedSubredditNames; if (concatenatedSubredditNames == null) { showErrorView(R.string.anonymous_multireddit_no_subreddit); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } }); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } else { FetchPostFilterAndConcatenatedSubredditNames.fetchPostFilter(mRedditDataRoomDatabase, mExecutor, new Handler(), usage, nameOfUsage, (postFilter) -> { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed() && !isDetached()) { this.postFilter = postFilter; this.postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean(SharedPreferencesUtils.NSFW_BASE, false); initializeAndBindPostViewModelForAnonymous(null); } }); } } else { if (postType == PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE) { if (concatenatedSubredditNames == null) { FetchPostFilterAndConcatenatedSubredditNames.fetchPostFilterAndConcatenatedSubredditNames(mRedditDataRoomDatabase, mExecutor, new Handler(), usage, nameOfUsage, (postFilter, concatenatedSubredditNames) -> { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed() && !isDetached()) { this.postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean(SharedPreferencesUtils.NSFW_BASE, false); this.concatenatedSubredditNames = concatenatedSubredditNames; if (concatenatedSubredditNames == null) { showErrorView(R.string.anonymous_front_page_no_subscriptions); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } }); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } else if (postType == PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT) { if (concatenatedSubredditNames == null) { FetchPostFilterAndConcatenatedSubredditNames.fetchPostFilterAndConcatenatedSubredditNames(mRedditDataRoomDatabase, mExecutor, new Handler(), multiRedditPath, usage, nameOfUsage, (postFilter, concatenatedSubredditNames) -> { if (mActivity != null && !mActivity.isFinishing() && !mActivity.isDestroyed() && !isDetached()) { this.postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean(SharedPreferencesUtils.NSFW_BASE, false); this.concatenatedSubredditNames = concatenatedSubredditNames; if (concatenatedSubredditNames == null) { showErrorView(R.string.anonymous_multireddit_no_subreddit); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } }); } else { initializeAndBindPostViewModelForAnonymous(concatenatedSubredditNames); } } else { initializeAndBindPostViewModelForAnonymous(null); } } } if (nColumns == 1 && mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SWIPE_ACTION, false)) { swipeActionEnabled = true; touchHelper.attachToRecyclerView(binding.recyclerViewPostFragment, 1); } binding.recyclerViewPostFragment.setAdapter(mAdapter); binding.recyclerViewPostFragment.setCacheManager(mAdapter); binding.recyclerViewPostFragment.setPlayerInitializer(order -> { VolumeInfo volumeInfo = new VolumeInfo(true, 0f); return new PlaybackInfo(INDEX_UNSET, TIME_UNSET, volumeInfo); }); SharedPreferencesLiveDataKt.stringLiveData(mSharedPreferences, SharedPreferencesUtils.SIMULTANEOUS_AUTOPLAY_LIMIT, "1").observe(getViewLifecycleOwner(), limit -> { if (getPostAdapter() != null) { getPostAdapter().setSimultaneousAutoplayLimit(Integer.parseInt(limit)); } }); return binding.getRoot(); } private void initializeAndBindPostViewModel() { if (postType == PostPagingSource.TYPE_SEARCH) { mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, mActivity.accessToken, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, mPostHistorySharedPreferences, subredditName, query, trendingSource, postType, sortType, postFilter, readPostsList)).get(PostViewModel.class); } else if (postType == PostPagingSource.TYPE_SUBREDDIT) { mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, mActivity.accessToken, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, mPostHistorySharedPreferences, subredditName, postType, sortType, postFilter, readPostsList)) .get(PostViewModel.class); } else if (postType == PostPagingSource.TYPE_MULTI_REDDIT) { mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, mActivity.accessToken, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, mPostHistorySharedPreferences, multiRedditPath, query, postType, sortType, postFilter, readPostsList)) .get(PostViewModel.class); } else if (postType == PostPagingSource.TYPE_USER) { mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit, mActivity.accessToken, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, mPostHistorySharedPreferences, username, postType, sortType, postFilter, where, readPostsList)) .get(PostViewModel.class); } else { mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mOauthRetrofit, mActivity.accessToken, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, mPostHistorySharedPreferences, postType, sortType, postFilter, readPostsList)).get(PostViewModel.class); } bindPostViewModel(); } private void initializeAndBindPostViewModelForAnonymous(String concatenatedSubredditNames) { if (postType == PostPagingSource.TYPE_SEARCH) { mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mRetrofit, null, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, null, subredditName, query, trendingSource, postType, sortType, postFilter, readPostsList)).get(PostViewModel.class); } else if (postType == PostPagingSource.TYPE_SUBREDDIT) { mPostViewModel = new ViewModelProvider(this, new PostViewModel.Factory(mExecutor, mRetrofit, null, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, null, subredditName, postType, sortType, postFilter, readPostsList)).get(PostViewModel.class); } else if (postType == PostPagingSource.TYPE_USER) { mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mRetrofit, null, mActivity.accountName, mSharedPreferences, mPostFeedScrolledPositionSharedPreferences, null, username, postType, sortType, postFilter, where, readPostsList)).get(PostViewModel.class); } else { //Anonymous front page or multireddit mPostViewModel = new ViewModelProvider(PostFragment.this, new PostViewModel.Factory(mExecutor, mRetrofit, mSharedPreferences, concatenatedSubredditNames, postType, sortType, postFilter, readPostsList)) .get(PostViewModel.class); } bindPostViewModel(); } private void bindPostViewModel() { mPostViewModel.getPosts().observe(getViewLifecycleOwner(), posts -> mAdapter.submitData(getViewLifecycleOwner().getLifecycle(), posts)); mPostViewModel.moderationEventLiveData.observe(getViewLifecycleOwner(), moderationEvent -> { EventBus.getDefault().post(new PostUpdateEventToPostList(moderationEvent.getPost(), moderationEvent.getPosition())); EventBus.getDefault().post(new PostUpdateEventToPostDetailFragment(moderationEvent.getPost())); Toast.makeText(mActivity, moderationEvent.getToastMessageResId(), Toast.LENGTH_SHORT).show(); }); mAdapter.addLoadStateListener(combinedLoadStates -> { LoadState refreshLoadState = combinedLoadStates.getRefresh(); LoadState appendLoadState = combinedLoadStates.getAppend(); binding.swipeRefreshLayoutPostFragment.setRefreshing(refreshLoadState instanceof LoadState.Loading); if (refreshLoadState instanceof LoadState.NotLoading) { if (refreshLoadState.getEndOfPaginationReached() && mAdapter.getItemCount() < 1) { noPostFound(); } else { binding.fetchPostInfoLinearLayoutPostFragment.setVisibility(View.GONE); hasPost = true; } } else if (refreshLoadState instanceof LoadState.Error) { binding.fetchPostInfoLinearLayoutPostFragment.setOnClickListener(view -> refresh()); showErrorView(R.string.load_posts_error); } if (!(refreshLoadState instanceof LoadState.Loading) && appendLoadState instanceof LoadState.NotLoading) { if (appendLoadState.getEndOfPaginationReached() && mAdapter.getItemCount() < 1) { noPostFound(); } } return null; }); binding.recyclerViewPostFragment.setAdapter(mAdapter.withLoadStateFooter(new Paging3LoadingStateAdapter(mActivity, mCustomThemeWrapper, R.string.load_more_posts_error, view -> mAdapter.retry()))); } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.post_fragment, menu); for (int i = 0; i < menu.size(); i++) { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, menu.getItem(i), null); } lazyModeItem = menu.findItem(R.id.action_lazy_mode_post_fragment); if (isInLazyMode) { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, lazyModeItem, getString(R.string.action_stop_lazy_mode)); } else { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, lazyModeItem, getString(R.string.action_start_lazy_mode)); } if (mActivity instanceof FilteredPostsActivity) { menu.findItem(R.id.action_filter_posts_post_fragment).setVisible(false); } if (mActivity instanceof FilteredPostsActivity || mActivity instanceof AccountPostsActivity || mActivity instanceof AccountSavedThingActivity) { menu.findItem(R.id.action_more_options_post_fragment).setVisible(false); } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_lazy_mode_post_fragment) { if (isInLazyMode) { stopLazyMode(); } else { startLazyMode(); } return true; } else if (item.getItemId() == R.id.action_filter_posts_post_fragment) { filterPosts(); return true; } else if (item.getItemId() == R.id.action_more_options_post_fragment) { FABMoreOptionsBottomSheetFragment fabMoreOptionsBottomSheetFragment= new FABMoreOptionsBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putBoolean(FABMoreOptionsBottomSheetFragment.EXTRA_ANONYMOUS_MODE, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)); fabMoreOptionsBottomSheetFragment.setArguments(bundle); fabMoreOptionsBottomSheetFragment.show(mActivity.getSupportFragmentManager(), fabMoreOptionsBottomSheetFragment.getTag()); return true; } return false; } private void noPostFound() { hasPost = false; if (isInLazyMode) { stopLazyMode(); } binding.fetchPostInfoLinearLayoutPostFragment.setOnClickListener(null); showErrorView(R.string.no_posts); } public void changeSortType(SortType sortType) { if (mPostViewModel != null) { if (mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_SORT_TYPE, true)) { switch (postType) { case PostPagingSource.TYPE_FRONT_PAGE: mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_BEST_POST, sortType.getType().name()).apply(); if (sortType.getTime() != null) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TIME_BEST_POST, sortType.getTime().name()).apply(); } break; case PostPagingSource.TYPE_SUBREDDIT: mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_SUBREDDIT_POST_BASE + subredditName, sortType.getType().name()).apply(); if (sortType.getTime() != null) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TIME_SUBREDDIT_POST_BASE + subredditName, sortType.getTime().name()).apply(); } break; case PostPagingSource.TYPE_USER: mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_USER_POST_BASE + username, sortType.getType().name()).apply(); if (sortType.getTime() != null) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TIME_USER_POST_BASE + username, sortType.getTime().name()).apply(); } break; case PostPagingSource.TYPE_SEARCH: mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_SEARCH_POST, sortType.getType().name()).apply(); if (sortType.getTime() != null) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TIME_SEARCH_POST, sortType.getTime().name()).apply(); } break; case PostPagingSource.TYPE_MULTI_REDDIT: case PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_MULTI_REDDIT_POST_BASE + multiRedditPath, sortType.getType().name()).apply(); if (sortType.getTime() != null) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TIME_MULTI_REDDIT_POST_BASE + multiRedditPath, sortType.getTime().name()).apply(); } break; case PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE: mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_SUBREDDIT_POST_BASE + Account.ANONYMOUS_ACCOUNT, sortType.getType().name()).apply(); if (sortType.getTime() != null) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TIME_SUBREDDIT_POST_BASE + Account.ANONYMOUS_ACCOUNT, sortType.getTime().name()).apply(); } break; } } if (binding.fetchPostInfoLinearLayoutPostFragment.getVisibility() != View.GONE) { binding.fetchPostInfoLinearLayoutPostFragment.setVisibility(View.GONE); mGlide.clear(binding.fetchPostInfoImageViewPostFragment); } hasPost = false; if (isInLazyMode) { stopLazyMode(); } this.sortType = sortType; mPostViewModel.changeSortType(sortType); goBackToTop(); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(IS_IN_LAZY_MODE_STATE, isInLazyMode); if (mLinearLayoutManager != null) { outState.putInt(RECYCLER_VIEW_POSITION_STATE, mLinearLayoutManager.findFirstVisibleItemPosition()); } else if (mStaggeredGridLayoutManager != null) { int[] into = new int[mStaggeredGridLayoutManager.getSpanCount()]; outState.putInt(RECYCLER_VIEW_POSITION_STATE, mStaggeredGridLayoutManager.findFirstVisibleItemPositions(into)[0]); } outState.putParcelable(POST_FILTER_STATE, postFilter); outState.putString(CONCATENATED_SUBREDDIT_NAMES_STATE, concatenatedSubredditNames); outState.putLong(POST_FRAGMENT_ID_STATE, postFragmentId); } @Override public void onStop() { super.onStop(); saveCache(); } private void saveCache() { if (savePostFeedScrolledPosition && postType == PostPagingSource.TYPE_FRONT_PAGE && sortType != null && sortType.getType() == SortType.Type.BEST && mAdapter != null) { Post currentPost = mAdapter.getItemByPosition(maxPosition); if (currentPost != null) { String accountNameForCache = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_ANONYMOUS : mActivity.accountName; String key = accountNameForCache + SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_FRONT_PAGE_BASE; String value = currentPost.getFullName(); mPostFeedScrolledPositionSharedPreferences.edit().putString(key, value).apply(); } } } @Override public void refresh() { binding.fetchPostInfoLinearLayoutPostFragment.setVisibility(View.GONE); hasPost = false; if (isInLazyMode) { stopLazyMode(); } saveCache(); mAdapter.refresh(); goBackToTop(); } @Override protected void showErrorView(int stringResId) { if (mActivity != null && isAdded()) { binding.swipeRefreshLayoutPostFragment.setRefreshing(false); binding.fetchPostInfoLinearLayoutPostFragment.setVisibility(View.VISIBLE); binding.fetchPostInfoTextViewPostFragment.setText(stringResId); mGlide.load(R.drawable.error_image).into(binding.fetchPostInfoImageViewPostFragment); } } @NonNull @Override protected SwipeRefreshLayout getSwipeRefreshLayout() { return binding.swipeRefreshLayoutPostFragment; } @NonNull @Override protected RecyclerView getPostRecyclerView() { return binding.recyclerViewPostFragment; } @Nullable @Override protected PostRecyclerViewAdapter getPostAdapter() { return mAdapter; } @Override public void changeNSFW(boolean nsfw) { postFilter.allowNSFW = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && nsfw; if (mPostViewModel != null) { mPostViewModel.changePostFilter(postFilter); } } @Override public void changePostLayout(int postLayout, boolean temporary) { this.postLayout = postLayout; if (!temporary) { switch (postType) { case PostPagingSource.TYPE_FRONT_PAGE: case PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_FRONT_PAGE_POST, postLayout).apply(); break; case PostPagingSource.TYPE_SUBREDDIT: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_SUBREDDIT_POST_BASE + subredditName, postLayout).apply(); break; case PostPagingSource.TYPE_USER: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE + username, postLayout).apply(); break; case PostPagingSource.TYPE_SEARCH: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_SEARCH_POST, postLayout).apply(); break; case PostPagingSource.TYPE_MULTI_REDDIT: case PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: mPostLayoutSharedPreferences.edit().putInt(SharedPreferencesUtils.POST_LAYOUT_MULTI_REDDIT_POST_BASE + multiRedditPath, postLayout).apply(); break; } } int previousPosition = -1; if (mLinearLayoutManager != null) { previousPosition = mLinearLayoutManager.findFirstVisibleItemPosition(); } else if (mStaggeredGridLayoutManager != null) { int[] into = new int[mStaggeredGridLayoutManager.getSpanCount()]; previousPosition = mStaggeredGridLayoutManager.findFirstVisibleItemPositions(into)[0]; } int nColumns = getNColumns(getResources()); if (nColumns == 1) { mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); if (binding.recyclerViewPostFragment.getItemDecorationCount() > 0) { binding.recyclerViewPostFragment.removeItemDecorationAt(0); } binding.recyclerViewPostFragment.setLayoutManager(mLinearLayoutManager); mStaggeredGridLayoutManager = null; } else { mStaggeredGridLayoutManager = new StaggeredGridLayoutManager(nColumns, StaggeredGridLayoutManager.VERTICAL); if (binding.recyclerViewPostFragment.getItemDecorationCount() > 0) { binding.recyclerViewPostFragment.removeItemDecorationAt(0); } binding.recyclerViewPostFragment.setLayoutManager(mStaggeredGridLayoutManager); StaggeredGridLayoutManagerItemOffsetDecoration itemDecoration = new StaggeredGridLayoutManagerItemOffsetDecoration(mActivity, R.dimen.staggeredLayoutManagerItemOffset, nColumns); binding.recyclerViewPostFragment.addItemDecoration(itemDecoration); mLinearLayoutManager = null; } if (previousPosition > 0) { binding.recyclerViewPostFragment.scrollToPosition(previousPosition); } if (mAdapter != null) { mAdapter.setPostLayout(postLayout); refreshAdapter(); } } @Override public void applyTheme() { binding.swipeRefreshLayoutPostFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutPostFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); binding.fetchPostInfoTextViewPostFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchPostInfoTextViewPostFragment.setTypeface(mActivity.typeface); } } @Override public void hideReadPosts() { mPostViewModel.hideReadPosts(); } @Override public void changePostFilter(PostFilter postFilter) { this.postFilter = postFilter; if (mPostViewModel != null) { mPostViewModel.changePostFilter(postFilter); } } @Override public PostFilter getPostFilter() { return postFilter; } @Override public void filterPosts() { if (postType == PostPagingSource.TYPE_SEARCH) { Intent intent = new Intent(mActivity, CustomizePostFilterActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_TRENDING_SOURCE, trendingSource); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(CustomizePostFilterActivity.EXTRA_START_FILTERED_POSTS_WHEN_FINISH, true); startActivity(intent); } else if (postType == PostPagingSource.TYPE_SUBREDDIT) { Intent intent = new Intent(mActivity, CustomizePostFilterActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, subredditName); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(CustomizePostFilterActivity.EXTRA_START_FILTERED_POSTS_WHEN_FINISH, true); startActivity(intent); } else if (postType == PostPagingSource.TYPE_MULTI_REDDIT || postType == PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT) { Intent intent = new Intent(mActivity, CustomizePostFilterActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, multiRedditPath); intent.putExtra(FilteredPostsActivity.EXTRA_QUERY, query); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(CustomizePostFilterActivity.EXTRA_START_FILTERED_POSTS_WHEN_FINISH, true); startActivity(intent); } else if (postType == PostPagingSource.TYPE_USER) { Intent intent = new Intent(mActivity, CustomizePostFilterActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, username); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(FilteredPostsActivity.EXTRA_USER_WHERE, where); intent.putExtra(CustomizePostFilterActivity.EXTRA_START_FILTERED_POSTS_WHEN_FINISH, true); startActivity(intent); } else { Intent intent = new Intent(mActivity, CustomizePostFilterActivity.class); intent.putExtra(FilteredPostsActivity.EXTRA_NAME, mActivity.getString(R.string.best)); intent.putExtra(FilteredPostsActivity.EXTRA_POST_TYPE, postType); intent.putExtra(CustomizePostFilterActivity.EXTRA_START_FILTERED_POSTS_WHEN_FINISH, true); startActivity(intent); } } @Override public boolean getIsNsfwSubreddit() { if (mActivity instanceof ViewSubredditDetailActivity) { return ((ViewSubredditDetailActivity) mActivity).isNsfwSubreddit(); } else if (mActivity instanceof FilteredPostsActivity) { return ((FilteredPostsActivity) mActivity).isNsfwSubreddit(); } else { return false; } } @Subscribe public void onChangeDefaultPostLayoutEvent(ChangeDefaultPostLayoutEvent changeDefaultPostLayoutEvent) { Bundle bundle = getArguments(); if (bundle != null) { switch (postType) { case PostPagingSource.TYPE_SUBREDDIT: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_SUBREDDIT_POST_BASE + bundle.getString(EXTRA_NAME))) { changePostLayout(changeDefaultPostLayoutEvent.defaultPostLayout, true); } break; case PostPagingSource.TYPE_USER: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE + bundle.getString(EXTRA_USER_NAME))) { changePostLayout(changeDefaultPostLayoutEvent.defaultPostLayout, true); } break; case PostPagingSource.TYPE_MULTI_REDDIT: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_MULTI_REDDIT_POST_BASE + bundle.getString(EXTRA_NAME))) { changePostLayout(changeDefaultPostLayoutEvent.defaultPostLayout, true); } break; case PostPagingSource.TYPE_SEARCH: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_SEARCH_POST)) { changePostLayout(changeDefaultPostLayoutEvent.defaultPostLayout, true); } break; case PostPagingSource.TYPE_FRONT_PAGE: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_FRONT_PAGE_POST)) { changePostLayout(changeDefaultPostLayoutEvent.defaultPostLayout, true); } break; } } } @Subscribe public void onChangeDefaultPostLayoutUnfoldedEvent(ChangeDefaultPostLayoutUnfoldedEvent event) { boolean foldEnabled = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_FOLD_SUPPORT, false); boolean isTablet = getResources().getBoolean(R.bool.isTablet); if (foldEnabled && isTablet) { Bundle bundle = getArguments(); if (bundle != null) { switch (postType) { case PostPagingSource.TYPE_SUBREDDIT: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_SUBREDDIT_POST_BASE + bundle.getString(EXTRA_NAME))) { changePostLayout(event.defaultPostLayoutUnfolded, true); } break; case PostPagingSource.TYPE_USER: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_USER_POST_BASE + bundle.getString(EXTRA_USER_NAME))) { changePostLayout(event.defaultPostLayoutUnfolded, true); } break; case PostPagingSource.TYPE_MULTI_REDDIT: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_MULTI_REDDIT_POST_BASE + bundle.getString(EXTRA_NAME))) { changePostLayout(event.defaultPostLayoutUnfolded, true); } break; case PostPagingSource.TYPE_SEARCH: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_SEARCH_POST)) { changePostLayout(event.defaultPostLayoutUnfolded, true); } break; case PostPagingSource.TYPE_FRONT_PAGE: if (!mPostLayoutSharedPreferences.contains(SharedPreferencesUtils.POST_LAYOUT_FRONT_PAGE_POST)) { changePostLayout(event.defaultPostLayoutUnfolded, true); } break; } } } } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { if (mAdapter != null) { String autoplay = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_AUTOPLAY, SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_NEVER); String dataSavingMode = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); boolean stateChanged = false; if (autoplay.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ON_WIFI)) { mAdapter.setAutoplay(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_WIFI); stateChanged = true; } if (dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { mAdapter.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); stateChanged = true; } if (stateChanged) { refreshAdapter(); } } } @Subscribe public void onChangeSavePostFeedScrolledPositionEvent(ChangeSavePostFeedScrolledPositionEvent changeSavePostFeedScrolledPositionEvent) { savePostFeedScrolledPosition = changeSavePostFeedScrolledPositionEvent.savePostFeedScrolledPosition; } @Subscribe public void onNeedForPostListFromPostRecyclerViewAdapterEvent(NeedForPostListFromPostFragmentEvent event) { if (postFragmentId == event.postFragmentTimeId && mAdapter != null) { EventBus.getDefault().post(new ProvidePostListToViewPostDetailActivityEvent(postFragmentId, new ArrayList<>(mAdapter.snapshot()), postType, subredditName, concatenatedSubredditNames, username, where, multiRedditPath, query, trendingSource, postFilter, sortType, readPostsList)); } } @Override protected void refreshAdapter() { int previousPosition = -1; if (mLinearLayoutManager != null) { previousPosition = mLinearLayoutManager.findFirstVisibleItemPosition(); } else if (mStaggeredGridLayoutManager != null) { int[] into = new int[mStaggeredGridLayoutManager.getSpanCount()]; previousPosition = mStaggeredGridLayoutManager.findFirstVisibleItemPositions(into)[0]; } RecyclerView.LayoutManager layoutManager = binding.recyclerViewPostFragment.getLayoutManager(); binding.recyclerViewPostFragment.setAdapter(null); binding.recyclerViewPostFragment.setLayoutManager(null); binding.recyclerViewPostFragment.setAdapter(mAdapter); binding.recyclerViewPostFragment.setLayoutManager(layoutManager); if (previousPosition > 0) { binding.recyclerViewPostFragment.scrollToPosition(previousPosition); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); if (isInLazyMode) { lazyModeRunnable.resetOldPosition(); } } else if (mStaggeredGridLayoutManager != null) { mStaggeredGridLayoutManager.scrollToPositionWithOffset(0, 0); if (isInLazyMode) { lazyModeRunnable.resetOldPosition(); } } } public SortType getSortType() { return sortType; } public int getPostType() { return postType; } @Override public void onPause() { super.onPause(); if (isInLazyMode) { pauseLazyMode(false); } if (mAdapter != null) { binding.recyclerViewPostFragment.onWindowVisibilityChanged(View.GONE); } } @Override public void onDestroyView() { binding.recyclerViewPostFragment.addOnWindowFocusChangedListener(null); super.onDestroyView(); } private void onWindowFocusChanged(boolean hasWindowsFocus) { if (mAdapter != null) { mAdapter.setCanPlayVideo(hasWindowsFocus); } } @Override public void approvePost(@NonNull Post post, int position) { mPostViewModel.approvePost(post, position); } @Override public void removePost(@NonNull Post post, int position, boolean isSpam) { mPostViewModel.removePost(post, position, isSpam); } @Override public void toggleSticky(@NonNull Post post, int position) { mPostViewModel.toggleSticky(post, position); } @Override public void toggleLock(@NonNull Post post, int position) { mPostViewModel.toggleLock(post, position); } @Override public void toggleNSFW(@NonNull Post post, int position) { mPostViewModel.toggleNSFW(post, position); } @Override public void toggleSpoiler(@NonNull Post post, int position) { mPostViewModel.toggleSpoiler(post, position); } @Override public void toggleMod(@NonNull Post post, int position) { mPostViewModel.toggleMod(post, position); } @Override public void toggleNotification(@NotNull Post post, int position) { mPostViewModel.toggleNotification(post, position); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragmentBase.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import static androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.CountDownTimer; import android.os.Handler; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.Toast; import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.core.content.res.ResourcesCompat; import androidx.core.view.ViewCompat; import androidx.fragment.app.Fragment; import androidx.media3.common.util.UnstableApi; import androidx.paging.ItemSnapshotList; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.PostRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon; import ml.docilealligator.infinityforreddit.asynctasks.LoadUserData; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.AdjustableTouchSlopItemTouchHelper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.events.ChangeAutoplayNsfwVideosEvent; import ml.docilealligator.infinityforreddit.events.ChangeCompactLayoutToolbarHiddenByDefaultEvent; import ml.docilealligator.infinityforreddit.events.ChangeDataSavingModeEvent; import ml.docilealligator.infinityforreddit.events.ChangeDefaultLinkPostLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangeDisableImagePreviewEvent; import ml.docilealligator.infinityforreddit.events.ChangeEasierToWatchInFullScreenEvent; import ml.docilealligator.infinityforreddit.events.ChangeEnableSwipeActionSwitchEvent; import ml.docilealligator.infinityforreddit.events.ChangeFixedHeightPreviewInCardEvent; import ml.docilealligator.infinityforreddit.events.ChangeHidePostFlairEvent; import ml.docilealligator.infinityforreddit.events.ChangeHidePostTypeEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideSubredditAndUserPrefixEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideTextPostContent; import ml.docilealligator.infinityforreddit.events.ChangeHideTheNumberOfCommentsEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideTheNumberOfVotesEvent; import ml.docilealligator.infinityforreddit.events.ChangeLongPressToHideToolbarInCompactLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangeMuteAutoplayingVideosEvent; import ml.docilealligator.infinityforreddit.events.ChangeMuteNSFWVideoEvent; import ml.docilealligator.infinityforreddit.events.ChangeNSFWBlurEvent; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.events.ChangeOnlyDisablePreviewInVideoAndGifPostsEvent; import ml.docilealligator.infinityforreddit.events.ChangePostFeedMaxResolutionEvent; import ml.docilealligator.infinityforreddit.events.ChangePostLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangePullToRefreshEvent; import ml.docilealligator.infinityforreddit.events.ChangeRememberMutingOptionInPostFeedEvent; import ml.docilealligator.infinityforreddit.events.ChangeShowAbsoluteNumberOfVotesEvent; import ml.docilealligator.infinityforreddit.events.ChangeShowElapsedTimeEvent; import ml.docilealligator.infinityforreddit.events.ChangeSpoilerBlurEvent; import ml.docilealligator.infinityforreddit.events.ChangeStartAutoplayVisibleAreaOffsetEvent; import ml.docilealligator.infinityforreddit.events.ChangeSwipeActionEvent; import ml.docilealligator.infinityforreddit.events.ChangeSwipeActionThresholdEvent; import ml.docilealligator.infinityforreddit.events.ChangeTimeFormatEvent; import ml.docilealligator.infinityforreddit.events.ChangeVibrateWhenActionTriggeredEvent; import ml.docilealligator.infinityforreddit.events.ChangeVideoAutoplayEvent; import ml.docilealligator.infinityforreddit.events.ChangeVoteButtonsPositionEvent; import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostList; import ml.docilealligator.infinityforreddit.events.ShowDividerInCompactLayoutPreferenceEvent; import ml.docilealligator.infinityforreddit.events.ShowThumbnailOnTheLeftInCompactLayoutEvent; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesLiveDataKt; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public abstract class PostFragmentBase extends Fragment { @Inject @Named("no_oauth") protected Retrofit mRetrofit; @Inject @Named("oauth") protected Retrofit mOauthRetrofit; @Inject @Named("default") protected SharedPreferences mSharedPreferences; @Inject protected RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject protected Executor mExecutor; protected BaseActivity mActivity; protected RequestManager mGlide; protected Window window; protected MenuItem lazyModeItem; protected LinearLayoutManagerBugFixed mLinearLayoutManager; protected StaggeredGridLayoutManager mStaggeredGridLayoutManager; protected boolean hasPost; protected long postFragmentId; protected boolean rememberMutingOptionInPostFeed; protected Boolean masterMutingOption; protected Handler lazyModeHandler; protected CountDownTimer resumeLazyModeCountDownTimer; protected RecyclerView.SmoothScroller smoothScroller; protected LazyModeRunnable lazyModeRunnable; protected float lazyModeInterval; protected boolean isInLazyMode = false; protected boolean isLazyModePaused = false; protected int postLayout; protected boolean swipeActionEnabled; protected ColorDrawable backgroundSwipeRight; protected ColorDrawable backgroundSwipeLeft; protected Drawable drawableSwipeRight; protected Drawable drawableSwipeLeft; protected boolean vibrateWhenActionTriggered; protected float swipeActionThreshold; protected int swipeLeftAction; protected int swipeRightAction; protected AdjustableTouchSlopItemTouchHelper touchHelper; private boolean shouldSwipeBack; protected final Map subredditOrUserIcons = new HashMap<>(); public PostFragmentBase() { // Required empty public constructor } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { EventBus.getDefault().register(this); window = mActivity.getWindow(); rememberMutingOptionInPostFeed = mSharedPreferences.getBoolean(SharedPreferencesUtils.REMEMBER_MUTING_OPTION_IN_POST_FEED, false); smoothScroller = new LinearSmoothScroller(mActivity) { @Override protected int getVerticalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; } }; lazyModeHandler = new Handler(); lazyModeRunnable = new LazyModeRunnable() { @Override public void run() { if (isInLazyMode && !isLazyModePaused && getPostAdapter() != null) { int nPosts = getPostAdapter().getItemCount(); if (getCurrentPosition() == -1) { if (mLinearLayoutManager != null) { setCurrentPosition(mLinearLayoutManager.findFirstVisibleItemPosition()); } else { int[] into = new int[2]; setCurrentPosition(mStaggeredGridLayoutManager.findFirstVisibleItemPositions(into)[1]); } } if (getCurrentPosition() != RecyclerView.NO_POSITION && nPosts > getCurrentPosition()) { incrementCurrentPosition(); smoothScroller.setTargetPosition(getCurrentPosition()); if (mLinearLayoutManager != null) { mLinearLayoutManager.startSmoothScroll(smoothScroller); } else { mStaggeredGridLayoutManager.startSmoothScroll(smoothScroller); } } } lazyModeHandler.postDelayed(this, (long) (lazyModeInterval * 1000)); } }; lazyModeInterval = Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.LAZY_MODE_INTERVAL_KEY, "2.5")); resumeLazyModeCountDownTimer = new CountDownTimer((long) (lazyModeInterval * 1000), (long) (lazyModeInterval * 1000)) { @Override public void onTick(long l) { } @Override public void onFinish() { resumeLazyMode(true); } }; mGlide = Glide.with(mActivity); vibrateWhenActionTriggered = mSharedPreferences.getBoolean(SharedPreferencesUtils.VIBRATE_WHEN_ACTION_TRIGGERED, true); swipeActionThreshold = Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_ACTION_THRESHOLD, "0.3")); swipeRightAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_RIGHT_ACTION, "1")); swipeLeftAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_LEFT_ACTION, "0")); initializeSwipeActionDrawable(); touchHelper = new AdjustableTouchSlopItemTouchHelper(new AdjustableTouchSlopItemTouchHelper.Callback() { boolean exceedThreshold = false; @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return makeMovementFlags(ACTION_STATE_IDLE, calculateMovementFlags(recyclerView, viewHolder)); } @Override public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { return false; } @Override public boolean isItemViewSwipeEnabled() { return true; } @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {} @Override public int convertToAbsoluteDirection(int flags, int layoutDirection) { if (shouldSwipeBack) { shouldSwipeBack = false; return 0; } return super.convertToAbsoluteDirection(flags, layoutDirection); } @Override public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { View itemView = viewHolder.itemView; int horizontalOffset = (int) Utils.convertDpToPixel(16, mActivity); if (dX > 0) { if (dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) { dX = (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold; if (!exceedThreshold && isCurrentlyActive) { exceedThreshold = true; if (vibrateWhenActionTriggered) { itemView.setHapticFeedbackEnabled(true); itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } backgroundSwipeRight.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom()); } else { exceedThreshold = false; backgroundSwipeRight.setBounds(0, 0, 0, 0); } drawableSwipeRight.setBounds(itemView.getLeft() + ((int) dX) - horizontalOffset - drawableSwipeRight.getIntrinsicWidth(), (itemView.getBottom() + itemView.getTop() - drawableSwipeRight.getIntrinsicHeight()) / 2, itemView.getLeft() + ((int) dX) - horizontalOffset, (itemView.getBottom() + itemView.getTop() + drawableSwipeRight.getIntrinsicHeight()) / 2); backgroundSwipeRight.draw(c); drawableSwipeRight.draw(c); } else if (dX < 0) { if (-dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) { dX = -(itemView.getRight() - itemView.getLeft()) * swipeActionThreshold; if (!exceedThreshold && isCurrentlyActive) { exceedThreshold = true; if (vibrateWhenActionTriggered) { itemView.setHapticFeedbackEnabled(true); itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } backgroundSwipeLeft.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom()); } else { exceedThreshold = false; backgroundSwipeLeft.setBounds(0, 0, 0, 0); } drawableSwipeLeft.setBounds(itemView.getRight() + ((int) dX) + horizontalOffset, (itemView.getBottom() + itemView.getTop() - drawableSwipeLeft.getIntrinsicHeight()) / 2, itemView.getRight() + ((int) dX) + horizontalOffset + drawableSwipeLeft.getIntrinsicWidth(), (itemView.getBottom() + itemView.getTop() + drawableSwipeLeft.getIntrinsicHeight()) / 2); backgroundSwipeLeft.draw(c); drawableSwipeLeft.draw(c); } if (!isCurrentlyActive && exceedThreshold && getPostAdapter() != null) { getPostAdapter().onItemSwipe(viewHolder, dX > 0 ? ItemTouchHelper.END : ItemTouchHelper.START, swipeLeftAction, swipeRightAction); exceedThreshold = false; } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } @Override public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return 1; } }); getPostRecyclerView().setOnTouchListener((view, motionEvent) -> { shouldSwipeBack = motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP; if (isInLazyMode) { pauseLazyMode(true); } return false; }); SharedPreferencesLiveDataKt.stringLiveData(mSharedPreferences, SharedPreferencesUtils.LONG_PRESS_POST_NON_MEDIA_AREA, SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS).observe(getViewLifecycleOwner(), s -> { if (getPostAdapter() != null) { getPostAdapter().setLongPressPostNonMediaAreaAction(s); } }); SharedPreferencesLiveDataKt.stringLiveData(mSharedPreferences, SharedPreferencesUtils.LONG_PRESS_POST_MEDIA, SharedPreferencesUtils.LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS).observe(getViewLifecycleOwner(), s -> { if (getPostAdapter() != null) { getPostAdapter().setLongPressPostMediaAction(s); } }); SharedPreferencesLiveDataKt.stringLiveData(mSharedPreferences, SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION, "360").observe(getViewLifecycleOwner(), s -> { if (getPostAdapter() != null) { getPostAdapter().setDataSavingModeDefaultResolution(Integer.parseInt(s)); } }); SharedPreferencesLiveDataKt.stringLiveData(mSharedPreferences, SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION_NO_DATA_SAVING, "0").observe(getViewLifecycleOwner(), s -> { if (getPostAdapter() != null) { getPostAdapter().setNonDataSavingModeDefaultResolution(Integer.parseInt(s)); } }); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ViewCompat.requestApplyInsets(view); } @Override public void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.mActivity = (BaseActivity) context; } public final boolean handleKeyDown(int keyCode) { boolean volumeKeysNavigatePosts = mSharedPreferences.getBoolean(SharedPreferencesUtils.VOLUME_KEYS_NAVIGATE_POSTS, false); if (volumeKeysNavigatePosts) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: return scrollPostsByCount(-1); case KeyEvent.KEYCODE_VOLUME_DOWN: return scrollPostsByCount(1); } } return false; } public final long getPostFragmentId() { return postFragmentId; } public boolean startLazyMode() { if (!hasPost) { Toast.makeText(mActivity, R.string.no_posts_no_lazy_mode, Toast.LENGTH_SHORT).show(); return false; } Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, lazyModeItem, getString(R.string.action_stop_lazy_mode)); if (getPostAdapter() != null && getPostAdapter().isAutoplay()) { getPostAdapter().setAutoplay(false); refreshAdapter(); } isInLazyMode = true; isLazyModePaused = false; lazyModeInterval = Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.LAZY_MODE_INTERVAL_KEY, "2.5")); lazyModeHandler.postDelayed(lazyModeRunnable, (long) (lazyModeInterval * 1000)); window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Toast.makeText(mActivity, getString(R.string.lazy_mode_start, lazyModeInterval), Toast.LENGTH_SHORT).show(); return true; } public void stopLazyMode() { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, lazyModeItem, getString(R.string.action_start_lazy_mode)); if (getPostAdapter() != null) { String autoplayString = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_AUTOPLAY, SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_NEVER); if (autoplayString.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ALWAYS_ON) || (autoplayString.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ON_WIFI) && Utils.isConnectedToWifi(mActivity))) { getPostAdapter().setAutoplay(true); refreshAdapter(); } } isInLazyMode = false; isLazyModePaused = false; lazyModeRunnable.resetOldPosition(); lazyModeHandler.removeCallbacks(lazyModeRunnable); resumeLazyModeCountDownTimer.cancel(); window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Toast.makeText(mActivity, getString(R.string.lazy_mode_stop), Toast.LENGTH_SHORT).show(); } public void resumeLazyMode(boolean resumeNow) { if (isInLazyMode) { if (getPostAdapter() != null && getPostAdapter().isAutoplay()) { getPostAdapter().setAutoplay(false); refreshAdapter(); } isLazyModePaused = false; window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); lazyModeRunnable.resetOldPosition(); if (resumeNow) { lazyModeHandler.post(lazyModeRunnable); } else { lazyModeHandler.postDelayed(lazyModeRunnable, (long) (lazyModeInterval * 1000)); } } } public void pauseLazyMode(boolean startTimer) { resumeLazyModeCountDownTimer.cancel(); isInLazyMode = true; isLazyModePaused = true; lazyModeHandler.removeCallbacks(lazyModeRunnable); window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); if (startTimer) { resumeLazyModeCountDownTimer.start(); } } public final boolean isInLazyMode() { return isInLazyMode; } protected abstract void refreshAdapter(); protected final int getNColumns(Resources resources) { final boolean foldEnabled = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_FOLD_SUPPORT, false); if (resources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { switch (postLayout) { case SharedPreferencesUtils.POST_LAYOUT_CARD_2: return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_CARD_LAYOUT_2, "1")); case SharedPreferencesUtils.POST_LAYOUT_COMPACT: return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_COMPACT_LAYOUT, "1")); case SharedPreferencesUtils.POST_LAYOUT_GALLERY: return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_GALLERY_LAYOUT, "2")); default: if (getResources().getBoolean(R.bool.isTablet)) { if (foldEnabled) { return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_UNFOLDED, "2")); } else { return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT, "2")); } } return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT, "1")); } } else { switch (postLayout) { case SharedPreferencesUtils.POST_LAYOUT_CARD_2: return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_CARD_LAYOUT_2, "2")); case SharedPreferencesUtils.POST_LAYOUT_COMPACT: return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_COMPACT_LAYOUT, "2")); case SharedPreferencesUtils.POST_LAYOUT_GALLERY: return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_GALLERY_LAYOUT, "2")); default: if (getResources().getBoolean(R.bool.isTablet) && foldEnabled) { return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_UNFOLDED, "2")); } return Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE, "2")); } } } public final void changePostLayout(int postLayout) { changePostLayout(postLayout, false); } public abstract void changePostLayout(int postLayout, boolean temporary); public final Boolean getMasterMutingOption() { return masterMutingOption; } public final void videoAutoplayChangeMutingOption(boolean isMute) { if (rememberMutingOptionInPostFeed) { masterMutingOption = isMute; } } public boolean getIsNsfwSubreddit() { return false; } public boolean isRecyclerViewItemSwipeable(RecyclerView.ViewHolder viewHolder) { if (swipeActionEnabled) { if (viewHolder instanceof PostRecyclerViewAdapter.PostBaseGalleryTypeViewHolder) { return !((PostRecyclerViewAdapter.PostBaseGalleryTypeViewHolder) viewHolder).isSwipeLocked(); } return true; } return false; } public final void loadIcon(String subredditOrUserName, boolean isSubreddit, LoadIconListener loadIconListener) { if (subredditOrUserIcons.containsKey(subredditOrUserName)) { loadIconListener.loadIconSuccess(subredditOrUserName, subredditOrUserIcons.get(subredditOrUserName)); } else { if (isSubreddit) { LoadSubredditIcon.loadSubredditIcon(mExecutor, new Handler(), mRedditDataRoomDatabase, subredditOrUserName, mActivity.accessToken, mActivity.accountName, mOauthRetrofit, mRetrofit, iconImageUrl -> { subredditOrUserIcons.put(subredditOrUserName, iconImageUrl); loadIconListener.loadIconSuccess(subredditOrUserName, iconImageUrl); }); } else { LoadUserData.loadUserData(mExecutor, new Handler(), mRedditDataRoomDatabase, mActivity.accessToken, subredditOrUserName, mOauthRetrofit, mRetrofit, iconImageUrl -> { subredditOrUserIcons.put(subredditOrUserName, iconImageUrl); loadIconListener.loadIconSuccess(subredditOrUserName, iconImageUrl); }); } } } protected abstract boolean scrollPostsByCount(int count); protected final void initializeSwipeActionDrawable() { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { backgroundSwipeRight = new ColorDrawable(mCustomThemeWrapper.getDownvoted()); drawableSwipeRight = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_downward_day_night_24dp, null); } else { backgroundSwipeRight = new ColorDrawable(mCustomThemeWrapper.getUpvoted()); drawableSwipeRight = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_upward_day_night_24dp, null); } if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { backgroundSwipeLeft = new ColorDrawable(mCustomThemeWrapper.getUpvoted()); drawableSwipeLeft = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_upward_day_night_24dp, null); } else { backgroundSwipeLeft = new ColorDrawable(mCustomThemeWrapper.getDownvoted()); drawableSwipeLeft = ResourcesCompat.getDrawable(mActivity.getResources(), R.drawable.ic_arrow_downward_day_night_24dp, null); } } protected int calculateMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { if (!(viewHolder instanceof PostRecyclerViewAdapter.PostBaseViewHolder) && !(viewHolder instanceof PostRecyclerViewAdapter.PostCompactBaseViewHolder)) { return 0; } else if (viewHolder instanceof PostRecyclerViewAdapter.PostBaseGalleryTypeViewHolder) { if (((PostRecyclerViewAdapter.PostBaseGalleryTypeViewHolder) viewHolder).isSwipeLocked()) { return 0; } } return ItemTouchHelper.START | ItemTouchHelper.END; } protected abstract void showErrorView(int stringResId); @NonNull protected abstract SwipeRefreshLayout getSwipeRefreshLayout(); @NonNull protected abstract RecyclerView getPostRecyclerView(); @Nullable protected abstract PostRecyclerViewAdapter getPostAdapter(); @Subscribe public void onPostUpdateEvent(PostUpdateEventToPostList event) { if (getPostAdapter() == null) { return; } ItemSnapshotList posts = getPostAdapter().snapshot(); if (event.positionInList >= 0 && event.positionInList < posts.size()) { Post post = posts.get(event.positionInList); if (post != null && post.getFullName().equals(event.post.getFullName())) { post.setTitle(event.post.getTitle()); post.setVoteType(event.post.getVoteType()); post.setScore(event.post.getScore()); post.setNComments(event.post.getNComments()); post.setNSFW(event.post.isNSFW()); post.setHidden(event.post.isHidden()); post.setSpoiler(event.post.isSpoiler()); post.setFlair(event.post.getFlair()); post.setSaved(event.post.isSaved()); post.setIsStickied(event.post.isStickied()); post.setApproved(event.post.isApproved()); post.setApprovedAtUTC(event.post.getApprovedAtUTC()); post.setApprovedBy(event.post.getApprovedBy()); post.setRemoved(event.post.isRemoved(), event.post.isSpam()); post.setIsLocked(event.post.isLocked()); post.setIsModerator(event.post.isModerator()); if (event.post.isRead()) { post.markAsRead(); } getPostAdapter().notifyItemChanged(event.positionInList); } } } @Subscribe public void onChangeShowElapsedTimeEvent(ChangeShowElapsedTimeEvent event) { if (getPostAdapter() != null) { getPostAdapter().setShowElapsedTime(event.showElapsedTime); refreshAdapter(); } } @Subscribe public void onChangeTimeFormatEvent(ChangeTimeFormatEvent changeTimeFormatEvent) { if (getPostAdapter() != null) { getPostAdapter().setTimeFormat(changeTimeFormatEvent.timeFormat); refreshAdapter(); } } @Subscribe public void onChangeVoteButtonsPositionEvent(ChangeVoteButtonsPositionEvent event) { if (getPostAdapter() != null) { getPostAdapter().setVoteButtonsPosition(event.voteButtonsOnTheRight); refreshAdapter(); } } @Subscribe public void onChangeNSFWBlurEvent(ChangeNSFWBlurEvent event) { if (getPostAdapter() != null) { getPostAdapter().setBlurNsfwAndDoNotBlurNsfwInNsfwSubreddits(event.needBlurNSFW, event.doNotBlurNsfwInNsfwSubreddits); refreshAdapter(); } } @Subscribe public void onChangeSpoilerBlurEvent(ChangeSpoilerBlurEvent event) { if (getPostAdapter() != null) { getPostAdapter().setBlurSpoiler(event.needBlurSpoiler); refreshAdapter(); } } @Subscribe public void onChangePostLayoutEvent(ChangePostLayoutEvent event) { changePostLayout(event.postLayout); } @Subscribe public void onShowDividerInCompactLayoutPreferenceEvent(ShowDividerInCompactLayoutPreferenceEvent event) { if (getPostAdapter() != null) { getPostAdapter().setShowDividerInCompactLayout(event.showDividerInCompactLayout); refreshAdapter(); } } @Subscribe public void onChangeDefaultLinkPostLayoutEvent(ChangeDefaultLinkPostLayoutEvent event) { if (getPostAdapter() != null) { getPostAdapter().setDefaultLinkPostLayout(event.defaultLinkPostLayout); refreshAdapter(); } } @Subscribe public void onChangeShowAbsoluteNumberOfVotesEvent(ChangeShowAbsoluteNumberOfVotesEvent changeShowAbsoluteNumberOfVotesEvent) { if (getPostAdapter() != null) { getPostAdapter().setShowAbsoluteNumberOfVotes(changeShowAbsoluteNumberOfVotesEvent.showAbsoluteNumberOfVotes); refreshAdapter(); } } @Subscribe public void onChangeVideoAutoplayEvent(ChangeVideoAutoplayEvent changeVideoAutoplayEvent) { if (getPostAdapter() != null) { boolean autoplay = false; if (changeVideoAutoplayEvent.autoplay.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ALWAYS_ON)) { autoplay = true; } else if (changeVideoAutoplayEvent.autoplay.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ON_WIFI)) { autoplay = Utils.isConnectedToWifi(mActivity); } getPostAdapter().setAutoplay(autoplay); refreshAdapter(); } } @Subscribe public void onChangeAutoplayNsfwVideosEvent(ChangeAutoplayNsfwVideosEvent changeAutoplayNsfwVideosEvent) { if (getPostAdapter() != null) { getPostAdapter().setAutoplayNsfwVideos(changeAutoplayNsfwVideosEvent.autoplayNsfwVideos); refreshAdapter(); } } @Subscribe public void onChangeMuteAutoplayingVideosEvent(ChangeMuteAutoplayingVideosEvent changeMuteAutoplayingVideosEvent) { if (getPostAdapter() != null) { getPostAdapter().setMuteAutoplayingVideos(changeMuteAutoplayingVideosEvent.muteAutoplayingVideos); refreshAdapter(); } } @Subscribe public void onChangeRememberMutingOptionInPostFeedEvent(ChangeRememberMutingOptionInPostFeedEvent event) { rememberMutingOptionInPostFeed = event.rememberMutingOptionInPostFeedEvent; if (!event.rememberMutingOptionInPostFeedEvent) { masterMutingOption = null; } } @Subscribe public void onChangeSwipeActionEvent(ChangeSwipeActionEvent changeSwipeActionEvent) { swipeRightAction = changeSwipeActionEvent.swipeRightAction == -1 ? swipeRightAction : changeSwipeActionEvent.swipeRightAction; swipeLeftAction = changeSwipeActionEvent.swipeLeftAction == -1 ? swipeLeftAction : changeSwipeActionEvent.swipeLeftAction; initializeSwipeActionDrawable(); } @Subscribe public void onChangeSwipeActionThresholdEvent(ChangeSwipeActionThresholdEvent changeSwipeActionThresholdEvent) { swipeActionThreshold = changeSwipeActionThresholdEvent.swipeActionThreshold; } @Subscribe public void onChangeVibrateWhenActionTriggeredEvent(ChangeVibrateWhenActionTriggeredEvent changeVibrateWhenActionTriggeredEvent) { vibrateWhenActionTriggered = changeVibrateWhenActionTriggeredEvent.vibrateWhenActionTriggered; } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { if (getPostAdapter() != null) { String autoplay = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_AUTOPLAY, SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_NEVER); String dataSavingMode = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); boolean stateChanged = false; if (autoplay.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ON_WIFI)) { getPostAdapter().setAutoplay(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_WIFI); stateChanged = true; } if (dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { getPostAdapter().setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); stateChanged = true; } if (stateChanged) { refreshAdapter(); } } } @Subscribe public void onShowThumbnailOnTheLeftInCompactLayoutEvent(ShowThumbnailOnTheLeftInCompactLayoutEvent showThumbnailOnTheLeftInCompactLayoutEvent) { if (getPostAdapter() != null) { getPostAdapter().setShowThumbnailOnTheLeftInCompactLayout(showThumbnailOnTheLeftInCompactLayoutEvent.showThumbnailOnTheLeftInCompactLayout); refreshAdapter(); } } @Subscribe public void onChangeStartAutoplayVisibleAreaOffsetEvent(ChangeStartAutoplayVisibleAreaOffsetEvent changeStartAutoplayVisibleAreaOffsetEvent) { if (getPostAdapter() != null) { getPostAdapter().setStartAutoplayVisibleAreaOffset(changeStartAutoplayVisibleAreaOffsetEvent.startAutoplayVisibleAreaOffset); refreshAdapter(); } } @Subscribe public void onChangeMuteNSFWVideoEvent(ChangeMuteNSFWVideoEvent changeMuteNSFWVideoEvent) { if (getPostAdapter() != null) { getPostAdapter().setMuteNSFWVideo(changeMuteNSFWVideoEvent.muteNSFWVideo); refreshAdapter(); } } @Subscribe public void onChangeEnableSwipeActionSwitchEvent(ChangeEnableSwipeActionSwitchEvent changeEnableSwipeActionSwitchEvent) { if (getNColumns(getResources()) == 1 && touchHelper != null) { swipeActionEnabled = changeEnableSwipeActionSwitchEvent.enableSwipeAction; if (changeEnableSwipeActionSwitchEvent.enableSwipeAction) { touchHelper.attachToRecyclerView(getPostRecyclerView(), 1); } else { touchHelper.attachToRecyclerView(null, 1); } } } @Subscribe public void onChangePullToRefreshEvent(ChangePullToRefreshEvent changePullToRefreshEvent) { getSwipeRefreshLayout().setEnabled(changePullToRefreshEvent.pullToRefresh); } @Subscribe public void onChangeLongPressToHideToolbarInCompactLayoutEvent(ChangeLongPressToHideToolbarInCompactLayoutEvent changeLongPressToHideToolbarInCompactLayoutEvent) { if (getPostAdapter() != null) { getPostAdapter().setLongPressToHideToolbarInCompactLayout(changeLongPressToHideToolbarInCompactLayoutEvent.longPressToHideToolbarInCompactLayout); refreshAdapter(); } } @Subscribe public void onChangeCompactLayoutToolbarHiddenByDefaultEvent(ChangeCompactLayoutToolbarHiddenByDefaultEvent changeCompactLayoutToolbarHiddenByDefaultEvent) { if (getPostAdapter() != null) { getPostAdapter().setCompactLayoutToolbarHiddenByDefault(changeCompactLayoutToolbarHiddenByDefaultEvent.compactLayoutToolbarHiddenByDefault); refreshAdapter(); } } @Subscribe public void onChangeDataSavingModeEvent(ChangeDataSavingModeEvent changeDataSavingModeEvent) { if (getPostAdapter() != null) { boolean dataSavingMode = false; if (changeDataSavingModeEvent.dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { dataSavingMode = Utils.isConnectedToCellularData(mActivity); } else if (changeDataSavingModeEvent.dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ALWAYS)) { dataSavingMode = true; } getPostAdapter().setDataSavingMode(dataSavingMode); refreshAdapter(); } } @Subscribe public void onChangeDisableImagePreviewEvent(ChangeDisableImagePreviewEvent changeDisableImagePreviewEvent) { if (getPostAdapter() != null) { getPostAdapter().setDisableImagePreview(changeDisableImagePreviewEvent.disableImagePreview); refreshAdapter(); } } @Subscribe public void onChangeOnlyDisablePreviewInVideoAndGifPostsEvent(ChangeOnlyDisablePreviewInVideoAndGifPostsEvent changeOnlyDisablePreviewInVideoAndGifPostsEvent) { if (getPostAdapter() != null) { getPostAdapter().setOnlyDisablePreviewInVideoPosts(changeOnlyDisablePreviewInVideoAndGifPostsEvent.onlyDisablePreviewInVideoAndGifPosts); refreshAdapter(); } } @Subscribe public void onChangeHidePostTypeEvent(ChangeHidePostTypeEvent event) { if (getPostAdapter() != null) { getPostAdapter().setHidePostType(event.hidePostType); refreshAdapter(); } } @Subscribe public void onChangeHidePostFlairEvent(ChangeHidePostFlairEvent event) { if (getPostAdapter() != null) { getPostAdapter().setHidePostFlair(event.hidePostFlair); refreshAdapter(); } } @Subscribe public void onChangeHideSubredditAndUserEvent(ChangeHideSubredditAndUserPrefixEvent event) { if (getPostAdapter() != null) { getPostAdapter().setHideSubredditAndUserPrefix(event.hideSubredditAndUserPrefix); refreshAdapter(); } } @Subscribe public void onChangeHideTheNumberOfVotesEvent(ChangeHideTheNumberOfVotesEvent event) { if (getPostAdapter() != null) { getPostAdapter().setHideTheNumberOfVotes(event.hideTheNumberOfVotes); refreshAdapter(); } } @Subscribe public void onChangeHideTheNumberOfCommentsEvent(ChangeHideTheNumberOfCommentsEvent event) { if (getPostAdapter() != null) { getPostAdapter().setHideTheNumberOfComments(event.hideTheNumberOfComments); refreshAdapter(); } } @Subscribe public void onChangeFixedHeightPreviewCardEvent(ChangeFixedHeightPreviewInCardEvent event) { if (getPostAdapter() != null) { getPostAdapter().setFixedHeightPreviewInCard(event.fixedHeightPreviewInCard); refreshAdapter(); } } @Subscribe public void onChangeHideTextPostContentEvent(ChangeHideTextPostContent event) { if (getPostAdapter() != null) { getPostAdapter().setHideTextPostContent(event.hideTextPostContent); refreshAdapter(); } } @Subscribe public void onChangePostFeedMaxResolutionEvent(ChangePostFeedMaxResolutionEvent event) { if (getPostAdapter() != null) { getPostAdapter().setPostFeedMaxResolution(event.postFeedMaxResolution); refreshAdapter(); } } @Subscribe public void onChangeEasierToWatchInFullScreenEvent(ChangeEasierToWatchInFullScreenEvent event) { if (getPostAdapter() != null) { getPostAdapter().setEasierToWatchInFullScreen(event.easierToWatchInFullScreen); } } protected static abstract class LazyModeRunnable implements Runnable { private int currentPosition = -1; int getCurrentPosition() { return currentPosition; } void setCurrentPosition(int currentPosition) { this.currentPosition = currentPosition; } void incrementCurrentPosition() { currentPosition++; } void resetOldPosition() { currentPosition = -1; } } protected static class StaggeredGridLayoutManagerItemOffsetDecoration extends RecyclerView.ItemDecoration { private final int mHalfOffset; private final int mQuaterOffset; private final int mCard3HorizontalSpace; private final int mCard3VerticalSpace; private final int mNColumns; StaggeredGridLayoutManagerItemOffsetDecoration(int itemOffset, int nColumns) { mNColumns = nColumns; mCard3HorizontalSpace = -itemOffset / 4 * 3; mCard3VerticalSpace = -itemOffset / 4; mHalfOffset = itemOffset / 2; mQuaterOffset = itemOffset / 4; } StaggeredGridLayoutManagerItemOffsetDecoration(@NonNull Context context, @DimenRes int itemOffsetId, int nColumns) { this(context.getResources().getDimensionPixelSize(itemOffsetId), nColumns); } @OptIn(markerClass = UnstableApi.class) @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); StaggeredGridLayoutManager.LayoutParams layoutParams = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); int spanIndex = layoutParams.getSpanIndex(); if (parent.getAdapter() != null) { RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view); if (viewHolder instanceof PostRecyclerViewAdapter.PostMaterial3CardVideoAutoplayViewHolder || viewHolder instanceof PostRecyclerViewAdapter.PostMaterial3CardVideoAutoplayLegacyControllerViewHolder || viewHolder instanceof PostRecyclerViewAdapter.PostMaterial3CardWithPreviewViewHolder || viewHolder instanceof PostRecyclerViewAdapter.PostMaterial3CardGalleryTypeViewHolder || viewHolder instanceof PostRecyclerViewAdapter.PostMaterial3CardTextTypeViewHolder) { if (mNColumns == 2) { if (spanIndex == 0) { outRect.set(-mHalfOffset, mCard3VerticalSpace, mCard3HorizontalSpace, mCard3VerticalSpace); } else { outRect.set(mCard3HorizontalSpace, mCard3VerticalSpace, -mHalfOffset, mCard3VerticalSpace); } } else if (mNColumns == 3) { if (spanIndex == 0) { outRect.set(-mHalfOffset, mCard3VerticalSpace, mCard3HorizontalSpace, mCard3VerticalSpace); } else if (spanIndex == 1) { outRect.set(mCard3HorizontalSpace, mCard3VerticalSpace, mCard3HorizontalSpace, mCard3VerticalSpace); } else { outRect.set(mCard3HorizontalSpace, mCard3VerticalSpace, -mHalfOffset, mCard3VerticalSpace); } } return; } } if (mNColumns == 2) { if (spanIndex == 0) { outRect.set(mHalfOffset, 0, mQuaterOffset, 0); } else { outRect.set(mQuaterOffset, 0, mHalfOffset, 0); } } else if (mNColumns == 3) { if (spanIndex == 0) { outRect.set(mHalfOffset, 0, mQuaterOffset, 0); } else if (spanIndex == 1) { outRect.set(mQuaterOffset, 0, mQuaterOffset, 0); } else { outRect.set(mQuaterOffset, 0, mHalfOffset, 0); } } } } public interface LoadIconListener { void loadIconSuccess(String subredditOrUserName, String iconUrl); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/SidebarFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.core.MarkwonTheme; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.asynctasks.InsertSubredditData; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CopyTextBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentSidebarBinding; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.markdown.CustomMarkwonAdapter; import ml.docilealligator.infinityforreddit.markdown.EmoteCloseBracketInlineProcessor; import ml.docilealligator.infinityforreddit.markdown.EmotePlugin; import ml.docilealligator.infinityforreddit.markdown.EvenBetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry; import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin; import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils; import ml.docilealligator.infinityforreddit.subreddit.FetchSubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditViewModel; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; public class SidebarFragment extends Fragment { public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public SubredditViewModel mSubredditViewModel; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private ViewSubredditDetailActivity mActivity; private String subredditName; private LinearLayoutManagerBugFixed linearLayoutManager; private int markdownColor; private String sidebarDescription; private EmotePlugin emotePlugin; private ImageAndGifEntry imageAndGifEntry; private FragmentSidebarBinding binding; public SidebarFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSidebarBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); EventBus.getDefault().register(this); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.markdownRecyclerViewSidebarFragment.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); } subredditName = getArguments().getString(EXTRA_SUBREDDIT_NAME); if (subredditName == null) { Toast.makeText(mActivity, R.string.error_getting_subreddit_name, Toast.LENGTH_SHORT).show(); return binding.getRoot(); } binding.swipeRefreshLayoutSidebarFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutSidebarFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); markdownColor = mCustomThemeWrapper.getPrimaryTextColor(); int spoilerBackgroundColor = markdownColor | 0xFF000000; MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (mActivity.contentTypeface != null) { textView.setTypeface(mActivity.contentTypeface); } textView.setTextColor(markdownColor); textView.setOnLongClickListener(view -> { if (sidebarDescription != null && !sidebarDescription.equals("") && textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { Bundle bundle = new Bundle(); bundle.putString(CopyTextBottomSheetFragment.EXTRA_MARKDOWN, sidebarDescription); CopyTextBottomSheetFragment copyTextBottomSheetFragment = new CopyTextBottomSheetFragment(); copyTextBottomSheetFragment.setArguments(bundle); copyTextBottomSheetFragment.show(getChildFragmentManager(), copyTextBottomSheetFragment.getTag()); } return true; }); } @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { builder.linkColor(mCustomThemeWrapper.getLinkColor()); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); Uri uri = Uri.parse(link); intent.setData(uri); startActivity(intent); }); } }; EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener = (textView, url) -> { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(getChildFragmentManager(), null); return true; }; EmoteCloseBracketInlineProcessor emoteCloseBracketInlineProcessor = new EmoteCloseBracketInlineProcessor(); emotePlugin = EmotePlugin.create(mActivity, SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, mediaMetadata -> { Intent imageIntent = new Intent(mActivity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, subredditName); imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); ImageAndGifPlugin imageAndGifPlugin = new ImageAndGifPlugin(); imageAndGifEntry = new ImageAndGifEntry(mActivity, Glide.with(this), SharedPreferencesUtils.EMBEDDED_MEDIA_ALL, (mediaMetadata, commentId, postId) -> { Intent imageIntent = new Intent(mActivity, ViewImageOrGifActivity.class); if (mediaMetadata.isGIF) { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_GIF_URL_KEY, mediaMetadata.original.url); } else { imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_IMAGE_URL_KEY, mediaMetadata.original.url); } imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_SUBREDDIT_OR_USERNAME_KEY, subredditName); imageIntent.putExtra(ViewImageOrGifActivity.EXTRA_FILE_NAME_KEY, mediaMetadata.fileName); }); Markwon markwon = MarkdownUtils.createFullRedditMarkwon(mActivity, miscPlugin, emoteCloseBracketInlineProcessor, emotePlugin, imageAndGifPlugin, markdownColor, spoilerBackgroundColor, onLinkLongClickListener); CustomMarkwonAdapter markwonAdapter = MarkdownUtils.createCustomTablesAndImagesAdapter(mActivity, imageAndGifEntry); markwonAdapter.setOnLongClickListener(view -> { if (sidebarDescription != null && !sidebarDescription.equals("")) { Bundle bundle = new Bundle(); bundle.putString(CopyTextBottomSheetFragment.EXTRA_MARKDOWN, sidebarDescription); CopyTextBottomSheetFragment copyTextBottomSheetFragment = new CopyTextBottomSheetFragment(); copyTextBottomSheetFragment.setArguments(bundle); copyTextBottomSheetFragment.show(getChildFragmentManager(), copyTextBottomSheetFragment.getTag()); } return true; }); linearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.markdownRecyclerViewSidebarFragment.setLayoutManager(linearLayoutManager); binding.markdownRecyclerViewSidebarFragment.setAdapter(markwonAdapter); binding.markdownRecyclerViewSidebarFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { mActivity.contentScrollDown(); } else if (dy < 0) { mActivity.contentScrollUp(); } } }); mSubredditViewModel = new ViewModelProvider(mActivity, new SubredditViewModel.Factory(mRedditDataRoomDatabase, subredditName)) .get(SubredditViewModel.class); mSubredditViewModel.getSubredditLiveData().observe(getViewLifecycleOwner(), subredditData -> { if (subredditData != null) { sidebarDescription = subredditData.getSidebarDescription(); if (sidebarDescription != null && !sidebarDescription.equals("")) { markwonAdapter.setMarkdown(markwon, sidebarDescription); // noinspection NotifyDataSetChanged markwonAdapter.notifyDataSetChanged(); } } else { fetchSubredditData(); } }); binding.swipeRefreshLayoutSidebarFragment.setOnRefreshListener(this::fetchSubredditData); return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (ViewSubredditDetailActivity) context; } @Override public void onDestroy() { EventBus.getDefault().unregister(this); super.onDestroy(); } public void fetchSubredditData() { binding.swipeRefreshLayoutSidebarFragment.setRefreshing(true); Handler handler = new Handler(); FetchSubredditData.fetchSubredditData(mExecutor, handler, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? null : mOauthRetrofit, mRetrofit, subredditName, mActivity.accessToken, new FetchSubredditData.FetchSubredditDataListener() { @Override public void onFetchSubredditDataSuccess(SubredditData subredditData, int nCurrentOnlineSubscribers) { binding.swipeRefreshLayoutSidebarFragment.setRefreshing(false); InsertSubredditData.insertSubredditData(mExecutor, handler, mRedditDataRoomDatabase, subredditData, () -> binding.swipeRefreshLayoutSidebarFragment.setRefreshing(false)); } @Override public void onFetchSubredditDataFail(boolean isQuarantined) { binding.swipeRefreshLayoutSidebarFragment.setRefreshing(false); Toast.makeText(mActivity, R.string.cannot_fetch_sidebar, Toast.LENGTH_SHORT).show(); } }); } public void goBackToTop() { if (linearLayoutManager != null) { linearLayoutManager.scrollToPositionWithOffset(0, 0); } } public void setDataSavingMode(boolean dataSavingMode) { emotePlugin.setDataSavingMode(dataSavingMode); imageAndGifEntry.setDataSavingMode(dataSavingMode); } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { String dataSavingMode = mActivity.getDefaultSharedPreferences().getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { if (emotePlugin != null) { emotePlugin.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } if (imageAndGifEntry != null) { imageAndGifEntry.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/SubredditListingFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.adapters.SubredditListingRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentSubredditListingBinding; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subreddit.SubredditListingViewModel; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; /** * A simple {@link Fragment} subclass. */ public class SubredditListingFragment extends Fragment implements FragmentCommunicator { public static final String EXTRA_QUERY = "EQ"; public static final String EXTRA_IS_GETTING_SUBREDDIT_INFO = "EIGSI"; public static final String EXTRA_IS_MULTI_SELECTION = "EIMS"; SubredditListingViewModel mSubredditListingViewModel; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private LinearLayoutManagerBugFixed mLinearLayoutManager; private SubredditListingRecyclerViewAdapter mAdapter; private BaseActivity mActivity; private SortType sortType; private FragmentSubredditListingBinding binding; public SubredditListingFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentSubredditListingBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); applyTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewSubredditListingFragment.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); //binding.recyclerViewSubredditListingFragment.setPadding(0, 0, 0, mActivity.getNavBarHeight()); }/* else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { binding.recyclerViewSubredditListingFragment.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); } }*/ mLinearLayoutManager = new LinearLayoutManagerBugFixed(getActivity()); binding.recyclerViewSubredditListingFragment.setLayoutManager(mLinearLayoutManager); String query = getArguments().getString(EXTRA_QUERY); boolean isGettingSubredditInfo = getArguments().getBoolean(EXTRA_IS_GETTING_SUBREDDIT_INFO); String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_SEARCH_SUBREDDIT, SortType.Type.RELEVANCE.value); sortType = new SortType(SortType.Type.valueOf(sort.toUpperCase())); boolean nsfw = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.NSFW_BASE, false); mAdapter = new SubredditListingRecyclerViewAdapter(mActivity, mExecutor, mOauthRetrofit, mRetrofit, mCustomThemeWrapper, mActivity.accessToken, mActivity.accountName, mRedditDataRoomDatabase, getArguments().getBoolean(EXTRA_IS_MULTI_SELECTION, false), new SubredditListingRecyclerViewAdapter.Callback() { @Override public void retryLoadingMore() { mSubredditListingViewModel.retryLoadingMore(); } @Override public void subredditSelected(String subredditName, String iconUrl) { if (isGettingSubredditInfo) { Intent returnIntent = new Intent(); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME, subredditName); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON, iconUrl); mActivity.setResult(Activity.RESULT_OK, returnIntent); mActivity.finish(); } else { Intent intent = new Intent(mActivity, ViewSubredditDetailActivity.class); intent.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subredditName); mActivity.startActivity(intent); } } }); binding.recyclerViewSubredditListingFragment.setAdapter(mAdapter); if (mActivity instanceof RecyclerViewContentScrollingInterface) { binding.recyclerViewSubredditListingFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); } else if (dy < 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); } } }); } SubredditListingViewModel.Factory factory = new SubredditListingViewModel.Factory(mExecutor, new Handler(), mOauthRetrofit, query, sortType, mActivity.accessToken, mActivity.accountName, nsfw); mSubredditListingViewModel = new ViewModelProvider(this, factory).get(SubredditListingViewModel.class); mSubredditListingViewModel.getSubreddits().observe(getViewLifecycleOwner(), subredditData -> mAdapter.submitList(subredditData)); mSubredditListingViewModel.hasSubredditLiveData().observe(getViewLifecycleOwner(), hasSubreddit -> { binding.swipeRefreshLayoutSubredditListingFragment.setRefreshing(false); if (hasSubreddit) { binding.fetchSubredditListingInfoLinearLayoutSubredditListingFragment.setVisibility(View.GONE); } else { binding.fetchSubredditListingInfoLinearLayoutSubredditListingFragment.setOnClickListener(null); showErrorView(R.string.no_subreddits); } }); mSubredditListingViewModel.getInitialLoadingState().observe(getViewLifecycleOwner(), networkState -> { if (networkState.getStatus().equals(NetworkState.Status.SUCCESS)) { binding.swipeRefreshLayoutSubredditListingFragment.setRefreshing(false); } else if (networkState.getStatus().equals(NetworkState.Status.FAILED)) { binding.swipeRefreshLayoutSubredditListingFragment.setRefreshing(false); binding.fetchSubredditListingInfoLinearLayoutSubredditListingFragment.setOnClickListener(view -> refresh()); showErrorView(R.string.search_subreddits_error); } else { binding.swipeRefreshLayoutSubredditListingFragment.setRefreshing(true); } }); mSubredditListingViewModel.getPaginationNetworkState().observe(getViewLifecycleOwner(), networkState -> { mAdapter.setNetworkState(networkState); }); binding.swipeRefreshLayoutSubredditListingFragment.setOnRefreshListener(() -> mSubredditListingViewModel.refresh()); return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } private void showErrorView(int stringResId) { if (getActivity() != null && isAdded()) { binding.swipeRefreshLayoutSubredditListingFragment.setRefreshing(false); binding.fetchSubredditListingInfoLinearLayoutSubredditListingFragment.setVisibility(View.VISIBLE); binding.fetchSubredditListingInfoTextViewSubredditListingFragment.setText(stringResId); } } public void changeSortType(SortType sortType) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_SEARCH_SUBREDDIT, sortType.getType().name()).apply(); mSubredditListingViewModel.changeSortType(sortType); this.sortType = sortType; } @Override public void refresh() { binding.fetchSubredditListingInfoLinearLayoutSubredditListingFragment.setVisibility(View.GONE); mSubredditListingViewModel.refresh(); mAdapter.setNetworkState(null); } @Override public void applyTheme() { binding.swipeRefreshLayoutSubredditListingFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutSubredditListingFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); binding.fetchSubredditListingInfoTextViewSubredditListingFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchSubredditListingInfoTextViewSubredditListingFragment.setTypeface(mActivity.contentTypeface); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } public SortType getSortType() { return sortType; } public ArrayList getSelectedSubredditNames() { if (mSubredditListingViewModel != null) { List allSubreddits = mSubredditListingViewModel.getSubreddits().getValue(); if (allSubreddits == null) { return null; } ArrayList selectedSubreddits = new ArrayList<>(); for (SubredditData s : allSubreddits) { if (s.isSelected()) { selectedSubreddits.add(s); } } return selectedSubreddits; } return null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/SubscribedSubredditsListingFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import me.zhanghai.android.fastscroll.FastScrollerBuilder; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.SubscribedThingListingActivity; import ml.docilealligator.infinityforreddit.adapters.SubscribedSubredditsRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentSubscribedSubredditsListingBinding; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditViewModel; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; /** * A simple {@link Fragment} subclass. */ public class SubscribedSubredditsListingFragment extends Fragment implements FragmentCommunicator { public static final String EXTRA_ACCOUNT_PROFILE_IMAGE_URL = "EAPIU"; public static final String EXTRA_IS_SUBREDDIT_SELECTION = "EISS"; public static final String EXTRA_EXTRA_CLEAR_SELECTION = "EECS"; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; public SubscribedSubredditViewModel mSubscribedSubredditViewModel; private BaseActivity mActivity; private RequestManager mGlide; private LinearLayoutManagerBugFixed mLinearLayoutManager; private FragmentSubscribedSubredditsListingBinding binding; public SubscribedSubredditsListingFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSubscribedSubredditsListingBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); applyTheme(); if ((mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge())) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewSubscribedSubredditsListingFragment.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); //binding.recyclerViewSubscribedSubredditsListingFragment.setPadding(0, 0, 0, mActivity.getNavBarHeight()); }/* else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { Resources resources = getResources(); int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { binding.recyclerViewSubscribedSubredditsListingFragment.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); } }*/ if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setEnabled(false); } mGlide = Glide.with(this); mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewSubscribedSubredditsListingFragment.setLayoutManager(mLinearLayoutManager); SubscribedSubredditsRecyclerViewAdapter adapter; if (getArguments().getBoolean(EXTRA_IS_SUBREDDIT_SELECTION)) { adapter = new SubscribedSubredditsRecyclerViewAdapter(mActivity, mExecutor, mOauthRetrofit, mRedditDataRoomDatabase, mCustomThemeWrapper, mActivity.accessToken, mActivity.accountName, getArguments().getBoolean(EXTRA_EXTRA_CLEAR_SELECTION), (name, iconUrl, subredditIsUser) -> { Intent returnIntent = new Intent(); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME, name); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON, iconUrl); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, subredditIsUser ? SelectThingReturnKey.THING_TYPE.USER : SelectThingReturnKey.THING_TYPE.SUBREDDIT); mActivity.setResult(Activity.RESULT_OK, returnIntent); mActivity.finish(); }); } else { adapter = new SubscribedSubredditsRecyclerViewAdapter(mActivity, mExecutor, mOauthRetrofit, mRedditDataRoomDatabase, mCustomThemeWrapper, mActivity.accessToken, mActivity.accountName); } binding.recyclerViewSubscribedSubredditsListingFragment.setAdapter(adapter); new FastScrollerBuilder(binding.recyclerViewSubscribedSubredditsListingFragment).useMd2Style().build(); mSubscribedSubredditViewModel = new ViewModelProvider(this, new SubscribedSubredditViewModel.Factory(mRedditDataRoomDatabase, mActivity.accountName)) .get(SubscribedSubredditViewModel.class); mSubscribedSubredditViewModel.getAllSubscribedSubreddits().observe(getViewLifecycleOwner(), subscribedSubredditData -> { binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setRefreshing(false); if (subscribedSubredditData == null || subscribedSubredditData.size() == 0) { binding.recyclerViewSubscribedSubredditsListingFragment.setVisibility(View.GONE); binding.noSubscriptionsLinearLayoutSubredditsListingFragment.setVisibility(View.VISIBLE); } else { binding.noSubscriptionsLinearLayoutSubredditsListingFragment.setVisibility(View.GONE); binding.recyclerViewSubscribedSubredditsListingFragment.setVisibility(View.VISIBLE); mGlide.clear(binding.noSubscriptionsImageViewSubredditsListingFragment); } if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { adapter.addUser(mActivity.accountName, getArguments().getString(EXTRA_ACCOUNT_PROFILE_IMAGE_URL)); } adapter.setSubscribedSubreddits(subscribedSubredditData); }); mSubscribedSubredditViewModel.getAllFavoriteSubscribedSubreddits().observe(getViewLifecycleOwner(), favoriteSubscribedSubredditData -> { binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setRefreshing(false); if (favoriteSubscribedSubredditData != null && favoriteSubscribedSubredditData.size() > 0) { binding.noSubscriptionsLinearLayoutSubredditsListingFragment.setVisibility(View.GONE); binding.recyclerViewSubscribedSubredditsListingFragment.setVisibility(View.VISIBLE); mGlide.clear(binding.noSubscriptionsImageViewSubredditsListingFragment); } adapter.setFavoriteSubscribedSubreddits(favoriteSubscribedSubredditData); }); return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } @Override public void stopRefreshProgressbar() { binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setRefreshing(false); } @Override public void applyTheme() { if (mActivity instanceof SubscribedThingListingActivity) { binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setOnRefreshListener(() -> ((SubscribedThingListingActivity) mActivity).loadSubscriptions(true)); binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); } else { binding.swipeRefreshLayoutSubscribedSubredditsListingFragment.setEnabled(false); } binding.errorTextViewSubscribedSubredditsListingFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.errorTextViewSubscribedSubredditsListingFragment.setTypeface(mActivity.contentTypeface); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } public void changeSearchQuery(String searchQuery) { mSubscribedSubredditViewModel.setSearchQuery(searchQuery); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ThemePreviewCommentsFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.CustomThemePreviewActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.databinding.FragmentThemePreviewCommentsBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class ThemePreviewCommentsFragment extends Fragment { private FragmentThemePreviewCommentsBinding binding; private CustomThemePreviewActivity activity; public ThemePreviewCommentsFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentThemePreviewCommentsBinding.inflate(inflater, container, false); CustomTheme customTheme = activity.getCustomTheme(); Drawable expandDrawable = Utils.getTintedDrawable(activity, R.drawable.ic_expand_more_grey_24dp, customTheme.commentIconAndInfoColor); binding.linearLayoutThemePreviewCommentsFragment.setBackgroundColor(customTheme.commentBackgroundColor); binding.authorTypeImageViewThemePreviewCommentsFragment.setColorFilter(customTheme.moderator, android.graphics.PorterDuff.Mode.SRC_IN); binding.authorTextViewThemePreviewCommentsFragment.setTextColor(customTheme.moderator); binding.commentTimeTextViewThemePreviewCommentsFragment.setTextColor(customTheme.secondaryTextColor); binding.commentMarkdownViewThemePreviewCommentsFragment.setTextColor(customTheme.commentColor); binding.authorFlairTextViewThemePreviewCommentsFragment.setTextColor(customTheme.authorFlairTextColor); binding.dividerThemePreviewCommentsFragment.setBackgroundColor(customTheme.dividerColor); binding.upvoteButtonThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.upvoteButtonThemePreviewCommentsFragment.setTextColor(customTheme.commentIconAndInfoColor); binding.downvoteButtonThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.moreButtonThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.expandButtonThemePreviewCommentsFragment.setCompoundDrawablesWithIntrinsicBounds(expandDrawable, null, null, null); binding.saveButtonThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.replyButtonThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.linearLayoutAwardBackgroundThemePreviewCommentsFragment.setBackgroundColor(customTheme.awardedCommentBackgroundColor); binding.authorTypeImageViewAwardBackgroundThemePreviewCommentsFragment.setColorFilter(customTheme.moderator, android.graphics.PorterDuff.Mode.SRC_IN); binding.authorTextViewAwardBackgroundThemePreviewCommentsFragment.setTextColor(customTheme.moderator); binding.commentTimeTextViewAwardBackgroundThemePreviewCommentsFragment.setTextColor(customTheme.secondaryTextColor); binding.commentMarkdownViewAwardBackgroundThemePreviewCommentsFragment.setTextColor(customTheme.commentColor); binding.authorFlairTextViewAwardBackgroundThemePreviewCommentsFragment.setTextColor(customTheme.authorFlairTextColor); binding.dividerAwardBackgroundThemePreviewCommentsFragment.setBackgroundColor(customTheme.dividerColor); binding.upvoteButtonAwardBackgroundThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.upvoteButtonAwardBackgroundThemePreviewCommentsFragment.setTextColor(customTheme.commentIconAndInfoColor); binding.downvoteButtonAwardBackgroundThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.moreButtonAwardBackgroundThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.expandButtonAwardBackgroundThemePreviewCommentsFragment.setCompoundDrawablesWithIntrinsicBounds(expandDrawable, null, null, null); binding.saveButtonAwardBackgroundThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.replyButtonAwardBackgroundThemePreviewCommentsFragment.setIconTint(ColorStateList.valueOf(customTheme.commentIconAndInfoColor)); binding.linearLayoutFullyCollapsedThemePreviewCommentsFragment.setBackgroundColor(customTheme.fullyCollapsedCommentBackgroundColor); binding.authorTextViewFullyCollapsedThemePreviewCommentsFragment.setTextColor(customTheme.username); binding.scoreTextViewFullyCollapsedThemePreviewCommentsFragment.setTextColor(customTheme.secondaryTextColor); binding.timeTextViewFullyCollapsedThemePreviewCommentsFragment.setTextColor(customTheme.secondaryTextColor); if (activity.typeface != null) { binding.authorTextViewThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.commentTimeTextViewThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.authorFlairTextViewThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.upvoteButtonThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.authorTextViewAwardBackgroundThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.commentTimeTextViewAwardBackgroundThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.authorFlairTextViewAwardBackgroundThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.upvoteButtonAwardBackgroundThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.authorTextViewFullyCollapsedThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.scoreTextViewFullyCollapsedThemePreviewCommentsFragment.setTypeface(activity.typeface); binding.timeTextViewFullyCollapsedThemePreviewCommentsFragment.setTypeface(activity.typeface); } if (activity.contentTypeface != null) { binding.commentMarkdownViewThemePreviewCommentsFragment.setTypeface(activity.contentTypeface); binding.commentMarkdownViewAwardBackgroundThemePreviewCommentsFragment.setTypeface(activity.contentTypeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (CustomThemePreviewActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ThemePreviewPostsFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import com.bumptech.glide.Glide; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.CustomThemePreviewActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.databinding.FragmentThemePreviewPostsBinding; /** * A simple {@link Fragment} subclass. */ public class ThemePreviewPostsFragment extends Fragment { private FragmentThemePreviewPostsBinding binding; private CustomThemePreviewActivity activity; public ThemePreviewPostsFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentThemePreviewPostsBinding.inflate(inflater, container, false); CustomTheme customTheme = activity.getCustomTheme(); binding.cardViewThemePreviewPostsFragment.setBackgroundTintList(ColorStateList.valueOf(customTheme.cardViewBackgroundColor)); Glide.with(this).load(R.drawable.subreddit_default_icon) .transform(new RoundedCornersTransformation(72, 0)) .into(binding.iconGifImageViewThemePreviewPostsFragment); binding.subredditNameTextViewThemePreviewPostsFragment.setTextColor(customTheme.subreddit); binding.userTextViewThemePreviewPostsFragment.setTextColor(customTheme.username); binding.postTimeTextViewBestThemePreviewPostsFragment.setTextColor(customTheme.secondaryTextColor); binding.titleTextViewBestThemePreviewPostsFragment.setTextColor(customTheme.postTitleColor); binding.contentTextViewThemePreviewPostsFragment.setTextColor(customTheme.postContentColor); binding.stickiedPostImageViewThemePreviewPostsFragment.setColorFilter(customTheme.stickiedPostIconTint, PorterDuff.Mode.SRC_IN); binding.typeTextViewThemePreviewPostsFragment.setBackgroundColor(customTheme.postTypeBackgroundColor); binding.typeTextViewThemePreviewPostsFragment.setBorderColor(customTheme.postTypeBackgroundColor); binding.typeTextViewThemePreviewPostsFragment.setTextColor(customTheme.postTypeTextColor); binding.spoilerCustomTextViewThemePreviewPostsFragment.setBackgroundColor(customTheme.spoilerBackgroundColor); binding.spoilerCustomTextViewThemePreviewPostsFragment.setBorderColor(customTheme.spoilerBackgroundColor); binding.spoilerCustomTextViewThemePreviewPostsFragment.setTextColor(customTheme.spoilerTextColor); binding.nsfwTextViewThemePreviewPostsFragment.setBackgroundColor(customTheme.nsfwBackgroundColor); binding.nsfwTextViewThemePreviewPostsFragment.setBorderColor(customTheme.nsfwBackgroundColor); binding.nsfwTextViewThemePreviewPostsFragment.setTextColor(customTheme.nsfwTextColor); binding.flairCustomTextViewThemePreviewPostsFragment.setBackgroundColor(customTheme.flairBackgroundColor); binding.flairCustomTextViewThemePreviewPostsFragment.setBorderColor(customTheme.flairBackgroundColor); binding.flairCustomTextViewThemePreviewPostsFragment.setTextColor(customTheme.flairTextColor); binding.awardsTextViewThemePreviewPostsFragment.setBackgroundColor(customTheme.awardsBackgroundColor); binding.awardsTextViewThemePreviewPostsFragment.setBorderColor(customTheme.awardsBackgroundColor); binding.awardsTextViewThemePreviewPostsFragment.setTextColor(customTheme.awardsTextColor); binding.archivedImageViewThemePreviewPostsFragment.setColorFilter(customTheme.archivedTint, PorterDuff.Mode.SRC_IN); binding.lockedImageViewThemePreviewPostsFragment.setColorFilter(customTheme.lockedIconTint, PorterDuff.Mode.SRC_IN); binding.crosspostImageViewThemePreviewPostsFragment.setColorFilter(customTheme.crosspostIconTint, PorterDuff.Mode.SRC_IN); binding.linkTextViewThemePreviewPostsFragment.setTextColor(customTheme.secondaryTextColor); binding.progressBarThemePreviewPostsFragment.setIndicatorColor(customTheme.colorAccent); binding.imageViewNoPreviewLinkThemePreviewPostsFragment.setBackgroundColor(customTheme.noPreviewPostTypeBackgroundColor); binding.upvoteButtonThemePreviewPostsFragment.setIconTint(ColorStateList.valueOf(customTheme.postIconAndInfoColor)); binding.upvoteButtonThemePreviewPostsFragment.setTextColor(customTheme.postIconAndInfoColor); binding.downvoteButtonThemePreviewPostsFragment.setIconTint(ColorStateList.valueOf(customTheme.postIconAndInfoColor)); binding.commentsCountButtonThemePreviewPostsFragment.setTextColor(customTheme.postIconAndInfoColor); binding.commentsCountButtonThemePreviewPostsFragment.setIconTint(ColorStateList.valueOf(customTheme.postIconAndInfoColor)); binding.saveButtonThemePreviewPostsFragment.setIconTint(ColorStateList.valueOf(customTheme.postIconAndInfoColor)); binding.shareButtonThemePreviewPostsFragment.setIconTint(ColorStateList.valueOf(customTheme.postIconAndInfoColor)); if (activity.typeface != null) { binding.subredditNameTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.userTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.postTimeTextViewBestThemePreviewPostsFragment.setTypeface(activity.typeface); binding.typeTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.spoilerCustomTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.nsfwTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.flairCustomTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.awardsTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.linkTextViewThemePreviewPostsFragment.setTypeface(activity.typeface); binding.upvoteButtonThemePreviewPostsFragment.setTypeface(activity.typeface); binding.commentsCountButtonThemePreviewPostsFragment.setTypeface(activity.typeface); } if (activity.titleTypeface != null) { binding.titleTextViewBestThemePreviewPostsFragment.setTypeface(activity.titleTypeface); } if (activity.contentTypeface != null) { binding.contentTextViewThemePreviewPostsFragment.setTypeface(activity.contentTypeface); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (CustomThemePreviewActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/UserListingFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.adapters.UserListingRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentUserListingBinding; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.user.UserData; import ml.docilealligator.infinityforreddit.user.UserListingViewModel; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Retrofit; /** * A simple {@link Fragment} subclass. */ public class UserListingFragment extends Fragment implements FragmentCommunicator { public static final String EXTRA_QUERY = "EQ"; public static final String EXTRA_IS_GETTING_USER_INFO = "EIGUI"; public static final String EXTRA_IS_MULTI_SELECTION = "EIMS"; UserListingViewModel mUserListingViewModel; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private LinearLayoutManagerBugFixed mLinearLayoutManager; private String mQuery; private UserListingRecyclerViewAdapter mAdapter; private BaseActivity mActivity; private SortType sortType; private FragmentUserListingBinding binding; public UserListingFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentUserListingBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); applyTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.recyclerViewUserListingFragment.setPadding( 0, 0, 0, allInsets.bottom ); return WindowInsetsCompat.CONSUMED; } }); //binding.recyclerViewUserListingFragment.setPadding(0, 0, 0, mActivity.getNavBarHeight()); }/* else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mSharedPreferences.getBoolean(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY, true)) { int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); if (navBarResourceId > 0) { binding.recyclerViewUserListingFragment.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); } }*/ mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); binding.recyclerViewUserListingFragment.setLayoutManager(mLinearLayoutManager); mQuery = getArguments().getString(EXTRA_QUERY); boolean isGettingUserInfo = getArguments().getBoolean(EXTRA_IS_GETTING_USER_INFO); String sort = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_SEARCH_USER, SortType.Type.RELEVANCE.value); sortType = new SortType(SortType.Type.valueOf(sort.toUpperCase())); boolean nsfw = !mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) && mNsfwAndSpoilerSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.NSFW_BASE, false); mAdapter = new UserListingRecyclerViewAdapter(mActivity, mExecutor, mOauthRetrofit, mRetrofit, mCustomThemeWrapper, mActivity.accessToken, mActivity.accountName, mRedditDataRoomDatabase, getArguments().getBoolean(EXTRA_IS_MULTI_SELECTION, false), new UserListingRecyclerViewAdapter.Callback() { @Override public void retryLoadingMore() { mUserListingViewModel.retryLoadingMore(); } @Override public void userSelected(String username, String iconUrl) { if (isGettingUserInfo) { Intent returnIntent = new Intent(); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME, username); returnIntent.putExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_ICON, iconUrl); mActivity.setResult(Activity.RESULT_OK, returnIntent); mActivity.finish(); } else { Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, username); mActivity.startActivity(intent); } } }); binding.recyclerViewUserListingFragment.setAdapter(mAdapter); if (mActivity instanceof RecyclerViewContentScrollingInterface) { binding.recyclerViewUserListingFragment.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); } else if (dy < 0) { ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); } } }); } UserListingViewModel.Factory factory = new UserListingViewModel.Factory(mExecutor, mActivity.mHandler, mRetrofit, mQuery, sortType, nsfw); mUserListingViewModel = new ViewModelProvider(this, factory).get(UserListingViewModel.class); mUserListingViewModel.getUsers().observe(getViewLifecycleOwner(), UserData -> mAdapter.submitList(UserData)); mUserListingViewModel.hasUser().observe(getViewLifecycleOwner(), hasUser -> { binding.swipeRefreshLayoutUserListingFragment.setRefreshing(false); if (hasUser) { binding.fetchUserListingInfoLinearLayoutUserListingFragment.setVisibility(View.GONE); } else { binding.fetchUserListingInfoLinearLayoutUserListingFragment.setOnClickListener(view -> { //Do nothing }); showErrorView(R.string.no_users); } }); mUserListingViewModel.getInitialLoadingState().observe(getViewLifecycleOwner(), networkState -> { if (networkState.getStatus().equals(NetworkState.Status.SUCCESS)) { binding.swipeRefreshLayoutUserListingFragment.setRefreshing(false); } else if (networkState.getStatus().equals(NetworkState.Status.FAILED)) { binding.swipeRefreshLayoutUserListingFragment.setRefreshing(false); binding.fetchUserListingInfoLinearLayoutUserListingFragment.setOnClickListener(view -> refresh()); showErrorView(R.string.search_users_error); } else { binding.swipeRefreshLayoutUserListingFragment.setRefreshing(true); } }); mUserListingViewModel.getPaginationNetworkState().observe(getViewLifecycleOwner(), networkState -> { mAdapter.setNetworkState(networkState); }); binding.swipeRefreshLayoutUserListingFragment.setOnRefreshListener(() -> mUserListingViewModel.refresh()); return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } private void showErrorView(int stringResId) { if (getActivity() != null && isAdded()) { binding.swipeRefreshLayoutUserListingFragment.setRefreshing(false); binding.fetchUserListingInfoLinearLayoutUserListingFragment.setVisibility(View.VISIBLE); binding.fetchUserListingInfoTextViewUserListingFragment.setText(stringResId); } } public void changeSortType(SortType sortType) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_SEARCH_USER, sortType.getType().name()).apply(); mUserListingViewModel.changeSortType(sortType); this.sortType = sortType; } @Override public void refresh() { binding.fetchUserListingInfoLinearLayoutUserListingFragment.setVisibility(View.GONE); mUserListingViewModel.refresh(); mAdapter.setNetworkState(null); } @Override public void applyTheme() { binding.swipeRefreshLayoutUserListingFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutUserListingFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); binding.fetchUserListingInfoTextViewUserListingFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchUserListingInfoTextViewUserListingFragment.setTypeface(mActivity.contentTypeface); } } public void goBackToTop() { if (mLinearLayoutManager != null) { mLinearLayoutManager.scrollToPositionWithOffset(0, 0); } } public SortType getSortType() { return sortType; } public ArrayList getSelectedUsernames() { if (mUserListingViewModel != null) { List allUsers = mUserListingViewModel.getUsers().getValue(); if (allUsers != null) { ArrayList selectedUsernames = new ArrayList<>(); for (UserData u : allUsers) { if (u.isSelected()) { selectedUsernames.add(u.getName()); } } return selectedUsernames; } else { return null; } } return null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewImgurImageFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.Manifest; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; 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.CustomTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.davemorrissey.labs.subscaleview.ImageSource; import java.io.File; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; import ml.docilealligator.infinityforreddit.activities.ViewImgurMediaActivity; import ml.docilealligator.infinityforreddit.asynctasks.SaveBitmapImageToFile; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentViewImgurImageBinding; import ml.docilealligator.infinityforreddit.post.ImgurMedia; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ViewImgurImageFragment extends Fragment { public static final String EXTRA_IMGUR_IMAGES = "EII"; public static final String EXTRA_INDEX = "EI"; public static final String EXTRA_MEDIA_COUNT = "EMC"; private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0; @Inject Executor mExecutor; @Inject @Named("default") SharedPreferences mSharedPreferences; private ViewImgurMediaActivity activity; private RequestManager glide; private ImgurMedia imgurMedia; private boolean isDownloading = false; private boolean isActionBarHidden = false; private FragmentViewImgurImageBinding binding; public ViewImgurImageFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentViewImgurImageBinding.inflate(inflater, container, false); ((Infinity) activity.getApplication()).getAppComponent().inject(this); setHasOptionsMenu(true); imgurMedia = getArguments().getParcelable(EXTRA_IMGUR_IMAGES); glide = Glide.with(activity); loadImage(); binding.imageViewViewImgurImageFragment.setOnClickListener(view -> { if (isActionBarHidden) { activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); isActionBarHidden = false; if (activity.isUseBottomAppBar()) { binding.bottomNavigationViewImgurImageFragment.setVisibility(View.VISIBLE); } } else { activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); isActionBarHidden = true; if (activity.isUseBottomAppBar()) { binding.bottomNavigationViewImgurImageFragment.setVisibility(View.GONE); } } }); binding.imageViewViewImgurImageFragment.setMinimumDpi(80); binding.imageViewViewImgurImageFragment.setDoubleTapZoomDpi(240); binding.imageViewViewImgurImageFragment.resetScaleAndCenter(); binding.loadImageErrorLinearLayoutViewImgurImageFragment.setOnClickListener(view -> { binding.progressBarViewImgurImageFragment.setVisibility(View.VISIBLE); binding.loadImageErrorLinearLayoutViewImgurImageFragment.setVisibility(View.GONE); loadImage(); }); if (activity.isUseBottomAppBar()) { binding.bottomNavigationViewImgurImageFragment.setVisibility(View.VISIBLE); binding.titleTextViewViewImgurImageFragment.setText(getString(R.string.view_imgur_media_activity_image_label, getArguments().getInt(EXTRA_INDEX) + 1, getArguments().getInt(EXTRA_MEDIA_COUNT))); binding.downloadImageViewViewImgurImageFragment.setOnClickListener(view -> { if (isDownloading) { return; } isDownloading = true; requestPermissionAndDownload(); }); binding.shareImageViewViewImgurImageFragment.setOnClickListener(view -> { shareImage(); }); binding.wallpaperImageViewViewImgurImageFragment.setOnClickListener(view -> { setWallpaper(); }); } return binding.getRoot(); } private void loadImage() { glide.asBitmap().load(imgurMedia.getLink()).listener(new RequestListener() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { binding.progressBarViewImgurImageFragment.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewImgurImageFragment.setVisibility(View.VISIBLE); return false; } @Override public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { binding.progressBarViewImgurImageFragment.setVisibility(View.GONE); return false; } }).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { binding.imageViewViewImgurImageFragment.setImage(ImageSource.bitmap(resource)); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.view_imgur_image_fragment, menu); for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); Utils.setTitleWithCustomFontToMenuItem(activity.typeface, item, null); } super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_download_view_imgur_image_fragment) { if (isDownloading) { return false; } isDownloading = true; requestPermissionAndDownload(); return true; } else if (itemId == R.id.action_share_view_imgur_image_fragment) { shareImage(); return true; } else if (itemId == R.id.action_set_wallpaper_view_imgur_image_fragment) { setWallpaper(); return true; } return false; } private void requestPermissionAndDownload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted download(); } } else { download(); } } private void download() { isDownloading = false; String subredditName = getArguments().getString(ViewImgurMediaActivity.EXTRA_SUBREDDIT_NAME); boolean isNsfw = getArguments().getBoolean(ViewImgurMediaActivity.EXTRA_IS_NSFW); String title = getArguments().getString(ViewImgurMediaActivity.EXTRA_POST_TITLE_KEY); Log.d("ImgurDownload", "ViewImgurImageFragment - Starting download of image, isNsfw=" + isNsfw); // Check if download location is set String downloadLocation; int mediaType = imgurMedia.getType() == ImgurMedia.TYPE_VIDEO ? DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO : DownloadMediaService.EXTRA_MEDIA_TYPE_IMAGE; Log.d("ImgurDownload", "Media type: " + mediaType + " (" + (mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO ? "VIDEO" : mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_GIF ? "GIF" : "IMAGE") + ")"); String defaultSharedPrefsFile = "ml.docilealligator.infinityforreddit_preferences"; // Check for the location in both SharedPreferences - this will help identify the issue String imageLoc1 = mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String imageLoc2 = activity.getSharedPreferences(SharedPreferencesUtils.SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE) .getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String imageLoc3 = activity.getSharedPreferences(defaultSharedPrefsFile, Context.MODE_PRIVATE) .getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); Log.d("ImgurDownload", "Image location from injected prefs: " + (imageLoc1.isEmpty() ? "EMPTY" : imageLoc1)); Log.d("ImgurDownload", "Image location from SHARED_PREFERENCES_FILE: " + (imageLoc2.isEmpty() ? "EMPTY" : imageLoc2)); Log.d("ImgurDownload", "Image location from default_preferences: " + (imageLoc3.isEmpty() ? "EMPTY" : imageLoc3)); if (isNsfw && mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); Log.d("ImgurDownload", "Using NSFW download location: " + (downloadLocation.isEmpty() ? "EMPTY" : "SET")); } else { if (mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); Log.d("ImgurDownload", "Using VIDEO download location: " + (downloadLocation.isEmpty() ? "EMPTY" : "SET")); } else { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); Log.d("ImgurDownload", "Using IMAGE download location: " + (downloadLocation.isEmpty() ? "EMPTY" : "SET")); // If the location is empty, try the other SharedPreferences if (downloadLocation == null || downloadLocation.isEmpty()) { downloadLocation = imageLoc2.isEmpty() ? imageLoc3 : imageLoc2; Log.d("ImgurDownload", "Image location was empty, trying backup location: " + (downloadLocation.isEmpty() ? "EMPTY" : downloadLocation)); } } } if (downloadLocation == null || downloadLocation.isEmpty()) { Log.e("ImgurDownload", "Download location not set!"); Toast.makeText(activity, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return; } //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadMediaService.constructJobInfo(activity, 5000000, imgurMedia, subredditName, isNsfw, title); ((JobScheduler) activity.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); Log.d("ImgurDownload", "Download job scheduled successfully for single image"); Toast.makeText(activity, R.string.download_started, Toast.LENGTH_SHORT).show(); } private void shareImage() { glide.asBitmap().load(imgurMedia.getLink()).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { File cacheDir = Utils.getCacheDir(activity); if (cacheDir != null) { Toast.makeText(activity, R.string.save_image_first, Toast.LENGTH_SHORT).show(); SaveBitmapImageToFile.SaveBitmapImageToFile(mExecutor, new Handler(), resource, cacheDir.getPath(), imgurMedia.getFileName(), new SaveBitmapImageToFile.SaveBitmapImageToFileListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", imageFile); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.setType("image/*"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); } @Override public void saveFailed() { Toast.makeText(activity, R.string.cannot_save_image, Toast.LENGTH_SHORT).show(); } }); } else { Toast.makeText(activity, R.string.cannot_get_storage, Toast.LENGTH_SHORT).show(); } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); } private void setWallpaper() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { SetAsWallpaperBottomSheetFragment setAsWallpaperBottomSheetFragment = new SetAsWallpaperBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putInt(SetAsWallpaperBottomSheetFragment.EXTRA_VIEW_PAGER_POSITION, activity.getCurrentPagePosition()); setAsWallpaperBottomSheetFragment.setArguments(bundle); setAsWallpaperBottomSheetFragment.show(activity.getSupportFragmentManager(), setAsWallpaperBottomSheetFragment.getTag()); } else { ((SetAsWallpaperCallback) activity).setToBoth(activity.getCurrentPagePosition()); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(activity, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); isDownloading = false; } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED && isDownloading) { download(); } } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (ViewImgurMediaActivity) context; } @Override public void onDestroyView() { super.onDestroyView(); glide.clear(binding.imageViewViewImgurImageFragment); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewImgurVideoFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.Manifest; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.OptIn; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.Fragment; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Tracks; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.SimpleCache; import androidx.media3.datasource.okhttp.OkHttpDataSource; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.ProgressiveMediaSource; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.ui.PlayerView; import com.google.android.material.button.MaterialButton; import com.google.common.collect.ImmutableList; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.ViewImgurMediaActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PlaybackSpeedBottomSheetFragment; import ml.docilealligator.infinityforreddit.databinding.FragmentViewImgurVideoBinding; import ml.docilealligator.infinityforreddit.post.ImgurMedia; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import okhttp3.OkHttpClient; public class ViewImgurVideoFragment extends Fragment { public static final String EXTRA_IMGUR_VIDEO = "EIV"; public static final String EXTRA_INDEX = "EI"; public static final String EXTRA_MEDIA_COUNT = "EMC"; private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0; private static final String IS_MUTE_STATE = "IMS"; private static final String POSITION_STATE = "PS"; private static final String PLAYBACK_SPEED_STATE = "PSS"; private ViewImgurMediaActivity activity; private ImgurMedia imgurMedia; private ExoPlayer player; private DataSource.Factory dataSourceFactory; private boolean wasPlaying = false; private boolean isMute = false; private boolean isDownloading = false; private int playbackSpeed = 100; @Inject @Named("media3") OkHttpClient mOkHttpClient; @Inject @Named("default") SharedPreferences mSharedPreferences; @UnstableApi @Inject SimpleCache mSimpleCache; private ViewImgurVideoFragmentBindingAdapter binding; public ViewImgurVideoFragment() { // Required empty public constructor } @OptIn(markerClass = UnstableApi.class) @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = new ViewImgurVideoFragmentBindingAdapter(FragmentViewImgurVideoBinding.inflate(inflater, container, false)); ((Infinity) activity.getApplication()).getAppComponent().inject(this); setHasOptionsMenu(true); if (activity.typeface != null) { binding.getTitleTextView().setTypeface(activity.typeface); } activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); imgurMedia = getArguments().getParcelable(EXTRA_IMGUR_VIDEO); if (!mSharedPreferences.getBoolean(SharedPreferencesUtils.VIDEO_PLAYER_IGNORE_NAV_BAR, false)) { if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT || getResources().getBoolean(R.bool.isTablet)) { //Set player controller bottom margin in order to display it above the navbar int resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); LinearLayout controllerLinearLayout = binding.getRoot().findViewById(R.id.linear_layout_exo_playback_control_view); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) controllerLinearLayout.getLayoutParams(); params.bottomMargin = getResources().getDimensionPixelSize(resourceId); } else { //Set player controller right margin in order to display it above the navbar int resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); LinearLayout controllerLinearLayout = binding.getRoot().findViewById(R.id.linear_layout_exo_playback_control_view); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) controllerLinearLayout.getLayoutParams(); params.rightMargin = getResources().getDimensionPixelSize(resourceId); } } binding.getRoot().setControllerVisibilityListener(new PlayerView.ControllerVisibilityListener() { @Override public void onVisibilityChanged(int visibility) { switch (visibility) { case View.GONE: activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); break; case View.VISIBLE: activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } } }); TrackSelector trackSelector = new DefaultTrackSelector(activity); player = new ExoPlayer.Builder(activity) .setTrackSelector(trackSelector) .setRenderersFactory(new DefaultRenderersFactory(activity).setEnableDecoderFallback(true)) .build(); binding.getRoot().setPlayer(player); dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) .setUpstreamDataSourceFactory(new OkHttpDataSource.Factory(mOkHttpClient).setUserAgent(APIUtils.USER_AGENT)); player.prepare(); player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(imgurMedia.getLink()))); if (savedInstanceState != null) { playbackSpeed = savedInstanceState.getInt(PLAYBACK_SPEED_STATE); } setPlaybackSpeed(Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.DEFAULT_PLAYBACK_SPEED, "100"))); preparePlayer(savedInstanceState); binding.getTitleTextView().setText(getString(R.string.view_imgur_media_activity_video_label, getArguments().getInt(EXTRA_INDEX) + 1, getArguments().getInt(EXTRA_MEDIA_COUNT))); if (activity.isUseBottomAppBar()) { binding.getBottomAppBar().setVisibility(View.VISIBLE); binding.getBackButton().setOnClickListener(view -> { activity.finish(); }); binding.getDownloadButton().setOnClickListener(view -> { if (isDownloading) { return; } isDownloading = true; requestPermissionAndDownload(); }); binding.getPlaybackSpeedButton().setOnClickListener(view -> { changePlaybackSpeed(); }); } return binding.getRoot(); } private void changePlaybackSpeed() { PlaybackSpeedBottomSheetFragment playbackSpeedBottomSheetFragment = new PlaybackSpeedBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putInt(PlaybackSpeedBottomSheetFragment.EXTRA_PLAYBACK_SPEED, playbackSpeed); playbackSpeedBottomSheetFragment.setArguments(bundle); playbackSpeedBottomSheetFragment.show(getChildFragmentManager(), playbackSpeedBottomSheetFragment.getTag()); } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.view_imgur_video_fragment, menu); for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); Utils.setTitleWithCustomFontToMenuItem(activity.typeface, item, null); } super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_download_view_imgur_video_fragment) { isDownloading = true; requestPermissionAndDownload(); return true; } else if (item.getItemId() == R.id.action_playback_speed_view_imgur_video_fragment) { changePlaybackSpeed(); return true; } return false; } public void setPlaybackSpeed(int speed100X) { this.playbackSpeed = speed100X; player.setPlaybackParameters(new PlaybackParameters((float) (speed100X / 100.0))); } private void requestPermissionAndDownload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted download(); } } else { download(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(activity, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); isDownloading = false; } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED && isDownloading) { download(); } } } private void download() { isDownloading = false; String subredditName = getArguments().getString(ViewImgurMediaActivity.EXTRA_SUBREDDIT_NAME); boolean isNsfw = getArguments().getBoolean(ViewImgurMediaActivity.EXTRA_IS_NSFW); String title = getArguments().getString(ViewImgurMediaActivity.EXTRA_POST_TITLE_KEY); // Check if download location is set String downloadLocation; // Imgur videos should be saved to video location if (isNsfw && mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); } else { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); } if (downloadLocation == null || downloadLocation.isEmpty()) { Toast.makeText(activity, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return; } //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadMediaService.constructJobInfo(activity, 5000000, imgurMedia, subredditName, isNsfw, title); ((JobScheduler) activity.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); Toast.makeText(activity, R.string.download_started, Toast.LENGTH_SHORT).show(); } private void preparePlayer(Bundle savedInstanceState) { if (mSharedPreferences.getBoolean(SharedPreferencesUtils.LOOP_VIDEO, true)) { player.setRepeatMode(Player.REPEAT_MODE_ALL); } else { player.setRepeatMode(Player.REPEAT_MODE_OFF); } wasPlaying = true; boolean muteVideo = mSharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_VIDEO, false); if (savedInstanceState != null) { long position = savedInstanceState.getLong(POSITION_STATE); if (position > 0) { player.seekTo(position); } isMute = savedInstanceState.getBoolean(IS_MUTE_STATE); if (isMute) { player.setVolume(0f); binding.getMuteButton().setIconResource(R.drawable.ic_mute_24dp); } else { player.setVolume(1f); binding.getMuteButton().setIconResource(R.drawable.ic_unmute_24dp); } } else if (muteVideo) { isMute = true; player.setVolume(0f); binding.getMuteButton().setIconResource(R.drawable.ic_mute_24dp); } else { binding.getMuteButton().setIconResource(R.drawable.ic_unmute_24dp); } MaterialButton playPauseButton = binding.getRoot().findViewById(R.id.exo_play_pause_button_exo_playback_control_view); Drawable playDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_play_arrow_24dp, null); Drawable pauseDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_pause_24dp, null); playPauseButton.setOnClickListener(view -> { Util.handlePlayPauseButtonAction(player); }); player.addListener(new Player.Listener() { @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { if (events.containsAny( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { playPauseButton.setIcon(Util.shouldShowPlayButton(player) ? playDrawable : pauseDrawable); } } @Override public void onTracksChanged(@NonNull Tracks tracks) { ImmutableList trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { for (int i = 0; i < trackGroups.size(); i++) { String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { binding.getMuteButton().setVisibility(View.VISIBLE); binding.getMuteButton().setOnClickListener(view -> { if (isMute) { isMute = false; player.setVolume(1f); binding.getMuteButton().setIconResource(R.drawable.ic_unmute_24dp); } else { isMute = true; player.setVolume(0f); binding.getMuteButton().setIconResource(R.drawable.ic_mute_24dp); } }); break; } } } else { binding.getMuteButton().setVisibility(View.GONE); } } }); } @Override public void onResume() { super.onResume(); if (wasPlaying) { player.setPlayWhenReady(true); } } @Override public void onPause() { super.onPause(); wasPlaying = player.getPlayWhenReady(); player.setPlayWhenReady(false); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(IS_MUTE_STATE, isMute); outState.putLong(POSITION_STATE, player.getCurrentPosition()); outState.putInt(PLAYBACK_SPEED_STATE, playbackSpeed); } @Override public void onDestroy() { super.onDestroy(); player.seekToDefaultPosition(); player.stop(); player.release(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (ViewImgurMediaActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewImgurVideoFragmentBindingAdapter.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.widget.TextView; import androidx.media3.ui.PlayerView; import com.google.android.material.bottomappbar.BottomAppBar; import com.google.android.material.button.MaterialButton; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.databinding.FragmentViewImgurVideoBinding; class ViewImgurVideoFragmentBindingAdapter { private FragmentViewImgurVideoBinding binding; private MaterialButton muteButton; private BottomAppBar bottomAppBar; private TextView titleTextView; private MaterialButton backButton; private MaterialButton downloadButton; private MaterialButton playbackSpeedButton; ViewImgurVideoFragmentBindingAdapter(FragmentViewImgurVideoBinding binding) { this.binding = binding; muteButton = binding.getRoot().findViewById(R.id.mute_exo_playback_control_view); bottomAppBar = binding.getRoot().findViewById(R.id.bottom_navigation_exo_playback_control_view); titleTextView = binding.getRoot().findViewById(R.id.title_text_view_exo_playback_control_view); backButton = binding.getRoot().findViewById(R.id.back_button_exo_playback_control_view); downloadButton = binding.getRoot().findViewById(R.id.download_image_view_exo_playback_control_view); playbackSpeedButton = binding.getRoot().findViewById(R.id.playback_speed_image_view_exo_playback_control_view); } PlayerView getRoot() { return binding.getRoot(); } MaterialButton getMuteButton() { return muteButton; } BottomAppBar getBottomAppBar() { return bottomAppBar; } TextView getTitleTextView() { return titleTextView; } MaterialButton getBackButton() { return backButton; } MaterialButton getDownloadButton() { return downloadButton; } MaterialButton getPlaybackSpeedButton() { return playbackSpeedButton; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import static ml.docilealligator.infinityforreddit.activities.CommentActivity.RETURN_EXTRA_COMMENT_DATA_KEY; import static ml.docilealligator.infinityforreddit.activities.CommentActivity.WRITE_COMMENT_REQUEST_CODE; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.menu.MenuItemImpl; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.Insets; import androidx.core.view.MenuItemCompat; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.ConcatAdapter; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; import androidx.transition.AutoTransition; import androidx.transition.TransitionManager; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.evernote.android.state.State; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.livefront.bridge.Bridge; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import ml.docilealligator.infinityforreddit.CommentModerationActionHandler; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.PostModerationActionHandler; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.CommentActivity; import ml.docilealligator.infinityforreddit.activities.EditPostActivity; import ml.docilealligator.infinityforreddit.activities.PostFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.activities.ReportActivity; import ml.docilealligator.infinityforreddit.activities.SubmitCrosspostActivity; import ml.docilealligator.infinityforreddit.activities.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.adapters.CommentsRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.adapters.PostDetailRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.bottomsheetfragments.FlairBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PostCommentSortTypeBottomSheetFragment; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.comment.FetchComment; import ml.docilealligator.infinityforreddit.comment.ParseComment; import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter; import ml.docilealligator.infinityforreddit.commentfilter.FetchCommentFilter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.AdjustableTouchSlopItemTouchHelper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentViewPostDetailBinding; import ml.docilealligator.infinityforreddit.events.ChangeNSFWBlurEvent; import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent; import ml.docilealligator.infinityforreddit.events.ChangeSpoilerBlurEvent; import ml.docilealligator.infinityforreddit.events.FlairSelectedEvent; import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostDetailFragment; import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostList; import ml.docilealligator.infinityforreddit.message.ReadMessage; import ml.docilealligator.infinityforreddit.post.FetchPost; import ml.docilealligator.infinityforreddit.post.HidePost; import ml.docilealligator.infinityforreddit.post.ParsePost; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.readpost.InsertReadPost; import ml.docilealligator.infinityforreddit.readpost.ReadPostsUtils; import ml.docilealligator.infinityforreddit.subreddit.FetchSubredditData; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.thing.DeleteThing; import ml.docilealligator.infinityforreddit.thing.ReplyNotificationsToggle; import ml.docilealligator.infinityforreddit.thing.SaveThing; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.CommentScrollPositionCache; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; import ml.docilealligator.infinityforreddit.viewmodels.ViewPostDetailActivityViewModel; import ml.docilealligator.infinityforreddit.viewmodels.ViewPostDetailFragmentViewModel; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ViewPostDetailFragment extends Fragment implements FragmentCommunicator, PostModerationActionHandler, CommentModerationActionHandler { public static final String EXTRA_POST_DATA = "EPD"; public static final String EXTRA_POST_ID = "EPI"; public static final String EXTRA_SINGLE_COMMENT_ID = "ESCI"; public static final String EXTRA_CONTEXT_NUMBER = "ECN"; public static final String EXTRA_MESSAGE_FULLNAME = "EMF"; public static final String EXTRA_POST_LIST_POSITION = "EPLP"; private static final int EDIT_POST_REQUEST_CODE = 2; private static final String SCROLL_POSITION_STATE = "SPS"; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("redgifs") Retrofit mRedgifsRetrofit; @Inject Provider mStreamableApiProvider; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences mNsfwAndSpoilerSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("post_details") SharedPreferences mPostDetailsSharedPreferences; @Inject @Named("post_history") SharedPreferences mPostHistorySharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject ExoCreator mExoCreator; @Inject Executor mExecutor; @State Post mPost; @State boolean isLoadingMoreChildren = false; @State boolean isRefreshing = false; @State boolean isSingleCommentThreadMode = false; @State ArrayList comments; @State ArrayList children; @State boolean loadMoreChildrenSuccess = true; @State boolean hasMoreChildren; @State boolean isFetchingComments = false; @State String mMessageFullname; @State SortType.Type sortType; @State boolean mRespectSubredditRecommendedSortType; @State long viewPostDetailFragmentId; @State boolean commentFilterFetched; @State CommentFilter mCommentFilter; private ViewPostDetailActivity mActivity; private RequestManager mGlide; private Locale mLocale; private Menu mMenu; private int postListPosition = -1; private String mSingleCommentId; private String mContextNumber; private boolean showToast = false; private boolean mIsSmoothScrolling = false; private boolean mLockFab; private boolean mSwipeUpToHideFab; private boolean mExpandChildren; private boolean mSeparatePostAndComments = false; private boolean mMarkPostsAsRead; private ConcatAdapter mConcatAdapter; private PostDetailRecyclerViewAdapter mPostAdapter; private CommentsRecyclerViewAdapter mCommentsAdapter; private RecyclerView.SmoothScroller mSmoothScroller; private Drawable mSavedIcon; private Drawable mUnsavedIcon; private ColorDrawable backgroundSwipeRight; private ColorDrawable backgroundSwipeLeft; private Drawable drawableSwipeRight; private Drawable drawableSwipeLeft; private int swipeLeftAction; private int swipeRightAction; private float swipeActionThreshold; private AdjustableTouchSlopItemTouchHelper touchHelper; private boolean shouldSwipeBack; private int scrollPosition; private FragmentViewPostDetailBinding binding; private RecyclerView mCommentsRecyclerView; public ViewPostDetailFragmentViewModel viewPostDetailFragmentViewModel; private boolean mRememberCommentScrollPosition; private int mPendingScrollPositionRestore = -1; private boolean mScrollPositionRestored = false; public ViewPostDetailFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentViewPostDetailBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); setHasOptionsMenu(true); Bridge.restoreInstanceState(this, savedInstanceState); EventBus.getDefault().register(this); applyTheme(); binding.postDetailRecyclerViewViewPostDetailFragment.addOnWindowFocusChangedListener(this::onWindowFocusChanged); mSavedIcon = getMenuItemIcon(R.drawable.ic_bookmark_toolbar_24dp); mUnsavedIcon = getMenuItemIcon(R.drawable.ic_bookmark_border_toolbar_24dp); mCommentsRecyclerView = binding.commentsRecyclerViewViewPostDetailFragment; if (!((mPostDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.SEPARATE_POST_AND_COMMENTS_IN_LANDSCAPE_MODE, true) && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) || (mPostDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.SEPARATE_POST_AND_COMMENTS_IN_PORTRAIT_MODE, false) && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT))) { if (mCommentsRecyclerView != null) { mCommentsRecyclerView.setVisibility(View.GONE); mCommentsRecyclerView = null; } } else { mSeparatePostAndComments = true; boolean swapSides = mPostDetailsSharedPreferences.getBoolean( SharedPreferencesUtils.SWAP_POST_AND_COMMENTS_IN_SPLIT_MODE, false); if (swapSides) { ViewGroup parent = (ViewGroup) mCommentsRecyclerView.getParent(); if (parent instanceof LinearLayout) { View postView = binding.postDetailRecyclerViewViewPostDetailFragment; parent.removeView(postView); parent.addView(postView); } } } if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.postDetailRecyclerViewViewPostDetailFragment.setPadding( 0, 0, 0, (int) Utils.convertDpToPixel(144, mActivity) + allInsets.bottom ); if (mCommentsRecyclerView != null) { mCommentsRecyclerView.setPadding(0, 0, 0, (int) Utils.convertDpToPixel(144, mActivity) + allInsets.bottom); } return WindowInsetsCompat.CONSUMED; } }); /*binding.postDetailRecyclerViewViewPostDetailFragment.setPadding(0, 0, 0, activity.getNavBarHeight() + binding.postDetailRecyclerViewViewPostDetailFragment.getPaddingBottom()); if (mCommentsRecyclerView != null) { mCommentsRecyclerView.setPadding(0, 0, 0, activity.getNavBarHeight() + mCommentsRecyclerView.getPaddingBottom()); }*/ showToast = true; } mLockFab = mSharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_JUMP_TO_NEXT_TOP_LEVEL_COMMENT_BUTTON, false); mSwipeUpToHideFab = mSharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_UP_TO_HIDE_JUMP_TO_NEXT_TOP_LEVEL_COMMENT_BUTTON, false); mExpandChildren = !mSharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_TOP_LEVEL_COMMENTS_FIRST, false); mMarkPostsAsRead = mPostHistorySharedPreferences.getBoolean(mActivity.accountName + SharedPreferencesUtils.MARK_POSTS_AS_READ_BASE, false); mRememberCommentScrollPosition = mSharedPreferences.getBoolean(SharedPreferencesUtils.REMEMBER_COMMENT_SCROLL_POSITION, false); if (savedInstanceState == null) { mRespectSubredditRecommendedSortType = mSharedPreferences.getBoolean(SharedPreferencesUtils.RESPECT_SUBREDDIT_RECOMMENDED_COMMENT_SORT_TYPE, false); viewPostDetailFragmentId = System.currentTimeMillis(); } else { scrollPosition = savedInstanceState.getInt(SCROLL_POSITION_STATE); // if the scrollPosition < 0 do nothing if (scrollPosition >= 0) { if (getResources().getBoolean(R.bool.isTablet)) { boolean separatePortrait = mPostDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.SEPARATE_POST_AND_COMMENTS_IN_PORTRAIT_MODE, true); boolean separateLandscape = mPostDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.SEPARATE_POST_AND_COMMENTS_IN_LANDSCAPE_MODE, true); if (separatePortrait != separateLandscape) { if (mCommentsRecyclerView != null) { //restore the position for commentsadapter scrollPosition--; mCommentsRecyclerView.scrollToPosition(scrollPosition); } else { // restore the position for binding.postDetailRecyclerViewViewPostDetailFragment scrollPosition++; binding.postDetailRecyclerViewViewPostDetailFragment.scrollToPosition(scrollPosition); } } } else { if (mSeparatePostAndComments) { if (mCommentsRecyclerView != null) { scrollPosition--; mCommentsRecyclerView.scrollToPosition(scrollPosition); } } else { if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { if (mPostDetailsSharedPreferences.getBoolean(SharedPreferencesUtils.SEPARATE_POST_AND_COMMENTS_IN_LANDSCAPE_MODE, true)) { scrollPosition++; binding.postDetailRecyclerViewViewPostDetailFragment.scrollToPosition(scrollPosition); } } } } } } mGlide = Glide.with(this); mLocale = getResources().getConfiguration().locale; if (children != null && children.size() > 0) { (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (!mIsSmoothScrolling && !mLockFab) { if (!recyclerView.canScrollVertically(1)) { mActivity.hideFab(); } else { if (dy > 0) { if (mSwipeUpToHideFab) { mActivity.showFab(); } else { mActivity.hideFab(); } } else { if (mSwipeUpToHideFab) { mActivity.hideFab(); } else { mActivity.showFab(); } } } } if (!isLoadingMoreChildren && loadMoreChildrenSuccess) { int visibleItemCount = (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager().getChildCount(); int totalItemCount = (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager().getItemCount(); int firstVisibleItemPosition = ((LinearLayoutManagerBugFixed) (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager()).findFirstVisibleItemPosition(); if (mCommentsAdapter != null && mCommentsAdapter.getItemCount() >= 1 && (visibleItemCount + firstVisibleItemPosition >= totalItemCount) && firstVisibleItemPosition >= 0) { fetchMoreComments(); } } } @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { mIsSmoothScrolling = false; } } }); } else { (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (!mIsSmoothScrolling && !mLockFab) { if (!recyclerView.canScrollVertically(1)) { mActivity.hideFab(); } else { if (dy > 0) { if (mSwipeUpToHideFab) { mActivity.showFab(); } else { mActivity.hideFab(); } } else { if (mSwipeUpToHideFab) { mActivity.hideFab(); } else { mActivity.showFab(); } } } } } @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { mIsSmoothScrolling = false; } } }); } boolean enableSwipeAction = mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SWIPE_ACTION, false); boolean vibrateWhenActionTriggered = mSharedPreferences.getBoolean(SharedPreferencesUtils.VIBRATE_WHEN_ACTION_TRIGGERED, true); swipeActionThreshold = Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_ACTION_THRESHOLD, "0.3")); swipeRightAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_RIGHT_ACTION, "1")); swipeLeftAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_LEFT_ACTION, "0")); initializeSwipeActionDrawable(); touchHelper = new AdjustableTouchSlopItemTouchHelper(new AdjustableTouchSlopItemTouchHelper.Callback() { boolean exceedThreshold = false; @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { if (!(viewHolder instanceof CommentsRecyclerViewAdapter.CommentBaseViewHolder)) { return makeMovementFlags(0, 0); } int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; return makeMovementFlags(0, swipeFlags); } @Override public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { return false; } @Override public boolean isItemViewSwipeEnabled() { return true; } @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {} @Override public int convertToAbsoluteDirection(int flags, int layoutDirection) { if (shouldSwipeBack) { shouldSwipeBack = false; return 0; } return super.convertToAbsoluteDirection(flags, layoutDirection); } @Override public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { View itemView = viewHolder.itemView; int horizontalOffset = (int) Utils.convertDpToPixel(16, mActivity); if (dX > 0) { if (dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) { dX = (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold; if (!exceedThreshold && isCurrentlyActive) { exceedThreshold = true; if (vibrateWhenActionTriggered) { itemView.setHapticFeedbackEnabled(true); itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } backgroundSwipeRight.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom()); } else { exceedThreshold = false; backgroundSwipeRight.setBounds(0, 0, 0, 0); } drawableSwipeRight.setBounds(itemView.getLeft() + ((int) dX) - horizontalOffset - drawableSwipeRight.getIntrinsicWidth(), (itemView.getBottom() + itemView.getTop() - drawableSwipeRight.getIntrinsicHeight()) / 2, itemView.getLeft() + ((int) dX) - horizontalOffset, (itemView.getBottom() + itemView.getTop() + drawableSwipeRight.getIntrinsicHeight()) / 2); backgroundSwipeRight.draw(c); drawableSwipeRight.draw(c); } else if (dX < 0) { if (-dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) { dX = -(itemView.getRight() - itemView.getLeft()) * swipeActionThreshold; if (!exceedThreshold && isCurrentlyActive) { exceedThreshold = true; if (vibrateWhenActionTriggered) { itemView.setHapticFeedbackEnabled(true); itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } backgroundSwipeLeft.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom()); } else { exceedThreshold = false; backgroundSwipeLeft.setBounds(0, 0, 0, 0); } drawableSwipeLeft.setBounds(itemView.getRight() + ((int) dX) + horizontalOffset, (itemView.getBottom() + itemView.getTop() - drawableSwipeLeft.getIntrinsicHeight()) / 2, itemView.getRight() + ((int) dX) + horizontalOffset + drawableSwipeLeft.getIntrinsicWidth(), (itemView.getBottom() + itemView.getTop() + drawableSwipeLeft.getIntrinsicHeight()) / 2); backgroundSwipeLeft.draw(c); drawableSwipeLeft.draw(c); } if (!isCurrentlyActive && exceedThreshold && mCommentsAdapter != null) { mCommentsAdapter.onItemSwipe(viewHolder, dX > 0 ? ItemTouchHelper.END : ItemTouchHelper.START, swipeLeftAction, swipeRightAction); exceedThreshold = false; } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } @Override public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return 1; } }); (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).setOnTouchListener((view, motionEvent) -> { shouldSwipeBack = motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP; return false; }); if (enableSwipeAction) { touchHelper.attachToRecyclerView( (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView), Float.parseFloat(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_ACTION_SENSITIVITY_IN_COMMENTS, "5")) ); } binding.swipeRefreshLayoutViewPostDetailFragment.setOnRefreshListener(() -> refresh(true, true)); mSmoothScroller = new LinearSmoothScroller(mActivity) { @Override protected int getVerticalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; } }; mSingleCommentId = getArguments().getString(EXTRA_SINGLE_COMMENT_ID); mContextNumber = getArguments().getString(EXTRA_CONTEXT_NUMBER, "8"); if (savedInstanceState == null) { if (mSingleCommentId != null) { isSingleCommentThreadMode = true; } mMessageFullname = getArguments().getString(EXTRA_MESSAGE_FULLNAME); if (!mRespectSubredditRecommendedSortType || isSingleCommentThreadMode) { sortType = loadSortType(); mActivity.setTitle(sortType.fullName); } } else { if (sortType != null) { mActivity.setTitle(sortType.fullName); } } if (getArguments().containsKey(EXTRA_POST_LIST_POSITION)) { postListPosition = getArguments().getInt(EXTRA_POST_LIST_POSITION, -1); } viewPostDetailFragmentViewModel = new ViewModelProvider( this, ViewPostDetailFragmentViewModel.Companion.provideFactory(mOauthRetrofit, mActivity.accessToken, mActivity.accountName) ).get(ViewPostDetailFragmentViewModel.class); bindView(); return binding.getRoot(); } private void bindView() { if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) && mMessageFullname != null) { ReadMessage.readMessage(mOauthRetrofit, mActivity.accessToken, mMessageFullname, new ReadMessage.ReadMessageListener() { @Override public void readSuccess() { mMessageFullname = null; } @Override public void readFailed() { } }); } if (mPost == null) { mPost = getArguments().getParcelable(EXTRA_POST_DATA); } if (mPost == null) { fetchPostAndCommentsById(getArguments().getString(EXTRA_POST_ID)); } else { if (showSensitiveWarning()) { return; } setupMenu(); mPostAdapter = new PostDetailRecyclerViewAdapter(mActivity, this, mExecutor, mCustomThemeWrapper, mOauthRetrofit, mRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mRedditDataRoomDatabase, mGlide, mSeparatePostAndComments, mActivity.accessToken, mActivity.accountName, mPost, mLocale, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostDetailsSharedPreferences, mExoCreator, post -> EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition))); mPostAdapter.setCommentsSupplier(() -> mCommentsAdapter != null ? mCommentsAdapter.getVisibleComments() : null); mCommentsAdapter = new CommentsRecyclerViewAdapter(mActivity, this, mCustomThemeWrapper, mExecutor, mRetrofit, mOauthRetrofit, mActivity.accessToken, mActivity.accountName, mPost, mLocale, mSingleCommentId, isSingleCommentThreadMode, mSharedPreferences, mNsfwAndSpoilerSharedPreferences, new CommentsRecyclerViewAdapter.CommentRecyclerViewAdapterCallback() { @Override public void retryFetchingComments() { fetchCommentsRespectRecommendedSort(false); } @Override public void retryFetchingMoreComments() { isLoadingMoreChildren = false; loadMoreChildrenSuccess = true; fetchMoreComments(); } @Override public SortType.Type getSortType() { return sortType; } }); if (mCommentsRecyclerView != null) { binding.postDetailRecyclerViewViewPostDetailFragment.setAdapter(mPostAdapter); mCommentsRecyclerView.setAdapter(mCommentsAdapter); } else { mConcatAdapter = new ConcatAdapter(mPostAdapter, mCommentsAdapter); binding.postDetailRecyclerViewViewPostDetailFragment.setAdapter(mConcatAdapter); } // Disable ALL item animations for potentially smoother expand/collapse binding.postDetailRecyclerViewViewPostDetailFragment.setItemAnimator(null); if (mCommentsRecyclerView != null) { mCommentsRecyclerView.setItemAnimator(null); } if (commentFilterFetched) { fetchCommentsAfterCommentFilterAvailable(); } else { FetchCommentFilter.fetchCommentFilter(mExecutor, new Handler(Looper.getMainLooper()), mRedditDataRoomDatabase, mPost.getSubredditName(), commentFilter -> { mCommentFilter = commentFilter; commentFilterFetched = true; fetchCommentsAfterCommentFilterAvailable(); }); } } binding.postDetailRecyclerViewViewPostDetailFragment.setCacheManager(mPostAdapter); binding.postDetailRecyclerViewViewPostDetailFragment.setPlayerInitializer(order -> { VolumeInfo volumeInfo = new VolumeInfo(true, 0f); return new PlaybackInfo(INDEX_UNSET, TIME_UNSET, volumeInfo); }); viewPostDetailFragmentViewModel.getPostModerationEventLiveData().observe(getViewLifecycleOwner(), moderationEvent -> { mPost = moderationEvent.getPost(); if (mPostAdapter != null) { mPostAdapter.updatePost(mPost); } EventBus.getDefault().post(new PostUpdateEventToPostList(moderationEvent.getPost(), moderationEvent.getPosition())); Toast.makeText(mActivity, moderationEvent.getToastMessageResId(), Toast.LENGTH_SHORT).show(); }); viewPostDetailFragmentViewModel.getCommentModerationEventLiveData().observe(getViewLifecycleOwner(), moderationEvent -> { if (mCommentsAdapter != null) { mCommentsAdapter.updateModdedStatus(moderationEvent.getComment(), moderationEvent.getPosition()); } Toast.makeText(mActivity, moderationEvent.getToastMessageResId(), Toast.LENGTH_SHORT).show(); }); } public void fetchCommentsAfterCommentFilterAvailable() { if (comments == null) { // Check if we have cached comments for this post if (mRememberCommentScrollPosition && mPost != null && !mScrollPositionRestored) { CommentScrollPositionCache.CachedPostComments cached = CommentScrollPositionCache.getInstance().get(mPost.getId()); if (cached != null && cached.comments != null && !cached.comments.isEmpty()) { // Use cached comments comments = cached.comments; children = cached.children; hasMoreChildren = cached.hasMoreChildren; mPendingScrollPositionRestore = cached.scrollPosition; mCommentsAdapter.addComments(comments, hasMoreChildren); restorePendingScrollPosition(); if (children != null && children.size() > 0) { setupChildrenScrollListener(); } return; } } fetchCommentsRespectRecommendedSort(false); } else { if (isRefreshing) { isRefreshing = false; refresh(true, true); } else if (isFetchingComments) { fetchCommentsRespectRecommendedSort(false); } else { mCommentsAdapter.addComments(comments, hasMoreChildren); restorePendingScrollPosition(); if (isLoadingMoreChildren) { isLoadingMoreChildren = false; fetchMoreComments(); } } } } private void setupMenu() { if (mMenu != null) { MenuItem saveItem = mMenu.findItem(R.id.action_save_view_post_detail_fragment); MenuItem hideItem = mMenu.findItem(R.id.action_hide_view_post_detail_fragment); mMenu.findItem(R.id.action_comment_view_post_detail_fragment).setVisible(true); mMenu.findItem(R.id.action_sort_view_post_detail_fragment).setVisible(true); mMenu.findItem(R.id.action_report_view_post_detail_fragment).setVisible(true); mMenu.findItem(R.id.action_crosspost_view_post_detail_fragment).setVisible(true); mMenu.findItem(R.id.action_add_to_post_filter_view_post_detail_fragment).setVisible(true); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (mPost.isSaved()) { saveItem.setVisible(true); saveItem.setIcon(mSavedIcon); } else { saveItem.setVisible(true); saveItem.setIcon(mUnsavedIcon); } if (mPost.isHidden()) { hideItem.setVisible(true); Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, hideItem, mActivity.getString(R.string.action_unhide_post)); } else { hideItem.setVisible(true); Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, hideItem, mActivity.getString(R.string.action_hide_post)); } } else { saveItem.setVisible(false); hideItem.setVisible(false); mMenu.findItem(R.id.action_crosspost_view_post_detail_fragment).setVisible(false); } if (mPost.getAuthor().equals(mActivity.accountName)) { if (mPost.getPostType() == Post.TEXT_TYPE) { mMenu.findItem(R.id.action_edit_view_post_detail_fragment).setVisible(true); } mMenu.findItem(R.id.action_delete_view_post_detail_fragment).setVisible(true); MenuItem nsfwItem = mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment); nsfwItem.setVisible(true); if (mPost.isNSFW()) { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, nsfwItem, mActivity.getString(R.string.action_unmark_nsfw)); } else { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, nsfwItem, mActivity.getString(R.string.action_mark_nsfw)); } MenuItem spoilerItem = mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment); spoilerItem.setVisible(true); if (mPost.isSpoiler()) { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, spoilerItem, mActivity.getString(R.string.action_unmark_spoiler)); } else { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, spoilerItem, mActivity.getString(R.string.action_mark_spoiler)); } mMenu.findItem(R.id.action_edit_flair_view_post_detail_fragment).setVisible(true); } mMenu.findItem(R.id.action_view_crosspost_parent_view_post_detail_fragment).setVisible(mPost.getCrosspostParentId() != null); } } private void restorePendingScrollPosition() { if (mPendingScrollPositionRestore >= 0 && !mScrollPositionRestored) { mScrollPositionRestored = true; final int positionToRestore = mPendingScrollPositionRestore; mPendingScrollPositionRestore = -1; RecyclerView targetRecyclerView = mCommentsRecyclerView != null ? mCommentsRecyclerView : binding.postDetailRecyclerViewViewPostDetailFragment; // Post to ensure the adapter has processed the new items targetRecyclerView.post(() -> { if (isAdded() && targetRecyclerView.getLayoutManager() instanceof LinearLayoutManager) { LinearLayoutManager layoutManager = (LinearLayoutManager) targetRecyclerView.getLayoutManager(); // Adjust position based on layout mode int adjustedPosition = positionToRestore; if (mCommentsRecyclerView == null) { // Combined layout - need to account for post adapter item adjustedPosition = positionToRestore + 1; } // Use scrollToPositionWithOffset to position the item at the top of the view layoutManager.scrollToPositionWithOffset(adjustedPosition, 0); } }); } } private void setupChildrenScrollListener() { RecyclerView targetRecyclerView = mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView; targetRecyclerView.clearOnScrollListeners(); targetRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (!mIsSmoothScrolling && !mLockFab) { if (!recyclerView.canScrollVertically(1)) { mActivity.hideFab(); } else { if (dy > 0) { if (mSwipeUpToHideFab) { mActivity.showFab(); } else { mActivity.hideFab(); } } else { if (mSwipeUpToHideFab) { mActivity.hideFab(); } else { mActivity.showFab(); } } } } if (!isLoadingMoreChildren && loadMoreChildrenSuccess) { int visibleItemCount = targetRecyclerView.getLayoutManager().getChildCount(); int totalItemCount = targetRecyclerView.getLayoutManager().getItemCount(); int firstVisibleItemPosition = ((LinearLayoutManagerBugFixed) targetRecyclerView.getLayoutManager()).findFirstVisibleItemPosition(); if ((visibleItemCount + firstVisibleItemPosition >= totalItemCount) && firstVisibleItemPosition >= 0) { fetchMoreComments(); } } } @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { mIsSmoothScrolling = false; } } }); } private void initializeSwipeActionDrawable() { if (swipeRightAction == SharedPreferencesUtils.SWIPE_ACITON_DOWNVOTE) { backgroundSwipeRight = new ColorDrawable(mCustomThemeWrapper.getDownvoted()); drawableSwipeRight = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_arrow_downward_day_night_24dp, null); } else { backgroundSwipeRight = new ColorDrawable(mCustomThemeWrapper.getUpvoted()); drawableSwipeRight = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_arrow_upward_day_night_24dp, null); } if (swipeLeftAction == SharedPreferencesUtils.SWIPE_ACITON_UPVOTE) { backgroundSwipeLeft = new ColorDrawable(mCustomThemeWrapper.getUpvoted()); drawableSwipeLeft = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_arrow_upward_day_night_24dp, null); } else { backgroundSwipeLeft = new ColorDrawable(mCustomThemeWrapper.getDownvoted()); drawableSwipeLeft = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_arrow_downward_day_night_24dp, null); } } private Drawable getMenuItemIcon(int drawableId) { Drawable icon = AppCompatResources.getDrawable(mActivity, drawableId); if (icon != null) { icon.setTint(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor()); } return icon; } public void addComment(Comment comment) { if (mCommentsAdapter != null) { mCommentsAdapter.addComment(comment); } if (mPostAdapter != null) { mPostAdapter.addOneComment(); } EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } public void addChildComment(Comment comment, String parentFullname, int parentPosition) { if (mCommentsAdapter != null) { mCommentsAdapter.addChildComment(comment, parentFullname, parentPosition); } if (mPostAdapter != null) { mPostAdapter.addOneComment(); } EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } public void editComment(Comment comment, int position) { if (mCommentsAdapter != null) { mCommentsAdapter.editComment(comment, position); } } public void editComment(String commentContentMarkdown, int position) { if (mCommentsAdapter != null) { mCommentsAdapter.editComment( commentContentMarkdown, position); } } public void changeFlair(Flair flair) { Map params = new HashMap<>(); params.put(APIUtils.API_TYPE_KEY, APIUtils.API_TYPE_JSON); params.put(APIUtils.FLAIR_TEMPLATE_ID_KEY, flair.getId()); params.put(APIUtils.LINK_KEY, mPost.getFullName()); params.put(APIUtils.TEXT_KEY, flair.getText()); mOauthRetrofit.create(RedditAPI.class).selectFlair(mPost.getSubredditNamePrefixed(), APIUtils.getOAuthHeader(mActivity.accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { refresh(true, false); showMessage(R.string.update_flair_success); } else { showMessage(R.string.update_flair_failed); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { showMessage(R.string.update_flair_failed); } }); } public void changeSortType(SortType sortType) { binding.fetchPostInfoLinearLayoutViewPostDetailFragment.setVisibility(View.GONE); mGlide.clear(binding.fetchPostInfoImageViewViewPostDetailFragment); if (children != null) { children.clear(); } this.sortType = sortType.getType(); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_SORT_TYPE, true)) { mSortTypeSharedPreferences.edit().putString(SharedPreferencesUtils.SORT_TYPE_POST_COMMENT, sortType.getType().name()).apply(); } mRespectSubredditRecommendedSortType = false; fetchCommentsRespectRecommendedSort(false, sortType.getType()); } @NonNull private SortType.Type loadSortType() { String sortTypeName = mSortTypeSharedPreferences.getString(SharedPreferencesUtils.SORT_TYPE_POST_COMMENT, SortType.Type.CONFIDENCE.name()); if (SortType.Type.BEST.name().equals(sortTypeName)) { // migrate from BEST to CONFIDENCE // key guaranteed to exist because got non-default value mSortTypeSharedPreferences.edit() .putString(SharedPreferencesUtils.SORT_TYPE_POST_COMMENT, SortType.Type.CONFIDENCE.name()) .apply(); return SortType.Type.CONFIDENCE; } return SortType.Type.valueOf(sortTypeName); } public void goToTop() { ((LinearLayoutManagerBugFixed) binding.postDetailRecyclerViewViewPostDetailFragment.getLayoutManager()).scrollToPositionWithOffset(0, 0); if (mCommentsRecyclerView != null) { ((LinearLayoutManagerBugFixed) mCommentsRecyclerView.getLayoutManager()).scrollToPositionWithOffset(0, 0); } } public void saveComment(int position, boolean isSaved) { if (mCommentsAdapter != null) { mCommentsAdapter.setSaveComment(position, isSaved); } } public void searchComment(String query, boolean searchNextComment) { if (mCommentsAdapter != null) { ArrayList visibleComments = mCommentsAdapter.getVisibleComments(); int currentSearchIndex = mCommentsAdapter.getSearchCommentIndex(); if (currentSearchIndex >= 0) { mCommentsAdapter.notifyItemChanged(currentSearchIndex); } if (visibleComments != null) { if (searchNextComment) { for (int i = currentSearchIndex + 1; i < visibleComments.size(); i++) { if (visibleComments.get(i).getCommentRawText() != null && visibleComments.get(i).getCommentRawText().toLowerCase().contains(query.toLowerCase())) { if (mCommentsAdapter != null) { mCommentsAdapter.highlightSearchResult(i); mCommentsAdapter.notifyItemChanged(i); if (mCommentsRecyclerView == null) { binding.postDetailRecyclerViewViewPostDetailFragment.scrollToPosition(i + 1); } else { mCommentsRecyclerView.scrollToPosition(i); } } return; } } } else { for (int i = currentSearchIndex - 1; i >= 0; i--) { if (visibleComments.get(i).getCommentRawText() != null && visibleComments.get(i).getCommentRawText().toLowerCase().contains(query.toLowerCase())) { if (mCommentsAdapter != null) { mCommentsAdapter.highlightSearchResult(i); mCommentsAdapter.notifyItemChanged(i); if (mCommentsRecyclerView == null) { binding.postDetailRecyclerViewViewPostDetailFragment.scrollToPosition(i + 1); } else { mCommentsRecyclerView.scrollToPosition(i); } } return; } } } } } } public void resetSearchCommentIndex() { if (mCommentsAdapter != null) { mCommentsAdapter.resetCommentSearchIndex(); } } public void loadIcon(List comments, ViewPostDetailActivityViewModel.LoadIconListener loadIconListener) { mActivity.loadAuthorIcons(comments, loadIconListener); } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.view_post_detail_fragment, menu); applyMenuItemTheme(menu); mMenu = menu; if (mPost != null) { setupMenu(); } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_search_view_post_detail_fragment) { if (mActivity.toggleSearchPanelVisibility() && mCommentsAdapter != null) { mCommentsAdapter.resetCommentSearchIndex(); } } else if (itemId == R.id.action_refresh_view_post_detail_fragment) { refresh(true, true); return true; } else if (itemId == R.id.action_comment_view_post_detail_fragment) { if (mPost != null) { if (mPost.isArchived()) { showMessage(R.string.archived_post_reply_unavailable); return true; } if (mPost.isLocked()) { showMessage(R.string.locked_post_comment_unavailable); return true; } if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { showMessage(R.string.login_first); return true; } Intent intent = new Intent(mActivity, CommentActivity.class); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_TITLE_KEY, mPost.getTitle()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_MARKDOWN_KEY, mPost.getSelfText()); intent.putExtra(CommentActivity.EXTRA_COMMENT_PARENT_BODY_KEY, mPost.getSelfTextPlain()); intent.putExtra(CommentActivity.EXTRA_PARENT_FULLNAME_KEY, mPost.getFullName()); intent.putExtra(CommentActivity.EXTRA_PARENT_DEPTH_KEY, 0); intent.putExtra(CommentActivity.EXTRA_SUBREDDIT_NAME_KEY, mPost.getSubredditName()); intent.putExtra(CommentActivity.EXTRA_IS_REPLYING_KEY, false); startActivityForResult(intent, WRITE_COMMENT_REQUEST_CODE); } return true; } else if (itemId == R.id.action_save_view_post_detail_fragment) { if (mPost != null && !mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (mPost.isSaved()) { item.setIcon(mUnsavedIcon); SaveThing.unsaveThing(mOauthRetrofit, mActivity.accessToken, mPost.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { if (isAdded()) { mPost.setSaved(false); item.setIcon(mUnsavedIcon); showMessage(R.string.post_unsaved_success); } EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } @Override public void failed() { if (isAdded()) { mPost.setSaved(true); item.setIcon(mSavedIcon); showMessage(R.string.post_unsaved_failed); } EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } }); } else { item.setIcon(mSavedIcon); SaveThing.saveThing(mOauthRetrofit, mActivity.accessToken, mPost.getFullName(), new SaveThing.SaveThingListener() { @Override public void success() { if (isAdded()) { mPost.setSaved(true); item.setIcon(mSavedIcon); showMessage(R.string.post_saved_success); } EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } @Override public void failed() { if (isAdded()) { mPost.setSaved(false); item.setIcon(mUnsavedIcon); showMessage(R.string.post_saved_failed); } EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } }); } } return true; } else if (itemId == R.id.action_sort_view_post_detail_fragment) { if (mPost != null) { PostCommentSortTypeBottomSheetFragment postCommentSortTypeBottomSheetFragment = PostCommentSortTypeBottomSheetFragment.getNewInstance(sortType); postCommentSortTypeBottomSheetFragment.show(mActivity.getSupportFragmentManager(), postCommentSortTypeBottomSheetFragment.getTag()); } return true; } else if (itemId == R.id.action_view_crosspost_parent_view_post_detail_fragment) { Intent crosspostIntent = new Intent(mActivity, ViewPostDetailActivity.class); crosspostIntent.putExtra(ViewPostDetailActivity.EXTRA_POST_ID, mPost.getCrosspostParentId()); startActivity(crosspostIntent); return true; } else if (itemId == R.id.action_hide_view_post_detail_fragment) { if (mPost != null && !mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (mPost.isHidden()) { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, getString(R.string.action_hide_post)); HidePost.unhidePost(mOauthRetrofit, mActivity.accessToken, mPost.getFullName(), new HidePost.HidePostListener() { @Override public void success() { mPost.setHidden(false); Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, mActivity.getString(R.string.action_hide_post)); showMessage(R.string.post_unhide_success); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } @Override public void failed() { mPost.setHidden(true); Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, mActivity.getString(R.string.action_unhide_post)); showMessage(R.string.post_unhide_failed); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } }); } else { Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, getString(R.string.action_unhide_post)); HidePost.hidePost(mOauthRetrofit, mActivity.accessToken, mPost.getFullName(), new HidePost.HidePostListener() { @Override public void success() { mPost.setHidden(true); Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, mActivity.getString(R.string.action_unhide_post)); showMessage(R.string.post_hide_success); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } @Override public void failed() { mPost.setHidden(false); Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, mActivity.getString(R.string.action_hide_post)); showMessage(R.string.post_hide_failed); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } }); } } return true; } else if (itemId == R.id.action_edit_view_post_detail_fragment) { if (mPost.getMediaMetadataMap() == null) { Intent editPostIntent = new Intent(mActivity, EditPostActivity.class); editPostIntent.putExtra(EditPostActivity.EXTRA_FULLNAME, mPost.getFullName()); editPostIntent.putExtra(EditPostActivity.EXTRA_TITLE, mPost.getTitle()); editPostIntent.putExtra(EditPostActivity.EXTRA_CONTENT, mPost.getSelfText()); startActivityForResult(editPostIntent, EDIT_POST_REQUEST_CODE); } else { Toast.makeText(mActivity, R.string.cannot_edit_post_with_images, Toast.LENGTH_LONG).show(); } return true; } else if (itemId == R.id.action_delete_view_post_detail_fragment) { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.delete_this_post) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.delete, (dialogInterface, i) -> DeleteThing.delete(mOauthRetrofit, mPost.getFullName(), mActivity.accessToken, new DeleteThing.DeleteThingListener() { @Override public void deleteSuccess() { Toast.makeText(mActivity, R.string.delete_post_success, Toast.LENGTH_SHORT).show(); mActivity.finish(); } @Override public void deleteFailed() { showMessage(R.string.delete_post_failed); } })) .setNegativeButton(R.string.cancel, null) .show(); return true; } else if (itemId == R.id.action_nsfw_view_post_detail_fragment) { if (mPost.isNSFW()) { unmarkNSFW(); } else { markNSFW(); } return true; } else if (itemId == R.id.action_spoiler_view_post_detail_fragment) { if (mPost.isSpoiler()) { unmarkSpoiler(); } else { markSpoiler(); } return true; } else if (itemId == R.id.action_edit_flair_view_post_detail_fragment) { FlairBottomSheetFragment flairBottomSheetFragment = new FlairBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putString(FlairBottomSheetFragment.EXTRA_SUBREDDIT_NAME, mPost.getSubredditName()); bundle.putLong(FlairBottomSheetFragment.EXTRA_VIEW_POST_DETAIL_FRAGMENT_ID, viewPostDetailFragmentId); flairBottomSheetFragment.setArguments(bundle); flairBottomSheetFragment.show(mActivity.getSupportFragmentManager(), flairBottomSheetFragment.getTag()); return true; } else if (itemId == R.id.action_report_view_post_detail_fragment) { if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { Toast.makeText(mActivity, R.string.login_first, Toast.LENGTH_SHORT).show(); return true; } Intent intent = new Intent(mActivity, ReportActivity.class); intent.putExtra(ReportActivity.EXTRA_SUBREDDIT_NAME, mPost.getSubredditName()); intent.putExtra(ReportActivity.EXTRA_THING_FULLNAME, mPost.getFullName()); startActivity(intent); return true; } else if (itemId == R.id.action_crosspost_view_post_detail_fragment) { Intent submitCrosspostIntent = new Intent(mActivity, SubmitCrosspostActivity.class); submitCrosspostIntent.putExtra(SubmitCrosspostActivity.EXTRA_POST, mPost); startActivity(submitCrosspostIntent); return true; } else if (itemId == R.id.action_add_to_post_filter_view_post_detail_fragment) { Intent intent = new Intent(mActivity, PostFilterPreferenceActivity.class); intent.putExtra(PostFilterPreferenceActivity.EXTRA_POST, mPost); startActivity(intent); return true; } return false; } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == WRITE_COMMENT_REQUEST_CODE) { if (data != null && resultCode == Activity.RESULT_OK) { if (data.hasExtra(RETURN_EXTRA_COMMENT_DATA_KEY)) { Comment comment = data.getParcelableExtra(RETURN_EXTRA_COMMENT_DATA_KEY); if (comment != null && comment.getDepth() == 0) { addComment(comment); } else { String parentFullname = data.getStringExtra(CommentActivity.EXTRA_PARENT_FULLNAME_KEY); int parentPosition = data.getIntExtra(CommentActivity.EXTRA_PARENT_POSITION_KEY, -1); if (parentFullname != null && parentPosition >= 0) { addChildComment(comment, parentFullname, parentPosition); } } } else { Toast.makeText(mActivity, R.string.send_comment_failed, Toast.LENGTH_SHORT).show(); } } } else if (requestCode == EDIT_POST_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK) { refresh(true, false); } } } private void tryMarkingPostAsRead() { if (mMarkPostsAsRead && mPost != null && !mPost.isRead()) { mPost.markAsRead(); int readPostsLimit = ReadPostsUtils.GetReadPostsLimit(mActivity.accountName, mPostHistorySharedPreferences); InsertReadPost.insertReadPost(mRedditDataRoomDatabase, mExecutor, mActivity.accountName, mPost.getId(), readPostsLimit); EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); } } @Override public void onResume() { super.onResume(); if (mPostAdapter != null) { mPostAdapter.setCanStartActivity(true); } if (mCommentsAdapter != null) { mCommentsAdapter.setCanStartActivity(true); } if (binding.postDetailRecyclerViewViewPostDetailFragment != null) { binding.postDetailRecyclerViewViewPostDetailFragment.onWindowVisibilityChanged(View.VISIBLE); } tryMarkingPostAsRead(); } @Override public void onPause() { super.onPause(); if (binding.postDetailRecyclerViewViewPostDetailFragment != null) { binding.postDetailRecyclerViewViewPostDetailFragment.onWindowVisibilityChanged(View.GONE); } // Save comments and scroll position to cache if feature is enabled if (mRememberCommentScrollPosition && mPost != null && mCommentsAdapter != null) { ArrayList visibleComments = mCommentsAdapter.getVisibleComments(); if (visibleComments != null && !visibleComments.isEmpty()) { RecyclerView targetRecyclerView = mCommentsRecyclerView != null ? mCommentsRecyclerView : binding.postDetailRecyclerViewViewPostDetailFragment; int currentPosition = 0; if (targetRecyclerView != null && targetRecyclerView.getLayoutManager() != null) { LinearLayoutManager layoutManager = (LinearLayoutManager) targetRecyclerView.getLayoutManager(); currentPosition = layoutManager.findFirstVisibleItemPosition(); // Adjust position for combined layout mode (subtract 1 for post adapter item) if (mCommentsRecyclerView == null && currentPosition > 0) { currentPosition = currentPosition - 1; } if (currentPosition < 0) { currentPosition = 0; } } CommentScrollPositionCache.getInstance().save( mPost.getId(), visibleComments, children, hasMoreChildren, currentPosition); } } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); comments = mCommentsAdapter == null ? null : mCommentsAdapter.getVisibleComments(); if (mCommentsRecyclerView != null) { LinearLayoutManager myLayoutManager = (LinearLayoutManager) mCommentsRecyclerView.getLayoutManager(); scrollPosition = myLayoutManager != null ? myLayoutManager.findFirstVisibleItemPosition() : 0; } else { LinearLayoutManager myLayoutManager = (LinearLayoutManager) binding.postDetailRecyclerViewViewPostDetailFragment.getLayoutManager(); scrollPosition = myLayoutManager != null ? myLayoutManager.findFirstVisibleItemPosition() : 0; } outState.putInt(SCROLL_POSITION_STATE, scrollPosition); Bridge.saveInstanceState(this, outState); } @Override public void onDestroyView() { Bridge.clear(this); EventBus.getDefault().unregister(this); binding.postDetailRecyclerViewViewPostDetailFragment.addOnWindowFocusChangedListener(null); super.onDestroyView(); } @SuppressLint("RestrictedApi") protected boolean applyMenuItemTheme(Menu menu) { if (mCustomThemeWrapper != null) { for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); if (((MenuItemImpl) item).requestsActionButton()) { MenuItemCompat.setIconTintList(item, ColorStateList .valueOf(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor())); } Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, null); } } return true; } private void fetchPostAndCommentsById(String subredditId) { binding.fetchPostInfoLinearLayoutViewPostDetailFragment.setVisibility(View.GONE); binding.swipeRefreshLayoutViewPostDetailFragment.setRefreshing(true); mGlide.clear(binding.fetchPostInfoImageViewViewPostDetailFragment); Call postAndComments; if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (isSingleCommentThreadMode && mSingleCommentId != null) { postAndComments = mRetrofit.create(RedditAPI.class).getPostAndCommentsSingleThreadById( subredditId, mSingleCommentId, sortType, mContextNumber); } else { postAndComments = mRetrofit.create(RedditAPI.class).getPostAndCommentsById(subredditId, sortType); } } else { if (isSingleCommentThreadMode && mSingleCommentId != null) { postAndComments = mOauthRetrofit.create(RedditAPI.class).getPostAndCommentsSingleThreadByIdOauth(subredditId, mSingleCommentId, sortType, mContextNumber, APIUtils.getOAuthHeader(mActivity.accessToken)); } else { postAndComments = mOauthRetrofit.create(RedditAPI.class).getPostAndCommentsByIdOauth(subredditId, sortType, APIUtils.getOAuthHeader(mActivity.accessToken)); } } postAndComments.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!isAdded()) { return; } binding.swipeRefreshLayoutViewPostDetailFragment.setRefreshing(false); if (response.isSuccessful()) { ParsePost.parsePost(mExecutor, new Handler(), response.body(), new ParsePost.ParsePostListener() { @Override public void onParsePostSuccess(Post post) { mPost = post; if (showSensitiveWarning()) { return; } tryMarkingPostAsRead(); setupMenu(); mPostAdapter = new PostDetailRecyclerViewAdapter(mActivity, ViewPostDetailFragment.this, mExecutor, mCustomThemeWrapper, mOauthRetrofit, mRetrofit, mRedgifsRetrofit, mStreamableApiProvider, mRedditDataRoomDatabase, mGlide, mSeparatePostAndComments, mActivity.accessToken, mActivity.accountName, mPost, mLocale, mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostDetailsSharedPreferences, mExoCreator, post1 -> EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition))); mPostAdapter.setCommentsSupplier(() -> mCommentsAdapter != null ? mCommentsAdapter.getVisibleComments() : null); mCommentsAdapter = new CommentsRecyclerViewAdapter(mActivity, ViewPostDetailFragment.this, mCustomThemeWrapper, mExecutor, mRetrofit, mOauthRetrofit, mActivity.accessToken, mActivity.accountName, mPost, mLocale, mSingleCommentId, isSingleCommentThreadMode, mSharedPreferences, mNsfwAndSpoilerSharedPreferences, new CommentsRecyclerViewAdapter.CommentRecyclerViewAdapterCallback() { @Override public void retryFetchingComments() { fetchCommentsRespectRecommendedSort(false); } @Override public void retryFetchingMoreComments() { isLoadingMoreChildren = false; loadMoreChildrenSuccess = true; fetchMoreComments(); } @Override public SortType.Type getSortType() { return sortType; } }); if (mCommentsRecyclerView != null) { binding.postDetailRecyclerViewViewPostDetailFragment.setAdapter(mPostAdapter); mCommentsRecyclerView.setAdapter(mCommentsAdapter); } else { mConcatAdapter = new ConcatAdapter(mPostAdapter, mCommentsAdapter); binding.postDetailRecyclerViewViewPostDetailFragment.setAdapter(mConcatAdapter); } FetchCommentFilter.fetchCommentFilter(mExecutor, new Handler(Looper.getMainLooper()), mRedditDataRoomDatabase, mPost.getSubredditName(), new FetchCommentFilter.FetchCommentFilterListener() { @Override public void success(CommentFilter commentFilter) { mCommentFilter = commentFilter; commentFilterFetched = true; // Check if we have cached comments for this post if (mRememberCommentScrollPosition && !mScrollPositionRestored) { CommentScrollPositionCache.CachedPostComments cached = CommentScrollPositionCache.getInstance().get(mPost.getId()); if (cached != null && cached.comments != null && !cached.comments.isEmpty()) { // Use cached comments comments = cached.comments; children = cached.children; hasMoreChildren = cached.hasMoreChildren; mPendingScrollPositionRestore = cached.scrollPosition; mCommentsAdapter.addComments(comments, hasMoreChildren); restorePendingScrollPosition(); if (children != null && children.size() > 0) { setupChildrenScrollListener(); } return; } } if (mRespectSubredditRecommendedSortType) { fetchCommentsRespectRecommendedSort(false); } else { ParseComment.parseComment(mExecutor, new Handler(), response.body(), mExpandChildren, mCommentFilter, new ParseComment.ParseCommentListener() { @Override public void onParseCommentSuccess(ArrayList topLevelComments, ArrayList expandedComments, String parentId, ArrayList moreChildrenIds) { ViewPostDetailFragment.this.children = moreChildrenIds; hasMoreChildren = children.size() != 0; mCommentsAdapter.addComments(expandedComments, hasMoreChildren); restorePendingScrollPosition(); if (children.size() > 0) { (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).clearOnScrollListeners(); (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (!mIsSmoothScrolling && !mLockFab) { if (!recyclerView.canScrollVertically(1)) { mActivity.hideFab(); } else { if (dy > 0) { if (mSwipeUpToHideFab) { mActivity.showFab(); } else { mActivity.hideFab(); } } else { if (mSwipeUpToHideFab) { mActivity.hideFab(); } else { mActivity.showFab(); } } } } if (!isLoadingMoreChildren && loadMoreChildrenSuccess) { int visibleItemCount = (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager().getChildCount(); int totalItemCount = (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager().getItemCount(); int firstVisibleItemPosition = ((LinearLayoutManagerBugFixed) (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager()).findFirstVisibleItemPosition(); if ((visibleItemCount + firstVisibleItemPosition >= totalItemCount) && firstVisibleItemPosition >= 0) { fetchMoreComments(); } } } @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { mIsSmoothScrolling = false; } } }); } } @Override public void onParseCommentFailed() { mCommentsAdapter.initiallyLoadCommentsFailed(); } }); } } }); } @Override public void onParsePostFail() { showErrorView(subredditId); } }); } else { showErrorView(subredditId); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { if (isAdded()) { showErrorView(subredditId); } } }); } private void fetchCommentsRespectRecommendedSort(boolean changeRefreshState, SortType.Type sortType) { if (mRespectSubredditRecommendedSortType && mPost != null) { if (mPost.getSuggestedSort() != null && !mPost.getSuggestedSort().equals("null") && !mPost.getSuggestedSort().isEmpty()) { try { SortType.Type sortTypeType = SortType.Type.valueOf(mPost.getSuggestedSort().toUpperCase(Locale.US)); mActivity.setTitle(sortTypeType.fullName); ViewPostDetailFragment.this.sortType = sortTypeType; fetchComments(changeRefreshState, ViewPostDetailFragment.this.sortType); return; } catch (IllegalArgumentException e) { e.printStackTrace(); } } FetchSubredditData.fetchSubredditData(mExecutor, new Handler(), mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? null : mOauthRetrofit, mRetrofit, mPost.getSubredditName(), mActivity.accessToken, new FetchSubredditData.FetchSubredditDataListener() { @Override public void onFetchSubredditDataSuccess(SubredditData subredditData, int nCurrentOnlineSubscribers) { String suggestedCommentSort = subredditData.getSuggestedCommentSort(); SortType.Type sortTypeType; if (suggestedCommentSort == null || suggestedCommentSort.equals("null") || suggestedCommentSort.equals("")) { mRespectSubredditRecommendedSortType = false; sortTypeType = loadSortType(); } else { try { sortTypeType = SortType.Type.valueOf(suggestedCommentSort.toUpperCase(Locale.US)); } catch (IllegalArgumentException e) { e.printStackTrace(); sortTypeType = loadSortType(); } } mActivity.setTitle(sortTypeType.fullName); ViewPostDetailFragment.this.sortType = sortTypeType; fetchComments(changeRefreshState, ViewPostDetailFragment.this.sortType); } @Override public void onFetchSubredditDataFail(boolean isQuarantined) { mRespectSubredditRecommendedSortType = false; SortType.Type sortTypeType = loadSortType(); mActivity.setTitle(sortTypeType.fullName); ViewPostDetailFragment.this.sortType = sortTypeType; fetchComments(changeRefreshState, ViewPostDetailFragment.this.sortType); } }); } else { fetchComments(changeRefreshState, sortType); } } private void fetchComments(boolean changeRefreshState, SortType.Type sortType) { isFetchingComments = true; mCommentsAdapter.setSingleComment(mSingleCommentId, isSingleCommentThreadMode); mCommentsAdapter.initiallyLoading(); String commentId = null; if (isSingleCommentThreadMode) { commentId = mSingleCommentId; } Retrofit retrofit = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit; FetchComment.fetchComments(mExecutor, new Handler(), retrofit, mActivity.accessToken, mActivity.accountName, mPost.getId(), commentId, sortType, mContextNumber, mExpandChildren, mCommentFilter, new FetchComment.FetchCommentListener() { @Override public void onFetchCommentSuccess(ArrayList expandedComments, String parentId, ArrayList children) { ViewPostDetailFragment.this.children = children; comments = expandedComments; hasMoreChildren = children.size() != 0; mCommentsAdapter.addComments(expandedComments, hasMoreChildren); restorePendingScrollPosition(); if (children.size() > 0) { (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).clearOnScrollListeners(); (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (!mIsSmoothScrolling && !mLockFab) { if (!recyclerView.canScrollVertically(1)) { mActivity.hideFab(); } else { if (dy > 0) { if (mSwipeUpToHideFab) { mActivity.showFab(); } else { mActivity.hideFab(); } } else { if (mSwipeUpToHideFab) { mActivity.hideFab(); } else { mActivity.showFab(); } } } } if (!isLoadingMoreChildren && loadMoreChildrenSuccess) { int visibleItemCount = (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager().getChildCount(); int totalItemCount = (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager().getItemCount(); int firstVisibleItemPosition = ((LinearLayoutManagerBugFixed) (mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView).getLayoutManager()).findFirstVisibleItemPosition(); if ((visibleItemCount + firstVisibleItemPosition >= totalItemCount) && firstVisibleItemPosition >= 0) { fetchMoreComments(); } } } @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { mIsSmoothScrolling = false; } } }); } if (changeRefreshState) { isRefreshing = false; } isFetchingComments = false; } @Override public void onFetchCommentFailed() { isFetchingComments = false; mCommentsAdapter.initiallyLoadCommentsFailed(); if (changeRefreshState) { isRefreshing = false; } } }); } private void fetchCommentsRespectRecommendedSort(boolean changeRefreshState) { fetchCommentsRespectRecommendedSort(changeRefreshState, sortType); } void fetchMoreComments() { if (isFetchingComments || isLoadingMoreChildren || !loadMoreChildrenSuccess) { return; } isLoadingMoreChildren = true; Retrofit retrofit = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mRetrofit : mOauthRetrofit; FetchComment.fetchMoreComment(mExecutor, new Handler(), retrofit, mActivity.accessToken, mActivity.accountName, children, mExpandChildren, mPost.getFullName(), sortType, new FetchComment.FetchMoreCommentListener() { @Override public void onFetchMoreCommentSuccess(ArrayList topLevelComments, ArrayList expandedComments, ArrayList moreChildrenIds) { children = moreChildrenIds; hasMoreChildren = !children.isEmpty(); mCommentsAdapter.addComments(expandedComments, hasMoreChildren); isLoadingMoreChildren = false; loadMoreChildrenSuccess = true; } @Override public void onFetchMoreCommentFailed() { isLoadingMoreChildren = false; loadMoreChildrenSuccess = false; mCommentsAdapter.loadMoreCommentsFailed(); } }); } public void refresh(boolean fetchPost, boolean fetchComments) { if (mPostAdapter != null && !isRefreshing) { isRefreshing = true; binding.fetchPostInfoLinearLayoutViewPostDetailFragment.setVisibility(View.GONE); mGlide.clear(binding.fetchPostInfoImageViewViewPostDetailFragment); if (!fetchPost && fetchComments) { fetchCommentsRespectRecommendedSort(true); } if (fetchPost) { Retrofit retrofit; if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { retrofit = mRetrofit; } else { retrofit = mOauthRetrofit; } FetchPost.fetchPost(mExecutor, new Handler(), retrofit, mPost.getId(), mActivity.accessToken, mActivity.accountName, new FetchPost.FetchPostListener() { @Override public void fetchPostSuccess(Post post) { if (isAdded()) { mPost = post; if (mPostAdapter != null) { mPostAdapter.updatePost(mPost); } EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)); setupMenu(); binding.swipeRefreshLayoutViewPostDetailFragment.setRefreshing(false); if (fetchComments) { fetchCommentsRespectRecommendedSort(true); } else { isRefreshing = false; } } } @Override public void fetchPostFailed() { if (isAdded()) { showMessage(R.string.refresh_post_failed); isRefreshing = false; } } }); } } } private void showErrorView(String subredditId) { binding.swipeRefreshLayoutViewPostDetailFragment.setRefreshing(false); binding.fetchPostInfoLinearLayoutViewPostDetailFragment.setVisibility(View.VISIBLE); binding.fetchPostInfoLinearLayoutViewPostDetailFragment.setOnClickListener(view -> fetchPostAndCommentsById(subredditId)); binding.fetchPostInfoTextViewViewPostDetailFragment.setText(R.string.load_post_error); } private void showMessage(int resId) { if (showToast) { Toast.makeText(mActivity, resId, Toast.LENGTH_SHORT).show(); } else { mActivity.showSnackBar(resId); } } private boolean showSensitiveWarning() { if (mPost != null && mPost.isNSFW() && (mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false) || !mNsfwAndSpoilerSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : (mActivity.accountName)) + SharedPreferencesUtils.NSFW_BASE, false))) { MaterialAlertDialogBuilder sensitiveWarningBuilder = new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.warning) .setMessage(R.string.this_post_contains_sensitive_content) .setPositiveButton(R.string.leave, (dialogInterface, i) -> { mActivity.finish(); }) .setCancelable(false); sensitiveWarningBuilder.show(); return true; } return false; } private void markNSFW() { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_unmark_nsfw); } Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, mPost.getFullName()); mOauthRetrofit.create(RedditAPI.class).markNSFW(APIUtils.getOAuthHeader(mActivity.accessToken), params) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_unmark_nsfw); } refresh(true, false); showMessage(R.string.mark_nsfw_success); } else { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_mark_nsfw); } showMessage(R.string.mark_nsfw_failed); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_mark_nsfw); } showMessage(R.string.mark_nsfw_failed); } }); } private void unmarkNSFW() { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_mark_nsfw); } Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, mPost.getFullName()); mOauthRetrofit.create(RedditAPI.class).unmarkNSFW(APIUtils.getOAuthHeader(mActivity.accessToken), params) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_mark_nsfw); } refresh(true, false); showMessage(R.string.unmark_nsfw_success); } else { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_unmark_nsfw); } showMessage(R.string.unmark_nsfw_failed); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { if (mMenu != null) { mMenu.findItem(R.id.action_nsfw_view_post_detail_fragment).setTitle(R.string.action_unmark_nsfw); } showMessage(R.string.unmark_nsfw_failed); } }); } private void markSpoiler() { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_unmark_spoiler); } Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, mPost.getFullName()); mOauthRetrofit.create(RedditAPI.class).markSpoiler(APIUtils.getOAuthHeader(mActivity.accessToken), params) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_unmark_spoiler); } refresh(true, false); showMessage(R.string.mark_spoiler_success); } else { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_mark_spoiler); } showMessage(R.string.mark_spoiler_failed); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_mark_spoiler); } showMessage(R.string.mark_spoiler_failed); } }); } private void unmarkSpoiler() { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_mark_spoiler); } Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, mPost.getFullName()); mOauthRetrofit.create(RedditAPI.class).unmarkSpoiler(APIUtils.getOAuthHeader(mActivity.accessToken), params) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_mark_spoiler); } refresh(true, false); showMessage(R.string.unmark_spoiler_success); } else { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_unmark_spoiler); } showMessage(R.string.unmark_spoiler_failed); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { if (mMenu != null) { mMenu.findItem(R.id.action_spoiler_view_post_detail_fragment).setTitle(R.string.action_unmark_spoiler); } showMessage(R.string.unmark_spoiler_failed); } }); } public void deleteComment(String fullName, int position) { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.delete_this_comment) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.delete, (dialogInterface, i) -> DeleteThing.delete(mOauthRetrofit, fullName, mActivity.accessToken, new DeleteThing.DeleteThingListener() { @Override public void deleteSuccess() { Toast.makeText(mActivity, R.string.delete_post_success, Toast.LENGTH_SHORT).show(); mCommentsAdapter.deleteComment(position); } @Override public void deleteFailed() { Toast.makeText(mActivity, R.string.delete_post_failed, Toast.LENGTH_SHORT).show(); } })) .setNegativeButton(R.string.cancel, null) .show(); } public void toggleReplyNotifications(Comment comment, int position) { ReplyNotificationsToggle.toggleEnableNotification(new Handler(Looper.getMainLooper()), mOauthRetrofit, mActivity.accessToken, comment, new ReplyNotificationsToggle.SendNotificationListener() { @Override public void onSuccess() { Toast.makeText(mActivity, comment.isSendReplies() ? R.string.reply_notifications_disabled : R.string.reply_notifications_enabled, Toast.LENGTH_SHORT).show(); mCommentsAdapter.toggleReplyNotifications(comment.getFullName(), position); } @Override public void onError() { Toast.makeText(mActivity, R.string.toggle_reply_notifications_failed, Toast.LENGTH_SHORT).show(); } }); } public void changeToNormalThreadMode() { isSingleCommentThreadMode = false; mSingleCommentId = null; mRespectSubredditRecommendedSortType = mSharedPreferences.getBoolean(SharedPreferencesUtils.RESPECT_SUBREDDIT_RECOMMENDED_COMMENT_SORT_TYPE, false); refresh(false, true); } public void scrollToNextParentComment() { RecyclerView chooseYourView = mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView; if (mCommentsAdapter != null && chooseYourView != null) { int currentPosition = ((LinearLayoutManagerBugFixed) chooseYourView.getLayoutManager()).findFirstVisibleItemPosition(); int nextParentPosition = mCommentsAdapter.getNextParentCommentPosition(mCommentsRecyclerView == null && !isSingleCommentThreadMode ? currentPosition - 1 : currentPosition); if (nextParentPosition < 0) { return; } int targetPosition = mCommentsRecyclerView == null && !isSingleCommentThreadMode ? nextParentPosition + 1 : nextParentPosition; ((LinearLayoutManagerBugFixed) chooseYourView.getLayoutManager()).scrollToPositionWithOffset(targetPosition, 0); } } public void scrollToPreviousParentComment() { RecyclerView chooseYourView = mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView; if (mCommentsAdapter != null && chooseYourView != null) { int currentPosition = ((LinearLayoutManagerBugFixed) chooseYourView.getLayoutManager()).findFirstVisibleItemPosition(); int previousParentPosition = mCommentsAdapter.getPreviousParentCommentPosition(mCommentsRecyclerView == null && !isSingleCommentThreadMode ? currentPosition - 1 : currentPosition); if (previousParentPosition < 0) { return; } int targetPosition = mCommentsRecyclerView == null && !isSingleCommentThreadMode ? previousParentPosition + 1 : previousParentPosition; ((LinearLayoutManagerBugFixed) chooseYourView.getLayoutManager()).scrollToPositionWithOffset(targetPosition, 0); } } public void scrollToParentComment(int position, int currentDepth) { RecyclerView chooseYourView = mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView; if (mCommentsAdapter != null && chooseYourView != null) { int viewPosition = mCommentsRecyclerView == null ? (!isSingleCommentThreadMode ? position + 1 : position + 2) : (!isSingleCommentThreadMode ? position : position + 1); int previousParentPosition = mCommentsAdapter.getParentCommentPosition(viewPosition, currentDepth); if (previousParentPosition < 0) { return; } mSmoothScroller.setTargetPosition(mCommentsRecyclerView == null && !isSingleCommentThreadMode ? previousParentPosition + 1 : previousParentPosition); mIsSmoothScrolling = true; chooseYourView.getLayoutManager().startSmoothScroll(mSmoothScroller); } } public void delayTransition() { TransitionManager.beginDelayedTransition((mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView), new AutoTransition()); } public boolean getIsNsfwSubreddit() { if (mActivity != null) { return mActivity.isNsfwSubreddit(); } return false; } public int getPostListPosition() { return postListPosition; } @Subscribe public void onPostUpdateEvent(PostUpdateEventToPostDetailFragment event) { if (mPost.getId().equals(event.post.getId())) { mPost.setVoteType(event.post.getVoteType()); mPost.setSaved(event.post.isSaved()); mPost.setNSFW(event.post.isNSFW()); mPost.setSpoiler(event.post.isSpoiler()); mPost.setIsStickied(event.post.isStickied()); mPost.setApproved(event.post.isApproved()); mPost.setApprovedAtUTC(event.post.getApprovedAtUTC()); mPost.setApprovedBy(event.post.getApprovedBy()); mPost.setRemoved(event.post.isRemoved(), event.post.isSpam()); mPost.setIsLocked(event.post.isLocked()); mPost.setIsModerator(event.post.isModerator()); if (mMenu != null) { if (event.post.isSaved()) { mMenu.findItem(R.id.action_save_view_post_detail_fragment).setIcon(mSavedIcon); } else { mMenu.findItem(R.id.action_save_view_post_detail_fragment).setIcon(mUnsavedIcon); } } if (mPostAdapter != null) { mPostAdapter.updatePost(mPost); } } } @Subscribe public void onChangeNSFWBlurEvent(ChangeNSFWBlurEvent event) { if (mPostAdapter != null) { mPostAdapter.setBlurNsfwAndDoNotBlurNsfwInNsfwSubreddits(event.needBlurNSFW, event.doNotBlurNsfwInNsfwSubreddits); } if (mCommentsRecyclerView != null) { refreshAdapter(binding.postDetailRecyclerViewViewPostDetailFragment, mConcatAdapter); } else { refreshAdapter(binding.postDetailRecyclerViewViewPostDetailFragment, mPostAdapter); } } @Subscribe public void onChangeSpoilerBlurEvent(ChangeSpoilerBlurEvent event) { if (mPostAdapter != null) { mPostAdapter.setBlurSpoiler(event.needBlurSpoiler); } if (mCommentsRecyclerView != null) { refreshAdapter(binding.postDetailRecyclerViewViewPostDetailFragment, mConcatAdapter); } else { refreshAdapter(binding.postDetailRecyclerViewViewPostDetailFragment, mPostAdapter); } } private void refreshAdapter(RecyclerView recyclerView, RecyclerView.Adapter adapter) { int previousPosition = -1; if (recyclerView.getLayoutManager() != null) { previousPosition = ((LinearLayoutManagerBugFixed) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); } RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); recyclerView.setAdapter(null); recyclerView.setLayoutManager(null); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(layoutManager); if (previousPosition > 0) { recyclerView.scrollToPosition(previousPosition); } } @Subscribe public void onChangeNetworkStatusEvent(ChangeNetworkStatusEvent changeNetworkStatusEvent) { String autoplay = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_AUTOPLAY, SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_NEVER); String dataSavingMode = mSharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); boolean stateChanged = false; if (autoplay.equals(SharedPreferencesUtils.VIDEO_AUTOPLAY_VALUE_ON_WIFI)) { if (mPostAdapter != null) { mPostAdapter.setAutoplay(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_WIFI); } stateChanged = true; } if (dataSavingMode.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { if (mPostAdapter != null) { mPostAdapter.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } if (mCommentsAdapter != null) { mCommentsAdapter.setDataSavingMode(changeNetworkStatusEvent.connectedNetwork == Utils.NETWORK_TYPE_CELLULAR); } stateChanged = true; } if (stateChanged) { if (mCommentsRecyclerView == null) { refreshAdapter(binding.postDetailRecyclerViewViewPostDetailFragment, mConcatAdapter); } else { if (mPostAdapter != null) { refreshAdapter(binding.postDetailRecyclerViewViewPostDetailFragment, mPostAdapter); } refreshAdapter(mCommentsRecyclerView, mCommentsAdapter); } } } @Subscribe public void onFlairSelectedEvent(FlairSelectedEvent event) { if (event.viewPostDetailFragmentId == viewPostDetailFragmentId) { changeFlair(event.flair); } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (ViewPostDetailActivity) context; } @Override public void applyTheme() { binding.swipeRefreshLayoutViewPostDetailFragment.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); binding.swipeRefreshLayoutViewPostDetailFragment.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); binding.fetchPostInfoTextViewViewPostDetailFragment.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); if (mActivity.typeface != null) { binding.fetchPostInfoTextViewViewPostDetailFragment.setTypeface(mActivity.contentTypeface); } } private void onWindowFocusChanged(boolean hasWindowsFocus) { if (mPostAdapter != null) { mPostAdapter.setCanPlayVideo(hasWindowsFocus); } } @Override public void approvePost(@NonNull Post post, int position) { viewPostDetailFragmentViewModel.approvePost(post, position); } @Override public void removePost(@NonNull Post post, int position, boolean isSpam) { viewPostDetailFragmentViewModel.removePost(post, position, isSpam); } @Override public void toggleSticky(@NonNull Post post, int position) { viewPostDetailFragmentViewModel.toggleSticky(post, position); } @Override public void toggleLock(@NonNull Post post, int position) { viewPostDetailFragmentViewModel.toggleLock(post, position); } @Override public void toggleNSFW(@NonNull Post post, int position) { viewPostDetailFragmentViewModel.toggleNSFW(post, position); } @Override public void toggleSpoiler(@NonNull Post post, int position) { viewPostDetailFragmentViewModel.toggleSpoiler(post, position); } @Override public void toggleMod(@NonNull Post post, int position) { viewPostDetailFragmentViewModel.toggleMod(post, position); } @Override public void toggleNotification(@NotNull Post post, int position) { viewPostDetailFragmentViewModel.toggleNotification(post, position); } @Override public void approveComment(@NonNull Comment comment, int position) { viewPostDetailFragmentViewModel.approveComment(comment, position); } @Override public void removeComment(@NonNull Comment comment, int position, boolean isSpam) { viewPostDetailFragmentViewModel.removeComment(comment, position, isSpam); } @Override public void toggleLock(@NonNull Comment comment, int position) { viewPostDetailFragmentViewModel.toggleLock(comment, position); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRedditGalleryImageOrGifFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.Manifest; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.text.util.Linkify; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import com.github.piasy.biv.BigImageViewer; import com.github.piasy.biv.loader.ImageLoader; import com.github.piasy.biv.loader.glide.GlideImageLoader; import java.io.File; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import me.saket.bettermovementmethod.BetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.asynctasks.SaveBitmapImageToFile; import ml.docilealligator.infinityforreddit.asynctasks.SaveGIFToFile; import ml.docilealligator.infinityforreddit.bottomsheetfragments.CopyTextBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.customviews.GlideGifImageViewFactory; import ml.docilealligator.infinityforreddit.databinding.FragmentViewRedditGalleryImageOrGifBinding; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ViewRedditGalleryImageOrGifFragment extends Fragment { public static final String EXTRA_REDDIT_GALLERY_MEDIA = "ERGM"; public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_INDEX = "EI"; public static final String EXTRA_MEDIA_COUNT = "EMC"; public static final String EXTRA_IS_NSFW = "EIN"; private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject Executor mExecutor; private ViewRedditGalleryActivity activity; private RequestManager glide; private Post.Gallery media; private String subredditName; private boolean isNsfw; private boolean isDownloading = false; private boolean isUseBottomCaption = false; private boolean isFallback = false; private Handler handler; private FragmentViewRedditGalleryImageOrGifBinding binding; public ViewRedditGalleryImageOrGifFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { BigImageViewer.initialize(GlideImageLoader.with(activity.getApplicationContext())); binding = FragmentViewRedditGalleryImageOrGifBinding.inflate(inflater, container, false); ((Infinity) activity.getApplication()).getAppComponent().inject(this); setHasOptionsMenu(true); media = getArguments().getParcelable(EXTRA_REDDIT_GALLERY_MEDIA); subredditName = getArguments().getString(EXTRA_SUBREDDIT_NAME); isNsfw = getArguments().getBoolean(EXTRA_IS_NSFW, false); glide = Glide.with(activity); handler = new Handler(Looper.getMainLooper()); if (activity.typeface != null) { binding.titleTextViewViewRedditGalleryImageOrGifFragment.setTypeface(activity.typeface); binding.captionTextViewViewRedditGalleryImageOrGifFragment.setTypeface(activity.typeface); binding.captionUrlTextViewViewRedditGalleryImageOrGifFragment.setTypeface(activity.typeface); } binding.imageViewViewRedditGalleryImageOrGifFragment.setImageViewFactory(new GlideGifImageViewFactory(new SaveMemoryCenterInisdeDownsampleStrategy(Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"))))); binding.imageViewViewRedditGalleryImageOrGifFragment.setImageLoaderCallback(new ImageLoader.Callback() { @Override public void onCacheHit(int imageType, File image) { } @Override public void onCacheMiss(int imageType, File image) { } @Override public void onStart() { } @Override public void onProgress(int progress) { } @Override public void onFinish() { } @Override public void onSuccess(File image) { binding.progressBarViewRedditGalleryImageOrGifFragment.setVisibility(View.GONE); final SubsamplingScaleImageView view = binding.imageViewViewRedditGalleryImageOrGifFragment.getSSIV(); if (view != null) { view.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() { @Override public void onImageLoaded() { view.setMinimumDpi(80); view.setDoubleTapZoomDpi(240); view.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED); view.setQuickScaleEnabled(true); view.resetScaleAndCenter(); } @Override public void onImageLoadError(Exception e) { e.printStackTrace(); // For issue #558 // Make sure it's not stuck in a loop if it comes to that // Fallback url should be empty if it's not an album item if (!isFallback && media.hasFallback()) { binding.imageViewViewRedditGalleryImageOrGifFragment.cancel(); isFallback = true; loadImage(); } else { isFallback = false; } } }); } } @Override public void onFail(Exception error) { binding.progressBarViewRedditGalleryImageOrGifFragment.setVisibility(View.GONE); binding.loadImageErrorLinearLayoutViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); } }); loadImage(); String caption = media.caption; String captionUrl = media.captionUrl; boolean captionIsEmpty = TextUtils.isEmpty(caption); boolean captionUrlIsEmpty = TextUtils.isEmpty(captionUrl); boolean captionTextOrUrlIsNotEmpty = !captionIsEmpty || !captionUrlIsEmpty; binding.imageViewViewRedditGalleryImageOrGifFragment.setOnClickListener(view -> { if (activity.isActionBarHidden()) { activity.getWindow().getDecorView().setSystemUiVisibility(0); activity.setActionBarHidden(false); if (activity.isUseBottomAppBar()) { binding.bottomAppBarMenuViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); } if (captionTextOrUrlIsNotEmpty) { binding.captionLayoutViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); } } else { hideAppBar(); } }); binding.captionLayoutViewRedditGalleryImageOrGifFragment.setOnClickListener(view -> hideAppBar()); binding.loadImageErrorLinearLayoutViewRedditGalleryImageOrGifFragment.setOnClickListener(view -> { binding.progressBarViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); binding.loadImageErrorLinearLayoutViewRedditGalleryImageOrGifFragment.setVisibility(View.GONE); loadImage(); }); if (activity.isUseBottomAppBar()) { binding.bottomAppBarMenuViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); if (media.mediaType == Post.Gallery.TYPE_GIF) { binding.titleTextViewViewRedditGalleryImageOrGifFragment.setText(getString(R.string.view_reddit_gallery_activity_gif_label, getArguments().getInt(EXTRA_INDEX) + 1, getArguments().getInt(EXTRA_MEDIA_COUNT))); } else { binding.titleTextViewViewRedditGalleryImageOrGifFragment.setText(getString(R.string.view_reddit_gallery_activity_image_label, getArguments().getInt(EXTRA_INDEX) + 1, getArguments().getInt(EXTRA_MEDIA_COUNT))); } binding.downloadImageViewViewRedditGalleryImageOrGifFragment.setOnClickListener(view -> { if (isDownloading) { return; } isDownloading = true; requestPermissionAndDownload(); }); binding.shareImageViewViewRedditGalleryImageOrGifFragment.setOnClickListener(view -> { if (media.mediaType == Post.Gallery.TYPE_GIF) { shareGif(); } else { shareImage(); } }); binding.wallpaperImageViewViewRedditGalleryImageOrGifFragment.setOnClickListener(view -> { setWallpaper(); }); } if (captionTextOrUrlIsNotEmpty) { isUseBottomCaption = true; if (!activity.isUseBottomAppBar()) { binding.bottomAppBarMenuViewRedditGalleryImageOrGifFragment.setVisibility(View.GONE); } binding.captionLayoutViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); if (!captionIsEmpty) { binding.captionTextViewViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); binding.captionTextViewViewRedditGalleryImageOrGifFragment.setText(caption); binding.captionTextViewViewRedditGalleryImageOrGifFragment.setOnClickListener(view -> hideAppBar()); binding.captionTextViewViewRedditGalleryImageOrGifFragment.setOnLongClickListener(view -> { if (activity != null && !activity.isDestroyed() && !activity.isFinishing() && binding.captionTextViewViewRedditGalleryImageOrGifFragment.getSelectionStart() == -1 && binding.captionTextViewViewRedditGalleryImageOrGifFragment.getSelectionEnd() == -1) { CopyTextBottomSheetFragment.show( activity.getSupportFragmentManager(), caption, null); } return true; }); } if (!captionUrlIsEmpty) { String scheme = Uri.parse(captionUrl).getScheme(); String urlWithoutScheme = ""; if (!TextUtils.isEmpty(scheme)) { urlWithoutScheme = captionUrl.substring(scheme.length() + 3); } binding.captionUrlTextViewViewRedditGalleryImageOrGifFragment.setText(TextUtils.isEmpty(urlWithoutScheme) ? captionUrl : urlWithoutScheme); BetterLinkMovementMethod.linkify(Linkify.WEB_URLS, binding.captionUrlTextViewViewRedditGalleryImageOrGifFragment).setOnLinkLongClickListener((textView, url) -> { if (activity != null && !activity.isDestroyed() && !activity.isFinishing()) { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(captionUrl); urlMenuBottomSheetFragment.show(activity.getSupportFragmentManager(), null); } return true; }); binding.captionUrlTextViewViewRedditGalleryImageOrGifFragment.setVisibility(View.VISIBLE); binding.captionUrlTextViewViewRedditGalleryImageOrGifFragment.setHighlightColor(Color.TRANSPARENT); } } else { binding.captionLayoutViewRedditGalleryImageOrGifFragment.setVisibility(View.GONE); } return binding.getRoot(); } private void hideAppBar() { activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); activity.setActionBarHidden(true); if (activity.isUseBottomAppBar()) { binding.bottomAppBarMenuViewRedditGalleryImageOrGifFragment.setVisibility(View.GONE); } binding.captionLayoutViewRedditGalleryImageOrGifFragment.setVisibility(View.GONE); } private void loadImage() { if (isFallback) { binding.imageViewViewRedditGalleryImageOrGifFragment.showImage(Uri.parse(media.fallbackUrl)); } else { binding.imageViewViewRedditGalleryImageOrGifFragment.showImage(Uri.parse(media.url)); } } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.view_reddit_gallery_image_or_gif_fragment, menu); for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); Utils.setTitleWithCustomFontToMenuItem(activity.typeface, item, null); } super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_download_view_reddit_gallery_image_or_gif_fragment) { if (isDownloading) { return false; } isDownloading = true; requestPermissionAndDownload(); return true; } else if (itemId == R.id.action_share_view_reddit_gallery_image_or_gif_fragment) { if (media.mediaType == Post.Gallery.TYPE_GIF) { shareGif(); } else { shareImage(); } return true; } else if (itemId == R.id.action_set_wallpaper_view_reddit_gallery_image_or_gif_fragment) { //setWallpaper(); Toast.makeText(activity, Integer.toString(activity.getWindow().getDecorView().getSystemUiVisibility()), Toast.LENGTH_SHORT).show(); return true; } return false; } private void requestPermissionAndDownload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted download(); } } else { download(); } } private void download() { isDownloading = false; Post parentPost = activity.getPost(); int galleryIndex = getArguments().getInt(EXTRA_INDEX, 0); if (parentPost == null) { Toast.makeText(activity, R.string.downloading_media_failed_cannot_download_media, Toast.LENGTH_SHORT).show(); return; // Cannot proceed without the parent post object } // Check if download location is set String downloadLocation; // Determine which download location to use boolean isNsfw = getArguments().getBoolean(EXTRA_IS_NSFW, false); int mediaType = media.mediaType == Post.Gallery.TYPE_VIDEO ? DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO : DownloadMediaService.EXTRA_MEDIA_TYPE_IMAGE; android.util.Log.d("GalleryDownload", "Media type: " + mediaType + " (" + (mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO ? "VIDEO" : mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_GIF ? "GIF" : "IMAGE") + ")"); String defaultSharedPrefsFile = "ml.docilealligator.infinityforreddit_preferences"; // Check for the location in both SharedPreferences - this will help identify the issue String imageLoc1 = mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String imageLoc2 = activity.getSharedPreferences(SharedPreferencesUtils.SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE) .getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String imageLoc3 = activity.getSharedPreferences(defaultSharedPrefsFile, Context.MODE_PRIVATE) .getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); android.util.Log.d("ImgurDownload", "Image location from injected prefs: " + (imageLoc1.isEmpty() ? "EMPTY" : imageLoc1)); android.util.Log.d("ImgurDownload", "Image location from SHARED_PREFERENCES_FILE: " + (imageLoc2.isEmpty() ? "EMPTY" : imageLoc2)); android.util.Log.d("ImgurDownload", "Image location from default_preferences: " + (imageLoc3.isEmpty() ? "EMPTY" : imageLoc3)); if (isNsfw && mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); Log.d("GalleryDownload", "Using NSFW download location: " + (downloadLocation.isEmpty() ? "EMPTY" : "SET")); } else { if (mediaType == DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); Log.d("GalleryDownload", "Using VIDEO download location: " + (downloadLocation.isEmpty() ? "EMPTY" : "SET")); } else { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); Log.d("GalleryDownload", "Using IMAGE download location: " + (downloadLocation.isEmpty() ? "EMPTY" : "SET")); // If the location is empty, try the other SharedPreferences if (downloadLocation == null || downloadLocation.isEmpty()) { downloadLocation = imageLoc2.isEmpty() ? imageLoc3 : imageLoc2; Log.d("GalleryDownload", "Image location was empty, trying backup location: " + (downloadLocation.isEmpty() ? "EMPTY" : downloadLocation)); } } } Log.d("GalleryDownload", "ViewRedditGalleryImageOrGifFragment.download(): " + "mediaType=" + mediaType + ", isNsfw=" + isNsfw + ", determined downloadLocation=" + (downloadLocation == null || downloadLocation.isEmpty() ? "EMPTY" : downloadLocation)); if (downloadLocation == null || downloadLocation.isEmpty()) { Toast.makeText(activity, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return; } //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadMediaService.constructJobInfo(activity, 5000000, parentPost, galleryIndex); if (jobInfo != null) { Log.d("GalleryDownload", "ViewRedditGalleryImageOrGifFragment.download(): JobInfo created successfully."); ((JobScheduler) activity.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); } else { Log.e("GalleryDownload", "ViewRedditGalleryImageOrGifFragment.download(): Failed to create JobInfo!"); Toast.makeText(activity, "Error creating download job.", Toast.LENGTH_SHORT).show(); return; // Prevent further action if jobInfo is null } Toast.makeText(activity, R.string.download_started, Toast.LENGTH_SHORT).show(); } //TODO: Find a way to share original image, Glide messes with the size and quality, // compression should be up to the app being shared with (WhatsApp for example) private void shareImage() { glide.asBitmap().load(media.hasFallback() ? media.fallbackUrl : media.url).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { File cacheDir = Utils.getCacheDir(activity); if (cacheDir != null) { Toast.makeText(activity, R.string.save_image_first, Toast.LENGTH_SHORT).show(); SaveBitmapImageToFile.SaveBitmapImageToFile(mExecutor, handler, resource, cacheDir.getPath(), media.fileName, new SaveBitmapImageToFile.SaveBitmapImageToFileListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", imageFile); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.setType("image/*"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); } @Override public void saveFailed() { Toast.makeText(activity, R.string.cannot_save_image, Toast.LENGTH_SHORT).show(); } }); } else { Toast.makeText(activity, R.string.cannot_get_storage, Toast.LENGTH_SHORT).show(); } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); } private void shareGif() { Toast.makeText(activity, R.string.save_gif_first, Toast.LENGTH_SHORT).show(); glide.asGif().load(media.url).listener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady(GifDrawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { File cacheDir = Utils.getCacheDir(activity); if (cacheDir != null) { SaveGIFToFile.saveGifToFile(mExecutor, handler, resource, cacheDir.getPath(), media.fileName, new SaveGIFToFile.SaveGIFToFileListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", imageFile); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.setType("image/*"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); } @Override public void saveFailed() { Toast.makeText(activity, R.string.cannot_save_gif, Toast.LENGTH_SHORT).show(); } }); } else { Toast.makeText(activity, R.string.cannot_get_storage, Toast.LENGTH_SHORT).show(); } return false; } }).submit(); } private void setWallpaper() { if (media.mediaType != Post.Gallery.TYPE_GIF) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { SetAsWallpaperBottomSheetFragment setAsWallpaperBottomSheetFragment = new SetAsWallpaperBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putInt(SetAsWallpaperBottomSheetFragment.EXTRA_VIEW_PAGER_POSITION, activity.getCurrentPagePosition()); setAsWallpaperBottomSheetFragment.setArguments(bundle); setAsWallpaperBottomSheetFragment.show(activity.getSupportFragmentManager(), setAsWallpaperBottomSheetFragment.getTag()); } else { activity.setToBoth(activity.getCurrentPagePosition()); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(activity, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); isDownloading = false; } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED && isDownloading) { download(); } } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (ViewRedditGalleryActivity) context; } @Override public void onResume() { super.onResume(); SubsamplingScaleImageView ssiv = binding.imageViewViewRedditGalleryImageOrGifFragment.getSSIV(); if (ssiv == null || !ssiv.hasImage()) { loadImage(); } if (activity.isActionBarHidden()) { activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); } else { activity.getWindow().getDecorView().setSystemUiVisibility(0); } } @Override public void onDestroyView() { super.onDestroyView(); binding.imageViewViewRedditGalleryImageOrGifFragment.cancel(); isFallback = false; SubsamplingScaleImageView subsamplingScaleImageView = binding.imageViewViewRedditGalleryImageOrGifFragment.getSSIV(); if (subsamplingScaleImageView != null) { subsamplingScaleImageView.recycle(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRedditGalleryVideoFragment.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.Manifest; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.OptIn; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.Fragment; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Tracks; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.SimpleCache; import androidx.media3.datasource.okhttp.OkHttpDataSource; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.ProgressiveMediaSource; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.ui.PlayerView; import com.google.android.material.button.MaterialButton; import com.google.common.collect.ImmutableList; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PlaybackSpeedBottomSheetFragment; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import okhttp3.OkHttpClient; public class ViewRedditGalleryVideoFragment extends Fragment { public static final String EXTRA_REDDIT_GALLERY_VIDEO = "EIV"; public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_INDEX = "EI"; public static final String EXTRA_MEDIA_COUNT = "EMC"; public static final String EXTRA_IS_NSFW = "EIN"; private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0; private static final String IS_MUTE_STATE = "IMS"; private static final String POSITION_STATE = "PS"; private static final String PLAYBACK_SPEED_STATE = "PSS"; private ViewRedditGalleryActivity activity; private Post.Gallery galleryVideo; private String subredditName; private boolean isNsfw; private ExoPlayer player; private DataSource.Factory dataSourceFactory; private boolean wasPlaying = false; private boolean isMute = false; private boolean isDownloading = false; private int playbackSpeed = 100; @Inject @Named("media3") OkHttpClient mOkHttpClient; @Inject @Named("default") SharedPreferences mSharedPreferences; @UnstableApi @Inject SimpleCache mSimpleCache; private ViewRedditGalleryVideoFragmentBindingAdapter binding; public ViewRedditGalleryVideoFragment() { // Required empty public constructor } @OptIn(markerClass = UnstableApi.class) @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = new ViewRedditGalleryVideoFragmentBindingAdapter(ml.docilealligator.infinityforreddit.databinding.FragmentViewRedditGalleryVideoBinding.inflate(inflater, container, false)); ((Infinity) activity.getApplication()).getAppComponent().inject(this); setHasOptionsMenu(true); if (activity.typeface != null) { binding.getTitleTextView().setTypeface(activity.typeface); } activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); galleryVideo = getArguments().getParcelable(EXTRA_REDDIT_GALLERY_VIDEO); subredditName = getArguments().getString(EXTRA_SUBREDDIT_NAME); isNsfw = getArguments().getBoolean(EXTRA_IS_NSFW, false); if (!mSharedPreferences.getBoolean(SharedPreferencesUtils.VIDEO_PLAYER_IGNORE_NAV_BAR, false)) { if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT || getResources().getBoolean(R.bool.isTablet)) { //Set player controller bottom margin in order to display it above the navbar int resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); LinearLayout controllerLinearLayout = binding.getRoot().findViewById(R.id.linear_layout_exo_playback_control_view); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) controllerLinearLayout.getLayoutParams(); params.bottomMargin = getResources().getDimensionPixelSize(resourceId); } else { //Set player controller right margin in order to display it above the navbar int resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); LinearLayout controllerLinearLayout = binding.getRoot().findViewById(R.id.linear_layout_exo_playback_control_view); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) controllerLinearLayout.getLayoutParams(); params.rightMargin = getResources().getDimensionPixelSize(resourceId); } } binding.getPlayerView().setControllerVisibilityListener((PlayerView.ControllerVisibilityListener) visibility -> { switch (visibility) { case View.GONE: activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE); break; case View.VISIBLE: activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } }); TrackSelector trackSelector = new DefaultTrackSelector(activity); player = new ExoPlayer.Builder(activity) .setTrackSelector(trackSelector) .setRenderersFactory(new DefaultRenderersFactory(activity).setEnableDecoderFallback(true)) .build(); binding.getPlayerView().setPlayer(player); dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) .setUpstreamDataSourceFactory(new OkHttpDataSource.Factory(mOkHttpClient).setUserAgent(APIUtils.USER_AGENT)); player.prepare(); player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(galleryVideo.url))); if (savedInstanceState != null) { playbackSpeed = savedInstanceState.getInt(PLAYBACK_SPEED_STATE); } Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.DEFAULT_PLAYBACK_SPEED, "100")); preparePlayer(savedInstanceState); binding.getTitleTextView().setText(getString(R.string.view_reddit_gallery_activity_video_label, getArguments().getInt(EXTRA_INDEX) + 1, getArguments().getInt(EXTRA_MEDIA_COUNT))); if (activity.isUseBottomAppBar()) { binding.getBottomAppBar().setVisibility(View.VISIBLE); binding.getBackButton().setOnClickListener(view -> { activity.finish(); }); binding.getDownloadButton().setOnClickListener(view -> { if (isDownloading) { return; } isDownloading = true; requestPermissionAndDownload(); }); binding.getPlaybackSpeedButton().setOnClickListener(view -> { changePlaybackSpeed(); }); } return binding.getRoot(); } private void changePlaybackSpeed() { PlaybackSpeedBottomSheetFragment playbackSpeedBottomSheetFragment = new PlaybackSpeedBottomSheetFragment(); Bundle bundle = new Bundle(); bundle.putInt(PlaybackSpeedBottomSheetFragment.EXTRA_PLAYBACK_SPEED, playbackSpeed); playbackSpeedBottomSheetFragment.setArguments(bundle); playbackSpeedBottomSheetFragment.show(getChildFragmentManager(), playbackSpeedBottomSheetFragment.getTag()); } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.view_reddit_gallery_video_fragment, menu); for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); Utils.setTitleWithCustomFontToMenuItem(activity.typeface, item, null); } super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_download_view_reddit_gallery_video_fragment) { isDownloading = true; requestPermissionAndDownload(); return true; } else if (item.getItemId() == R.id.action_playback_speed_view_reddit_gallery_video_fragment) { changePlaybackSpeed(); return true; } return false; } public void setPlaybackSpeed(int speed100X) { this.playbackSpeed = speed100X; player.setPlaybackParameters(new PlaybackParameters((float) (speed100X / 100.0))); } private void requestPermissionAndDownload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not granted // No explanation needed; request the permission requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); } else { // Permission has already been granted download(); } } else { download(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(activity, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); isDownloading = false; } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED && isDownloading) { download(); } } } private void download() { isDownloading = false; // Get the parent Post object from the activity Post parentPost = activity.getPost(); // Assuming getPost() exists in ViewRedditGalleryActivity int galleryIndex = getArguments().getInt(EXTRA_INDEX, 0); if (parentPost == null) { Toast.makeText(activity, R.string.downloading_media_failed_cannot_download_media, Toast.LENGTH_SHORT).show(); return; // Cannot proceed without the parent post object } // Check if download location is set String downloadLocation; boolean isNsfw = getArguments().getBoolean(EXTRA_IS_NSFW, false); // Gallery videos should be saved to video location if (isNsfw && mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); } else { downloadLocation = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); } if (downloadLocation == null || downloadLocation.isEmpty()) { Toast.makeText(activity, R.string.download_location_not_set, Toast.LENGTH_SHORT).show(); return; } // Call the constructJobInfo overload that takes the Post object and index // This overload handles the correct filename generation internally. //TODO: contentEstimatedBytes JobInfo jobInfo = DownloadMediaService.constructJobInfo(activity, 5000000, parentPost, galleryIndex); ((JobScheduler) activity.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo); Toast.makeText(activity, R.string.download_started, Toast.LENGTH_SHORT).show(); } private void preparePlayer(Bundle savedInstanceState) { if (mSharedPreferences.getBoolean(SharedPreferencesUtils.LOOP_VIDEO, true)) { player.setRepeatMode(Player.REPEAT_MODE_ALL); } else { player.setRepeatMode(Player.REPEAT_MODE_OFF); } wasPlaying = true; boolean muteVideo = mSharedPreferences.getBoolean(SharedPreferencesUtils.MUTE_VIDEO, false); if (savedInstanceState != null) { long position = savedInstanceState.getLong(POSITION_STATE); if (position > 0) { player.seekTo(position); } isMute = savedInstanceState.getBoolean(IS_MUTE_STATE); if (isMute) { player.setVolume(0f); binding.getMuteButton().setImageResource(R.drawable.ic_mute_24dp); } else { player.setVolume(1f); binding.getMuteButton().setImageResource(R.drawable.ic_unmute_24dp); } } else if (muteVideo) { isMute = true; player.setVolume(0f); binding.getMuteButton().setImageResource(R.drawable.ic_mute_24dp); } else { binding.getMuteButton().setImageResource(R.drawable.ic_unmute_24dp); } MaterialButton playPauseButton = binding.getRoot().findViewById(R.id.exo_play_pause_button_exo_playback_control_view); Drawable playDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_play_arrow_24dp, null); Drawable pauseDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_pause_24dp, null); playPauseButton.setOnClickListener(view -> { Util.handlePlayPauseButtonAction(player); }); player.addListener(new Player.Listener() { @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { if (events.containsAny( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { playPauseButton.setIcon(Util.shouldShowPlayButton(player) ? playDrawable : pauseDrawable); } } @Override public void onTracksChanged(@NonNull Tracks tracks) { ImmutableList trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { for (int i = 0; i < trackGroups.size(); i++) { String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { binding.getMuteButton().setVisibility(View.VISIBLE); binding.getMuteButton().setOnClickListener(view -> { if (isMute) { isMute = false; player.setVolume(1f); binding.getMuteButton().setImageResource(R.drawable.ic_unmute_24dp); } else { isMute = true; player.setVolume(0f); binding.getMuteButton().setImageResource(R.drawable.ic_mute_24dp); } }); break; } } } else { binding.getMuteButton().setVisibility(View.GONE); } } }); } @Override public void onResume() { super.onResume(); if (wasPlaying) { player.setPlayWhenReady(true); } } @Override public void onPause() { super.onPause(); wasPlaying = player.getPlayWhenReady(); player.setPlayWhenReady(false); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(IS_MUTE_STATE, isMute); outState.putLong(POSITION_STATE, player.getCurrentPosition()); outState.putInt(PLAYBACK_SPEED_STATE, playbackSpeed); } @Override public void onDestroy() { super.onDestroy(); player.seekToDefaultPosition(); player.stop(); player.release(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); activity = (ViewRedditGalleryActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRedditGalleryVideoFragmentBindingAdapter.java ================================================ package ml.docilealligator.infinityforreddit.fragments; import android.widget.ImageButton; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.media3.ui.PlayerView; import com.google.android.material.bottomappbar.BottomAppBar; import com.google.android.material.button.MaterialButton; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.databinding.FragmentViewRedditGalleryVideoBinding; class ViewRedditGalleryVideoFragmentBindingAdapter { private FragmentViewRedditGalleryVideoBinding binding; private ImageButton muteButton; private BottomAppBar bottomAppBar; private TextView titleTextView; private MaterialButton backButton; private MaterialButton downloadButton; private MaterialButton playbackSpeedButton; ViewRedditGalleryVideoFragmentBindingAdapter(FragmentViewRedditGalleryVideoBinding binding) { this.binding = binding; muteButton = binding.getRoot().findViewById(R.id.mute_exo_playback_control_view); bottomAppBar = binding.getRoot().findViewById(R.id.bottom_navigation_exo_playback_control_view); titleTextView = binding.getRoot().findViewById(R.id.title_text_view_exo_playback_control_view); backButton = binding.getRoot().findViewById(R.id.back_button_exo_playback_control_view); downloadButton = binding.getRoot().findViewById(R.id.download_image_view_exo_playback_control_view); playbackSpeedButton = binding.getRoot().findViewById(R.id.playback_speed_image_view_exo_playback_control_view); } RelativeLayout getRoot() { return binding.getRoot(); } PlayerView getPlayerView() { return binding.playerViewViewRedditGalleryVideoFragment; } ImageButton getMuteButton() { return muteButton; } BottomAppBar getBottomAppBar() { return bottomAppBar; } TextView getTitleTextView() { return titleTextView; } MaterialButton getBackButton() { return backButton; } MaterialButton getDownloadButton() { return downloadButton; } MaterialButton getPlaybackSpeedButton() { return playbackSpeedButton; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/BlockQuoteWithExceptionParser.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.internal.util.Parsing; import org.commonmark.node.Block; import org.commonmark.node.BlockQuote; import org.commonmark.parser.block.AbstractBlockParser; import org.commonmark.parser.block.AbstractBlockParserFactory; import org.commonmark.parser.block.BlockContinue; import org.commonmark.parser.block.BlockStart; import org.commonmark.parser.block.MatchedBlockParser; import org.commonmark.parser.block.ParserState; // Parse and consume a block quote except when it's a spoiler opening public class BlockQuoteWithExceptionParser extends AbstractBlockParser { private final BlockQuote block = new BlockQuote(); private static boolean isMarker(ParserState state, int index) { CharSequence line = state.getLine(); return state.getIndent() < Parsing.CODE_BLOCK_INDENT && index < line.length() && line.charAt(index) == '>'; } private static boolean isMarkerSpoiler(ParserState state, int index) { CharSequence line = state.getLine(); int length = line.length(); try { return state.getIndent() < Parsing.CODE_BLOCK_INDENT && index < length && line.charAt(index) == '>' && (index + 1) < length && line.charAt(index + 1) == '!'; } catch (IndexOutOfBoundsException e) { e.printStackTrace(); return false; } } @Override public boolean isContainer() { return true; } @Override public boolean canContain(Block block) { return true; } @Override public BlockQuote getBlock() { return block; } @Override public BlockContinue tryContinue(ParserState state) { int nextNonSpace = state.getNextNonSpaceIndex(); if (isMarker(state, nextNonSpace)) { int newColumn = state.getColumn() + state.getIndent() + 1; // optional following space or tab if (Parsing.isSpaceOrTab(state.getLine(), nextNonSpace + 1)) { newColumn++; } return BlockContinue.atColumn(newColumn); } else { return BlockContinue.none(); } } public static class Factory extends AbstractBlockParserFactory { public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { int nextNonSpace = state.getNextNonSpaceIndex(); // Potential for a spoiler opening // We don't check for spoiler closing, neither does Reddit if (isMarkerSpoiler(state, nextNonSpace)) { // It might be a spoiler, don't consume return BlockStart.none(); } // Not a spoiler then else if (isMarker(state, nextNonSpace)) { int newColumn = state.getColumn() + state.getIndent() + 1; // optional following space or tab if (Parsing.isSpaceOrTab(state.getLine(), nextNonSpace + 1)) { newColumn++; } return BlockStart.of(new BlockQuoteWithExceptionParser()).atColumn(newColumn); } else { return BlockStart.none(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/CustomMarkwonAdapter.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; import androidx.annotation.IdRes; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.node.Node; import java.util.Collections; import java.util.List; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonReducer; import io.noties.markwon.recycler.MarkwonAdapter; import io.noties.markwon.recycler.SimpleEntry; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView; public class CustomMarkwonAdapter extends MarkwonAdapter { private BaseActivity activity; private final SparseArray> entries; private final Entry defaultEntry; private final MarkwonReducer reducer; private LayoutInflater layoutInflater; private Markwon markwon; private List nodes; @Nullable private View.OnClickListener onClickListener; @Nullable private View.OnLongClickListener onLongClickListener; @SuppressWarnings("WeakerAccess") CustomMarkwonAdapter( @NonNull BaseActivity activity, @NonNull SparseArray> entries, @NonNull Entry defaultEntry, @NonNull MarkwonReducer reducer) { this.activity = activity; this.entries = entries; this.defaultEntry = defaultEntry; this.reducer = reducer; setHasStableIds(true); } public void setOnClickListener(@Nullable View.OnClickListener onClickListener) { this.onClickListener = onClickListener; } public void setOnLongClickListener(@Nullable View.OnLongClickListener onLongClickListener) { this.onLongClickListener = onLongClickListener; } @NonNull public static CustomBuilderImpl builder( @NonNull BaseActivity activity, @LayoutRes int defaultEntryLayoutResId, @IdRes int defaultEntryTextViewResId ) { return builder(activity, SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId)); } @NonNull public static CustomBuilderImpl builder(@NonNull BaseActivity activity, @NonNull Entry defaultEntry) { //noinspection unchecked return new CustomBuilderImpl(activity, (Entry) defaultEntry); } @Override public void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown) { setParsedMarkdown(markwon, markwon.parse(markdown)); } @Override public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document) { setParsedMarkdown(markwon, reducer.reduce(document)); } @Override public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List nodes) { // clear all entries before applying defaultEntry.clear(); for (int i = 0, size = entries.size(); i < size; i++) { entries.valueAt(i).clear(); } this.markwon = markwon; this.nodes = nodes; } @NonNull @Override public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (layoutInflater == null) { layoutInflater = LayoutInflater.from(parent.getContext()); } final Entry entry = getEntry(viewType); return entry.createHolder(layoutInflater, parent); } @Override public void onBindViewHolder(@NonNull Holder holder, int position) { final Node node = nodes.get(position); final int viewType = getNodeViewType(node.getClass()); final Entry entry = getEntry(viewType); entry.bindHolder(markwon, holder, node); if (holder.itemView instanceof SpoilerOnClickTextView) { SpoilerOnClickTextView textView = (SpoilerOnClickTextView) holder.itemView; holder.itemView.setOnClickListener(view -> { if (onClickListener != null && textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { onClickListener.onClick(view); } }); holder.itemView.setOnLongClickListener(view -> { if (onLongClickListener != null && textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { return onLongClickListener.onLongClick(view); } return false; }); } else if (holder.itemView instanceof HorizontalScrollView) { TableLayout tableLayout = holder.itemView.findViewById(R.id.table_layout); if (tableLayout != null) { for (int i = 0; i < tableLayout.getChildCount(); i++) { if (tableLayout.getChildAt(i) instanceof TableRow) { TableRow tableRow = ((TableRow) tableLayout.getChildAt(i)); for (int j = 0; j < tableRow.getChildCount(); j++) { if (tableRow.getChildAt(j) instanceof TextView) { TextView textView = (TextView) tableRow.getChildAt(j); tableRow.getChildAt(j).setOnClickListener(view -> { if (onClickListener != null && textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { onClickListener.onClick(view); } }); tableRow.getChildAt(j).setOnLongClickListener(view -> { if (onLongClickListener != null && textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { onLongClickListener.onLongClick(view); return true; } return false; }); } } } } } } if (node instanceof ImageAndGifBlock) { if (onClickListener != null) { holder.itemView.setOnClickListener(onClickListener); } if (onLongClickListener != null) { holder.itemView.setOnLongClickListener(onLongClickListener); } if (holder instanceof ImageAndGifEntry.Holder) { ((ImageAndGifEntry.Holder) holder).binding.captionTextViewMarkdownImageAndGifBlock.setOnClickListener(view -> { if (onClickListener != null && ((ImageAndGifEntry.Holder) holder).binding.captionTextViewMarkdownImageAndGifBlock.getSelectionStart() == -1 && ((ImageAndGifEntry.Holder) holder).binding.captionTextViewMarkdownImageAndGifBlock.getSelectionEnd() == -1) { onClickListener.onClick(view); } }); ((ImageAndGifEntry.Holder) holder).binding.captionTextViewMarkdownImageAndGifBlock.setOnLongClickListener(view -> { if (onLongClickListener != null && ((ImageAndGifEntry.Holder) holder).binding.captionTextViewMarkdownImageAndGifBlock.getSelectionStart() == -1 && ((ImageAndGifEntry.Holder) holder).binding.captionTextViewMarkdownImageAndGifBlock.getSelectionEnd() == -1) { return onLongClickListener.onLongClick(view); } return false; }); } } } @Override public int getItemCount() { return nodes != null ? nodes.size() : 0; } @Override public void onViewRecycled(@NonNull Holder holder) { super.onViewRecycled(holder); final Entry entry = getEntry(holder.getItemViewType()); entry.onViewRecycled(holder); } @SuppressWarnings("unused") @NonNull public List getItems() { return nodes != null ? Collections.unmodifiableList(nodes) : Collections.emptyList(); } @Override public int getItemViewType(int position) { return getNodeViewType(nodes.get(position).getClass()); } @Override public long getItemId(int position) { final Node node = nodes.get(position); final int type = getNodeViewType(node.getClass()); final Entry entry = getEntry(type); return entry.id(node); } @Override public int getNodeViewType(@NonNull Class node) { // if has registered -> then return it, else 0 final int hash = node.hashCode(); if (entries.indexOfKey(hash) > -1) { return hash; } return 0; } @NonNull private Entry getEntry(int viewType) { return viewType == 0 ? defaultEntry : entries.get(viewType); } public static class CustomBuilderImpl implements Builder { private final BaseActivity activity; private final SparseArray> entries = new SparseArray<>(3); private final Entry defaultEntry; private MarkwonReducer reducer; CustomBuilderImpl(@NonNull BaseActivity activity, @NonNull Entry defaultEntry) { this.activity = activity; this.defaultEntry = defaultEntry; } @NonNull @Override public CustomBuilderImpl include( @NonNull Class node, @NonNull Entry entry) { //noinspection unchecked entries.append(node.hashCode(), (Entry) entry); return this; } @NonNull @Override public CustomBuilderImpl reducer(@NonNull MarkwonReducer reducer) { this.reducer = reducer; return this; } @NonNull @Override public CustomMarkwonAdapter build() { if (reducer == null) { reducer = MarkwonReducer.directChildren(); } return new CustomMarkwonAdapter(activity, entries, defaultEntry, reducer); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/Emote.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.node.CustomNode; import org.commonmark.node.Visitor; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; public class Emote extends CustomNode { private final MediaMetadata mediaMetadata; private final String title; public Emote(MediaMetadata mediaMetadata, String title) { this.mediaMetadata = mediaMetadata; this.title = title; } @Override public void accept(Visitor visitor) { visitor.visit(this); } public MediaMetadata getMediaMetadata() { return mediaMetadata; } public String getTitle() { return title; } @Override protected String toStringAttributes() { return "destination=" + mediaMetadata.original.url + ", title=" + title; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/EmoteCloseBracketInlineProcessor.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes; import androidx.annotation.Nullable; import org.commonmark.internal.Bracket; import org.commonmark.internal.util.Escaping; import org.commonmark.node.Link; import org.commonmark.node.LinkReferenceDefinition; import org.commonmark.node.Node; import java.util.Map; import java.util.regex.Pattern; import io.noties.markwon.inlineparser.InlineProcessor; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; public class EmoteCloseBracketInlineProcessor extends InlineProcessor { private static final Pattern WHITESPACE = Pattern.compile("\\s+"); @Nullable private Map mediaMetadataMap; @Override public char specialCharacter() { return ']'; } @Override protected Node parse() { index++; int startIndex = index; // Get previous `[` or `![` Bracket opener = lastBracket(); if (opener == null) { // No matching opener, just return a literal. return text("]"); } if (!opener.allowed) { // Matching opener but it's not allowed, just return a literal. removeLastBracket(); return text("]"); } // Check to see if we have a link/image String dest = null; String title = null; boolean isLinkOrImage = false; // Maybe a inline link like `[foo](/uri "title")` if (peek() == '(') { index++; spnl(); if ((dest = parseLinkDestination()) != null) { spnl(); // title needs a whitespace before if (WHITESPACE.matcher(input.substring(index - 1, index)).matches()) { title = parseLinkTitle(); spnl(); } if (peek() == ')') { index++; isLinkOrImage = true; } else { index = startIndex; } } } // Maybe a reference link like `[foo][bar]`, `[foo][]` or `[foo]` if (!isLinkOrImage) { // See if there's a link label like `[bar]` or `[]` int beforeLabel = index; parseLinkLabel(); int labelLength = index - beforeLabel; String ref = null; if (labelLength > 2) { ref = input.substring(beforeLabel, beforeLabel + labelLength); } else if (!opener.bracketAfter) { // If the second label is empty `[foo][]` or missing `[foo]`, then the first label is the reference. // But it can only be a reference when there's no (unescaped) bracket in it. // If there is, we don't even need to try to look up the reference. This is an optimization. ref = input.substring(opener.index, startIndex); } if (ref != null) { String label = Escaping.normalizeReference(ref); LinkReferenceDefinition definition = context.getLinkReferenceDefinition(label); if (definition != null) { dest = definition.getDestination(); title = definition.getTitle(); isLinkOrImage = true; } } } if (isLinkOrImage) { // If we got here, open is a potential opener Node linkOrImage; if (opener.image) { if (mediaMetadataMap == null) { index = startIndex; removeLastBracket(); return text("]"); } MediaMetadata mediaMetadata = mediaMetadataMap.get(dest); if (mediaMetadata == null) { index = startIndex; removeLastBracket(); return text("]"); } linkOrImage = new Emote(mediaMetadata, title); } else { linkOrImage = new Link(dest, title); } Node node = opener.node.getNext(); while (node != null) { Node next = node.getNext(); linkOrImage.appendChild(node); node = next; } // Process delimiters such as emphasis inside link/image processDelimiters(opener.previousDelimiter); mergeChildTextNodes(linkOrImage); // We don't need the corresponding text node anymore, we turned it into a link/image node opener.node.unlink(); removeLastBracket(); // Links within links are not allowed. We found this link, so there can be no other link around it. if (!opener.image) { Bracket bracket = lastBracket(); while (bracket != null) { if (!bracket.image) { // Disallow link opener. It will still get matched, but will not result in a link. bracket.allowed = false; } bracket = bracket.previous; } } return linkOrImage; } else { // no link or image index = startIndex; removeLastBracket(); return text("]"); } } public void setMediaMetadataMap(@Nullable Map mediaMetadataMap) { this.mediaMetadataMap = mediaMetadataMap; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/EmoteInlineProcessor.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.internal.Bracket; import org.commonmark.node.Node; import org.commonmark.node.Text; import io.noties.markwon.inlineparser.InlineProcessor; public class EmoteInlineProcessor extends InlineProcessor { @Override public char specialCharacter() { return '!'; } @Override protected Node parse() { int startIndex = index; index++; if (peek() == '[') { index++; Text node = text("!["); // Add entry to stack for this opener addBracket(Bracket.image(node, startIndex + 1, lastBracket(), lastDelimiter())); return node; } else { return null; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/EmotePlugin.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.text.Spanned; import android.text.style.ClickableSpan; import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import org.commonmark.node.Link; import org.commonmark.node.Node; import java.util.HashMap; import java.util.Map; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.RenderProps; import io.noties.markwon.SpanFactory; import io.noties.markwon.core.CoreProps; import io.noties.markwon.image.AsyncDrawable; import io.noties.markwon.image.AsyncDrawableLoader; import io.noties.markwon.image.AsyncDrawableScheduler; import io.noties.markwon.image.DrawableUtils; import io.noties.markwon.image.ImageProps; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class EmotePlugin extends AbstractMarkwonPlugin { private final AsyncDrawableLoader asyncDrawableLoader; private boolean dataSavingMode; private final boolean disableImagePreview; private final boolean canShowEmote; private final OnEmoteClickListener onEmoteClickListener; public interface GlideStore { @NonNull RequestBuilder load(@NonNull AsyncDrawable drawable); void cancel(@NonNull Target target); } public interface OnEmoteClickListener { void onEmoteClick(MediaMetadata mediaMetadata); } @NonNull public static EmotePlugin create(@NonNull final BaseActivity baseActivity, int embeddedMediaType, @NonNull final OnEmoteClickListener onEmoteClickListener) { // @since 4.5.0 cache RequestManager // sometimes `create` would be called after activity is destroyed, // so `Glide.with(baseActivity)` will throw an exception if (baseActivity.isFinishing() || baseActivity.isDestroyed()) { // No-op return new EmotePlugin(); } RequestManager requestManager = Glide.with(baseActivity); return new EmotePlugin(baseActivity, new GlideStore() { @NonNull @Override public RequestBuilder load(@NonNull AsyncDrawable drawable) { return requestManager.load(drawable.getDestination()); } @Override public void cancel(@NonNull Target target) { requestManager.clear(target); } }, embeddedMediaType, onEmoteClickListener); } @NonNull public static EmotePlugin create(@NonNull final BaseActivity baseActivity, int embeddedMediaType, boolean dataSavingMode, boolean disableImagePreview, @NonNull final OnEmoteClickListener onEmoteClickListener) { // @since 4.5.0 cache RequestManager // sometimes `create` would be called after activity is destroyed, // so `Glide.with(baseActivity)` will throw an exception if (baseActivity.isFinishing() || baseActivity.isDestroyed()) { // No-op return new EmotePlugin(); } RequestManager requestManager = Glide.with(baseActivity); return new EmotePlugin(new GlideStore() { @NonNull @Override public RequestBuilder load(@NonNull AsyncDrawable drawable) { return requestManager.load(drawable.getDestination()); } @Override public void cancel(@NonNull Target target) { requestManager.clear(target); } }, embeddedMediaType, dataSavingMode, disableImagePreview, onEmoteClickListener); } @SuppressWarnings("WeakerAccess") EmotePlugin(@NonNull final BaseActivity baseActivity, @NonNull GlideStore glideStore, int embeddedMediaType, @NonNull final OnEmoteClickListener onEmoteClickListener) { this.asyncDrawableLoader = new GlideAsyncDrawableLoader(glideStore); String dataSavingModeString = baseActivity.getDefaultSharedPreferences().getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ALWAYS)) { dataSavingMode = true; } else if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { dataSavingMode = Utils.getConnectedNetwork(baseActivity) == Utils.NETWORK_TYPE_CELLULAR; } disableImagePreview = baseActivity.getDefaultSharedPreferences().getBoolean(SharedPreferencesUtils.DISABLE_IMAGE_PREVIEW, false); canShowEmote = SharedPreferencesUtils.canShowEmote(embeddedMediaType); this.onEmoteClickListener = onEmoteClickListener; } @SuppressWarnings("WeakerAccess") EmotePlugin(@NonNull GlideStore glideStore, int embeddedMediaType, boolean dataSavingMode, boolean disableImagePreview, @NonNull final OnEmoteClickListener onEmoteClickListener) { this.asyncDrawableLoader = new GlideAsyncDrawableLoader(glideStore); this.dataSavingMode = dataSavingMode; this.disableImagePreview = disableImagePreview; canShowEmote = SharedPreferencesUtils.canShowEmote(embeddedMediaType); this.onEmoteClickListener = onEmoteClickListener; } // Return a no-op EmotePlugin when the Activity is destroyed. It's ugly. private EmotePlugin() { this.asyncDrawableLoader = AsyncDrawableLoader.noOp(); this.disableImagePreview = false; this.canShowEmote = false; this.onEmoteClickListener = mediaMetadata -> {}; } @Override public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { builder.setFactory(Emote.class, new EmoteSpanFactory()); } @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.asyncDrawableLoader(asyncDrawableLoader); } @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder.on(Emote.class, (visitor, emote) -> { if ((dataSavingMode && disableImagePreview) || !canShowEmote) { Link link = new Link(emote.getMediaMetadata().original.url, emote.getTitle()); final int length = visitor.length(); visitor.visitChildren(emote); final String destination = link.getDestination(); CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination); visitor.setSpansForNodeOptional(link, length); return; } // if there is no image spanFactory, ignore final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Emote.class); if (spanFactory == null) { visitor.visitChildren(emote); return; } final int length = visitor.length(); visitor.visitChildren(emote); // we must check if anything _was_ added, as we need at least one char to render if (length == visitor.length()) { visitor.builder().append('\uFFFC'); } final MarkwonConfiguration configuration = visitor.configuration(); final Node parent = emote.getParent(); final boolean link = parent instanceof Link; final String destination = configuration .imageDestinationProcessor() .process(emote.getMediaMetadata().original.url); final RenderProps props = visitor.renderProps(); // apply image properties // Please note that we explicitly set IMAGE_SIZE to null as we do not clear // properties after we applied span (we could though) ImageProps.DESTINATION.set(props, destination); ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link); ImageProps.IMAGE_SIZE.set(props, null); visitor.setSpans(length, spanFactory.getSpans(configuration, props)); visitor.setSpans(length, new ClickableSpan() { @Override public void onClick(@NonNull View widget) { onEmoteClickListener.onEmoteClick(emote.getMediaMetadata()); } }); }); } @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { AsyncDrawableScheduler.unschedule(textView); } @Override public void afterSetText(@NonNull TextView textView) { AsyncDrawableScheduler.schedule(textView); } public void setDataSavingMode(boolean dataSavingMode) { this.dataSavingMode = dataSavingMode; } private static class GlideAsyncDrawableLoader extends AsyncDrawableLoader { private final GlideStore glideStore; private final Map> cache = new HashMap<>(2); GlideAsyncDrawableLoader(@NonNull GlideStore glideStore) { this.glideStore = glideStore; } @Override public void load(@NonNull AsyncDrawable drawable) { final Target target = new AsyncDrawableTarget(drawable); cache.put(drawable, target); glideStore.load(drawable) .into(target); } @Override public void cancel(@NonNull AsyncDrawable drawable) { final Target target = cache.remove(drawable); if (target != null) { glideStore.cancel(target); } } @Nullable @Override public Drawable placeholder(@NonNull AsyncDrawable drawable) { return null; } private class AsyncDrawableTarget extends CustomTarget { private final AsyncDrawable drawable; AsyncDrawableTarget(@NonNull AsyncDrawable drawable) { this.drawable = drawable; } @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { if (cache.remove(drawable) != null) { if (drawable.isAttached()) { DrawableUtils.applyIntrinsicBoundsIfEmpty(resource); drawable.setResult(resource); if (resource instanceof Animatable) { ((Animatable) resource).start(); } } } } @Override public void onLoadStarted(@Nullable Drawable placeholder) { if (placeholder != null && drawable.isAttached()) { DrawableUtils.applyIntrinsicBoundsIfEmpty(placeholder); drawable.setResult(placeholder); } } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { if (cache.remove(drawable) != null) { if (errorDrawable != null && drawable.isAttached()) { DrawableUtils.applyIntrinsicBoundsIfEmpty(errorDrawable); drawable.setResult(errorDrawable); } } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { // we won't be checking if target is still present as cancellation // must remove target anyway if (drawable.isAttached()) { drawable.clearResult(); } } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/EmoteSpanFactory.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.RenderProps; import io.noties.markwon.SpanFactory; import io.noties.markwon.image.AsyncDrawable; import io.noties.markwon.image.AsyncDrawableSpan; import io.noties.markwon.image.ImageProps; public class EmoteSpanFactory implements SpanFactory { @Nullable @Override public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new AsyncDrawableSpan( configuration.theme(), new AsyncDrawable( ImageProps.DESTINATION.require(props), configuration.asyncDrawableLoader(), configuration.imageSizeResolver(), ImageProps.IMAGE_SIZE.get(props) ), AsyncDrawableSpan.ALIGN_BOTTOM, ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false) ); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/EvenBetterLinkMovementMethod.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.app.Activity; import android.graphics.RectF; import android.text.Layout; import android.text.Selection; import android.text.Spannable; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.style.BackgroundColorSpan; import android.text.style.ClickableSpan; import android.text.style.URLSpan; import android.text.util.Linkify; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.Window; import android.widget.TextView; import ml.docilealligator.infinityforreddit.R; public class EvenBetterLinkMovementMethod extends LinkMovementMethod { private static EvenBetterLinkMovementMethod singleInstance; private static final int LINKIFY_NONE = -2; private OnLinkClickListener onLinkClickListener; private OnLinkLongClickListener onLinkLongClickListener; private final RectF touchedLineBounds = new RectF(); private boolean isUrlHighlighted; private ClickableSpan clickableSpanUnderTouchOnActionDown; private int activeTextViewHashcode; private LongPressTimer ongoingLongPressTimer; private boolean wasLongPressRegistered; public interface OnLinkClickListener { /** * @param textView The TextView on which a click was registered. * @param url The clicked URL. * @return True if this click was handled. False to let Android handle the URL. */ boolean onClick(TextView textView, String url); } public interface OnLinkLongClickListener { /** * @param textView The TextView on which a long-click was registered. * @param url The long-clicked URL. * @return True if this long-click was handled. False to let Android handle the URL (as a short-click). */ boolean onLongClick(TextView textView, String url); } /** * Return a new instance of BetterLinkMovementMethod. */ public static EvenBetterLinkMovementMethod newInstance() { return new EvenBetterLinkMovementMethod(); } /** * @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES}, * {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}. * @param textViews The TextViews on which a {@link EvenBetterLinkMovementMethod} should be registered. * @return The registered {@link EvenBetterLinkMovementMethod} on the TextViews. */ public static EvenBetterLinkMovementMethod linkify(int linkifyMask, TextView... textViews) { EvenBetterLinkMovementMethod movementMethod = newInstance(); for (TextView textView : textViews) { addLinks(linkifyMask, movementMethod, textView); } return movementMethod; } /** * Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links. * * @param textViews The TextViews on which a {@link EvenBetterLinkMovementMethod} should be registered. * @return The registered {@link EvenBetterLinkMovementMethod} on the TextViews. */ public static EvenBetterLinkMovementMethod linkifyHtml(TextView... textViews) { return linkify(LINKIFY_NONE, textViews); } /** * Recursively register a {@link EvenBetterLinkMovementMethod} on every TextView inside a layout. * * @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES}, * {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}. * @return The registered {@link EvenBetterLinkMovementMethod} on the TextViews. */ public static EvenBetterLinkMovementMethod linkify(int linkifyMask, ViewGroup viewGroup) { EvenBetterLinkMovementMethod movementMethod = newInstance(); rAddLinks(linkifyMask, viewGroup, movementMethod); return movementMethod; } /** * Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links. * * @return The registered {@link EvenBetterLinkMovementMethod} on the TextViews. */ @SuppressWarnings("unused") public static EvenBetterLinkMovementMethod linkifyHtml(ViewGroup viewGroup) { return linkify(LINKIFY_NONE, viewGroup); } /** * Recursively register a {@link EvenBetterLinkMovementMethod} on every TextView inside a layout. * * @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES}, * {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}. * @return The registered {@link EvenBetterLinkMovementMethod} on the TextViews. */ public static EvenBetterLinkMovementMethod linkify(int linkifyMask, Activity activity) { // Find the layout passed to setContentView(). ViewGroup activityLayout = ((ViewGroup) ((ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT)).getChildAt(0)); EvenBetterLinkMovementMethod movementMethod = newInstance(); rAddLinks(linkifyMask, activityLayout, movementMethod); return movementMethod; } /** * Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links. * * @return The registered {@link EvenBetterLinkMovementMethod} on the TextViews. */ @SuppressWarnings("unused") public static EvenBetterLinkMovementMethod linkifyHtml(Activity activity) { return linkify(LINKIFY_NONE, activity); } /** * Get a static instance of BetterLinkMovementMethod. Do note that registering a click listener on the returned * instance is not supported because it will potentially be shared on multiple TextViews. */ @SuppressWarnings("unused") public static EvenBetterLinkMovementMethod getInstance() { if (singleInstance == null) { singleInstance = new EvenBetterLinkMovementMethod(); } return singleInstance; } protected EvenBetterLinkMovementMethod() { } /** * Set a listener that will get called whenever any link is clicked on the TextView. */ public EvenBetterLinkMovementMethod setOnLinkClickListener(OnLinkClickListener clickListener) { if (this == singleInstance) { throw new UnsupportedOperationException("Setting a click listener on the instance returned by getInstance() is not supported to avoid memory " + "leaks. Please use newInstance() or any of the linkify() methods instead."); } this.onLinkClickListener = clickListener; return this; } /** * Set a listener that will get called whenever any link is clicked on the TextView. */ public EvenBetterLinkMovementMethod setOnLinkLongClickListener(OnLinkLongClickListener longClickListener) { if (this == singleInstance) { throw new UnsupportedOperationException("Setting a long-click listener on the instance returned by getInstance() is not supported to avoid " + "memory leaks. Please use newInstance() or any of the linkify() methods instead."); } this.onLinkLongClickListener = longClickListener; return this; } // ======== PUBLIC APIs END ======== // private static void rAddLinks(int linkifyMask, ViewGroup viewGroup, EvenBetterLinkMovementMethod movementMethod) { for (int i = 0; i < viewGroup.getChildCount(); i++) { View child = viewGroup.getChildAt(i); if (child instanceof ViewGroup) { // Recursively find child TextViews. rAddLinks(linkifyMask, ((ViewGroup) child), movementMethod); } else if (child instanceof TextView) { TextView textView = (TextView) child; addLinks(linkifyMask, movementMethod, textView); } } } private static void addLinks(int linkifyMask, EvenBetterLinkMovementMethod movementMethod, TextView textView) { textView.setMovementMethod(movementMethod); if (linkifyMask != LINKIFY_NONE) { Linkify.addLinks(textView, linkifyMask); } } @Override public boolean onTouchEvent(final TextView textView, Spannable text, MotionEvent event) { if (activeTextViewHashcode != textView.hashCode()) { // Bug workaround: TextView stops calling onTouchEvent() once any URL is highlighted. // A hacky solution is to reset any "autoLink" property set in XML. But we also want // to do this once per TextView. activeTextViewHashcode = textView.hashCode(); textView.setAutoLinkMask(0); } final ClickableSpan clickableSpanUnderTouch = findClickableSpanUnderTouch(textView, text, event); if (event.getAction() == MotionEvent.ACTION_DOWN) { clickableSpanUnderTouchOnActionDown = clickableSpanUnderTouch; } final boolean touchStartedOverAClickableSpan = clickableSpanUnderTouchOnActionDown != null; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (clickableSpanUnderTouch != null) { highlightUrl(textView, clickableSpanUnderTouch, text); } if (touchStartedOverAClickableSpan && onLinkLongClickListener != null) { LongPressTimer.OnTimerReachedListener longClickListener = new LongPressTimer.OnTimerReachedListener() { @Override public void onTimerReached() { wasLongPressRegistered = true; removeUrlHighlightColor(textView); if (dispatchUrlLongClick(textView, clickableSpanUnderTouch)) { textView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } } }; startTimerForRegisteringLongClick(textView, longClickListener); } return touchStartedOverAClickableSpan; case MotionEvent.ACTION_UP: // Register a click only if the touch started and ended on the same URL. if (!wasLongPressRegistered && touchStartedOverAClickableSpan && clickableSpanUnderTouch == clickableSpanUnderTouchOnActionDown) { dispatchUrlClick(textView, clickableSpanUnderTouch); } cleanupOnTouchUp(textView); // Consume this event even if we could not find any spans to avoid letting Android handle this event. // Android's TextView implementation has a bug where links get clicked even when there is no more text // next to the link and the touch lies outside its bounds in the same direction. return touchStartedOverAClickableSpan; case MotionEvent.ACTION_CANCEL: cleanupOnTouchUp(textView); return false; case MotionEvent.ACTION_MOVE: // Stop listening for a long-press as soon as the user wanders off to unknown lands. if (clickableSpanUnderTouch != clickableSpanUnderTouchOnActionDown) { removeLongPressCallback(textView); } if (!wasLongPressRegistered) { // Toggle highlight. if (clickableSpanUnderTouch != null) { highlightUrl(textView, clickableSpanUnderTouch, text); } else { removeUrlHighlightColor(textView); } } return touchStartedOverAClickableSpan; default: return false; } } private void cleanupOnTouchUp(TextView textView) { wasLongPressRegistered = false; clickableSpanUnderTouchOnActionDown = null; removeUrlHighlightColor(textView); removeLongPressCallback(textView); } /** * Determines the touched location inside the TextView's text and returns the ClickableSpan found under it (if any). * * @return The touched ClickableSpan or null. */ protected ClickableSpan findClickableSpanUnderTouch(TextView textView, Spannable text, MotionEvent event) { // So we need to find the location in text where touch was made, regardless of whether the TextView // has scrollable text. That is, not the entire text is currently visible. int touchX = (int) event.getX(); int touchY = (int) event.getY(); // Ignore padding. touchX -= textView.getTotalPaddingLeft(); touchY -= textView.getTotalPaddingTop(); // Account for scrollable text. touchX += textView.getScrollX(); touchY += textView.getScrollY(); final Layout layout = textView.getLayout(); final int touchedLine = layout.getLineForVertical(touchY); final int touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX); touchedLineBounds.left = layout.getLineLeft(touchedLine); touchedLineBounds.top = layout.getLineTop(touchedLine); touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left; touchedLineBounds.bottom = layout.getLineBottom(touchedLine); if (touchedLineBounds.contains(touchX, touchY)) { // Find a ClickableSpan that lies under the touched area. final Object[] spans = text.getSpans(touchOffset, touchOffset, ClickableSpan.class); for (final Object span : spans) { if (span instanceof ClickableSpan) { return (ClickableSpan) span; } } // No ClickableSpan found under the touched location. return null; } else { // Touch lies outside the line's horizontal bounds where no spans should exist. return null; } } /** * Adds a background color span at clickableSpan's location. */ protected void highlightUrl(TextView textView, ClickableSpan clickableSpan, Spannable text) { if (isUrlHighlighted) { return; } isUrlHighlighted = true; int spanStart = text.getSpanStart(clickableSpan); int spanEnd = text.getSpanEnd(clickableSpan); BackgroundColorSpan highlightSpan = new BackgroundColorSpan(textView.getHighlightColor()); text.setSpan(highlightSpan, spanStart, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE); textView.setTag(R.id.bettermovementmethod_highlight_background_span, highlightSpan); Selection.setSelection(text, spanStart, spanEnd); } /** * Removes the highlight color under the Url. */ protected void removeUrlHighlightColor(TextView textView) { if (!isUrlHighlighted) { return; } isUrlHighlighted = false; Spannable text = (Spannable) textView.getText(); BackgroundColorSpan highlightSpan = (BackgroundColorSpan) textView.getTag(R.id.bettermovementmethod_highlight_background_span); text.removeSpan(highlightSpan); Selection.removeSelection(text); } protected void startTimerForRegisteringLongClick(TextView textView, LongPressTimer.OnTimerReachedListener longClickListener) { ongoingLongPressTimer = new LongPressTimer(); ongoingLongPressTimer.setOnTimerReachedListener(longClickListener); textView.postDelayed(ongoingLongPressTimer, ViewConfiguration.getLongPressTimeout()); } /** * Remove the long-press detection timer. */ protected void removeLongPressCallback(TextView textView) { if (ongoingLongPressTimer != null) { textView.removeCallbacks(ongoingLongPressTimer); ongoingLongPressTimer = null; } } protected void dispatchUrlClick(TextView textView, ClickableSpan clickableSpan) { ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan); boolean handled = onLinkClickListener != null && onLinkClickListener.onClick(textView, clickableSpanWithText.text()); if (!handled) { // Let Android handle this click. clickableSpanWithText.span().onClick(textView); } } protected boolean dispatchUrlLongClick(TextView textView, ClickableSpan clickableSpan) { ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan); boolean handled = onLinkLongClickListener != null && onLinkLongClickListener.onLongClick(textView, clickableSpanWithText.text()); if (!handled) { // Let Android handle this long click as a short-click. clickableSpanWithText.span().onClick(textView); } return true; } protected static final class LongPressTimer implements Runnable { private LongPressTimer.OnTimerReachedListener onTimerReachedListener; protected interface OnTimerReachedListener { void onTimerReached(); } @Override public void run() { onTimerReachedListener.onTimerReached(); } public void setOnTimerReachedListener(LongPressTimer.OnTimerReachedListener listener) { onTimerReachedListener = listener; } } /** * A wrapper to support all {@link ClickableSpan}s that may or may not provide URLs. */ protected static class ClickableSpanWithText { private final ClickableSpan span; private final String text; protected static ClickableSpanWithText ofSpan(TextView textView, ClickableSpan span) { Spanned s = (Spanned) textView.getText(); String text; if (span instanceof URLSpan) { text = ((URLSpan) span).getURL(); } else { int start = s.getSpanStart(span); int end = s.getSpanEnd(span); text = s.subSequence(start, end).toString(); } return new ClickableSpanWithText(span, text); } protected ClickableSpanWithText(ClickableSpan span, String text) { this.span = span; this.text = text; } protected ClickableSpan span() { return span; } protected String text() { return text; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/GiphyGifBlock.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.node.CustomBlock; import ml.docilealligator.infinityforreddit.thing.GiphyGif; public class GiphyGifBlock extends CustomBlock { public GiphyGif giphyGif; public GiphyGifBlock(GiphyGif giphyGif) { this.giphyGif = giphyGif; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/GiphyGifBlockParser.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.Nullable; import org.commonmark.node.Block; import org.commonmark.parser.block.AbstractBlockParser; import org.commonmark.parser.block.AbstractBlockParserFactory; import org.commonmark.parser.block.BlockContinue; import org.commonmark.parser.block.BlockStart; import org.commonmark.parser.block.MatchedBlockParser; import org.commonmark.parser.block.ParserState; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import ml.docilealligator.infinityforreddit.thing.GiphyGif; import ml.docilealligator.infinityforreddit.thing.UploadedImage; public class GiphyGifBlockParser extends AbstractBlockParser { private final GiphyGifBlock giphyGifBlock; GiphyGifBlockParser(GiphyGif giphyGif) { this.giphyGifBlock = new GiphyGifBlock(giphyGif); } @Override public Block getBlock() { return giphyGifBlock; } @Override public BlockContinue tryContinue(ParserState parserState) { return null; } public static class Factory extends AbstractBlockParserFactory { private final Pattern pattern = Pattern.compile("!\\[gif]\\(giphy\\|\\w+\\|downsized\\)"); @Nullable private GiphyGif giphyGif; // Only for editing comments with GiphyGif. No need to convert MediaMetadata to GiphyGif. @Nullable private Map uploadedImageMap; public Factory(@Nullable GiphyGif giphyGif, @Nullable List uploadedImages) { this.giphyGif = giphyGif; if (uploadedImages == null) { return; } uploadedImageMap = new HashMap<>(); for (UploadedImage u : uploadedImages) { uploadedImageMap.put(u.imageName, u); } } @Override public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { if (giphyGif == null && (uploadedImageMap == null || uploadedImageMap.isEmpty())) { return BlockStart.none(); } String line = state.getLine().toString(); Matcher matcher = pattern.matcher(line); if (matcher.find()) { int startIndex = line.lastIndexOf('('); if (startIndex > 0) { int endIndex = line.indexOf(')', startIndex); String id = line.substring(startIndex + 1, endIndex); if (giphyGif != null && giphyGif.id.equals(id)) { return BlockStart.of(new GiphyGifBlockParser(giphyGif)); } else if (uploadedImageMap != null && uploadedImageMap.containsKey(id)) { return BlockStart.of(new GiphyGifBlockParser(new GiphyGif(id, false))); } } } return BlockStart.none(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/GiphyGifPlugin.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.parser.Parser; import java.util.List; import io.noties.markwon.AbstractMarkwonPlugin; import ml.docilealligator.infinityforreddit.thing.GiphyGif; import ml.docilealligator.infinityforreddit.thing.UploadedImage; public class GiphyGifPlugin extends AbstractMarkwonPlugin { private final GiphyGifBlockParser.Factory factory; public GiphyGifPlugin(@Nullable GiphyGif giphyGif, @Nullable List uploadedImages) { this.factory = new GiphyGifBlockParser.Factory(giphyGif, uploadedImages); } @NonNull @Override public String processMarkdown(@NonNull String markdown) { return super.processMarkdown(markdown); } @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customBlockParserFactory(factory); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/ImageAndGifBlock.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.node.CustomBlock; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; public class ImageAndGifBlock extends CustomBlock { public MediaMetadata mediaMetadata; public ImageAndGifBlock(MediaMetadata mediaMetadata) { this.mediaMetadata = mediaMetadata; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/ImageAndGifBlockParser.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.Nullable; import org.commonmark.node.Block; import org.commonmark.parser.block.AbstractBlockParser; import org.commonmark.parser.block.AbstractBlockParserFactory; import org.commonmark.parser.block.BlockContinue; import org.commonmark.parser.block.BlockStart; import org.commonmark.parser.block.MatchedBlockParser; import org.commonmark.parser.block.ParserState; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; public class ImageAndGifBlockParser extends AbstractBlockParser { private final ImageAndGifBlock imageAndGifBlock; ImageAndGifBlockParser(MediaMetadata mediaMetadata) { this.imageAndGifBlock = new ImageAndGifBlock(mediaMetadata); } @Override public Block getBlock() { return imageAndGifBlock; } @Override public BlockContinue tryContinue(ParserState parserState) { return null; } public static class Factory extends AbstractBlockParserFactory { private final Pattern redditPreviewPattern = Pattern.compile("!\\[.*]\\(https://preview.redd.it/\\w+.(jpg|png|jpeg)((\\?+[-a-zA-Z0-9()@:%_+.~#?&/=]*)|)\\)"); private final Pattern iRedditPattern = Pattern.compile("!\\[.*]\\(https://i.redd.it/\\w+.(jpg|png|jpeg|gif)\\)"); private final Pattern gifPattern = Pattern.compile("!\\[gif]\\(giphy\\|\\w+(\\|downsized)?\\)"); @Nullable private Map mediaMetadataMap; private final int previewReddItLength = "https://preview.redd.it/".length(); private final int iReddItLength = "https://i.redd.it/".length(); @Override public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { if (mediaMetadataMap == null) { return BlockStart.none(); } String line = state.getLine().toString(); Matcher matcher = redditPreviewPattern.matcher(line); if (matcher.find()) { if (matcher.end() == line.length()) { int endIndex = line.indexOf('.', previewReddItLength); if (endIndex > 0) { int urlStartIndex = line.lastIndexOf("https://preview.redd.it/", matcher.end()); String id = line.substring(previewReddItLength + urlStartIndex, line.indexOf(".", previewReddItLength + urlStartIndex)); return mediaMetadataMap.containsKey(id) ? BlockStart.of(new ImageAndGifBlockParser(mediaMetadataMap.get(id))) : BlockStart.none(); } } } matcher = iRedditPattern.matcher(line); if (matcher.find()) { if (matcher.end() == line.length()) { int endIndex = line.indexOf('.', iReddItLength); if (endIndex > 0) { int urlStartIndex = line.lastIndexOf("https://i.redd.it/", matcher.end()); String id = line.substring(iReddItLength + urlStartIndex, line.indexOf(".", iReddItLength + urlStartIndex)); return mediaMetadataMap.containsKey(id) ? BlockStart.of(new ImageAndGifBlockParser(mediaMetadataMap.get(id))) : BlockStart.none(); } } } matcher = gifPattern.matcher(line); if (matcher.find()) { if (matcher.end() == line.length()) { String id = line.substring("![gif](".length(), line.length() - 1); return mediaMetadataMap.containsKey(id) ? BlockStart.of(new ImageAndGifBlockParser(mediaMetadataMap.get(id))) : BlockStart.none(); } } return BlockStart.none(); } public void setMediaMetadataMap(@Nullable Map mediaMetadataMap) { this.mediaMetadataMap = mediaMetadataMap; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/ImageAndGifEntry.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.SpannableString; import android.text.Spanned; import android.text.style.URLSpan; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.MultiTransformation; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.bitmap.CenterInside; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import io.noties.markwon.Markwon; import io.noties.markwon.recycler.MarkwonAdapter; import jp.wasabeef.glide.transformations.BlurTransformation; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import me.saket.bettermovementmethod.BetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.UrlMenuBottomSheetFragment; import ml.docilealligator.infinityforreddit.databinding.MarkdownImageAndGifBlockBinding; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ImageAndGifEntry extends MarkwonAdapter.Entry { private final BaseActivity baseActivity; private final RequestManager glide; private final SaveMemoryCenterInisdeDownsampleStrategy saveMemoryCenterInsideDownsampleStrategy; private final OnItemClickListener onItemClickListener; private boolean dataSavingMode; private final boolean disableImagePreview; private boolean blurImage; private final int colorAccent; private final int primaryTextColor; private final int postContentColor; private final int linkColor; private final boolean canShowImage; private final boolean canShowGif; public ImageAndGifEntry(BaseActivity baseActivity, RequestManager glide, int embeddedMediaType, OnItemClickListener onItemClickListener) { this.baseActivity = baseActivity; this.glide = glide; SharedPreferences sharedPreferences = baseActivity.getDefaultSharedPreferences(); this.saveMemoryCenterInsideDownsampleStrategy = new SaveMemoryCenterInisdeDownsampleStrategy( Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"))); this.onItemClickListener = onItemClickListener; colorAccent = baseActivity.getCustomThemeWrapper().getColorAccent(); primaryTextColor = baseActivity.getCustomThemeWrapper().getPrimaryTextColor(); postContentColor = baseActivity.getCustomThemeWrapper().getPostContentColor(); linkColor = baseActivity.getCustomThemeWrapper().getLinkColor(); canShowImage = SharedPreferencesUtils.canShowImage(embeddedMediaType); canShowGif = SharedPreferencesUtils.canShowGif(embeddedMediaType); String dataSavingModeString = sharedPreferences.getString(SharedPreferencesUtils.DATA_SAVING_MODE, SharedPreferencesUtils.DATA_SAVING_MODE_OFF); if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ALWAYS)) { dataSavingMode = true; } else if (dataSavingModeString.equals(SharedPreferencesUtils.DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA)) { dataSavingMode = Utils.getConnectedNetwork(baseActivity) == Utils.NETWORK_TYPE_CELLULAR; } disableImagePreview = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_IMAGE_PREVIEW, false); } public ImageAndGifEntry(BaseActivity baseActivity, RequestManager glide, int embeddedMediaType, boolean blurImage, OnItemClickListener onItemClickListener) { this(baseActivity, glide, embeddedMediaType, onItemClickListener); this.blurImage = blurImage; } public ImageAndGifEntry(BaseActivity baseActivity, RequestManager glide, int embeddedMediaType, boolean dataSavingMode, boolean disableImagePreview, boolean blurImage, OnItemClickListener onItemClickListener) { this.baseActivity = baseActivity; this.glide = glide; this.dataSavingMode = dataSavingMode; this.disableImagePreview = disableImagePreview; this.blurImage = blurImage; SharedPreferences sharedPreferences = baseActivity.getDefaultSharedPreferences(); this.saveMemoryCenterInsideDownsampleStrategy = new SaveMemoryCenterInisdeDownsampleStrategy( Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"))); this.onItemClickListener = onItemClickListener; colorAccent = baseActivity.getCustomThemeWrapper().getColorAccent(); primaryTextColor = baseActivity.getCustomThemeWrapper().getPrimaryTextColor(); postContentColor = baseActivity.getCustomThemeWrapper().getPostContentColor(); linkColor = baseActivity.getCustomThemeWrapper().getLinkColor(); canShowImage = SharedPreferencesUtils.canShowImage(embeddedMediaType); canShowGif = SharedPreferencesUtils.canShowGif(embeddedMediaType); } @NonNull @Override public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { return new Holder(MarkdownImageAndGifBlockBinding.inflate(inflater, parent, false)); } @Override public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull ImageAndGifBlock node) { holder.imageAndGifBlock = node; holder.commentId = currentCommentId; holder.postId = currentPostId; holder.binding.progressBarMarkdownImageAndGifBlock.setVisibility(View.VISIBLE); if (node.mediaMetadata.isGIF) { ViewGroup.LayoutParams params = holder.binding.imageViewMarkdownImageAndGifBlock.getLayoutParams(); params.width = (int) Utils.convertDpToPixel(160, baseActivity); holder.binding.imageViewMarkdownImageAndGifBlock.setLayoutParams(params); FrameLayout.LayoutParams progressBarParams = (FrameLayout.LayoutParams) holder.binding.progressBarMarkdownImageAndGifBlock.getLayoutParams(); progressBarParams.gravity = Gravity.CENTER_VERTICAL; progressBarParams.leftMargin = (int) Utils.convertDpToPixel(56, baseActivity); holder.binding.progressBarMarkdownImageAndGifBlock.setLayoutParams(progressBarParams); } RequestBuilder imageRequestBuilder; if (dataSavingMode) { if (disableImagePreview) { showImageAsUrl(holder, node); return; } else { imageRequestBuilder = glide.load(node.mediaMetadata.downscaled.url).listener(holder.requestListener); holder.binding.imageViewMarkdownImageAndGifBlock.setRatio((float) node.mediaMetadata.downscaled.y / node.mediaMetadata.downscaled.x); } } else if ((node.mediaMetadata.isGIF && !canShowGif) || (!node.mediaMetadata.isGIF && !canShowImage)) { showImageAsUrl(holder, node); return; } else { imageRequestBuilder = glide.load(node.mediaMetadata.original.url).listener(holder.requestListener); holder.binding.imageViewMarkdownImageAndGifBlock.setRatio((float) node.mediaMetadata.original.y / node.mediaMetadata.original.x); } if (blurImage && !node.mediaMetadata.isGIF) { imageRequestBuilder .apply(RequestOptions.bitmapTransform( new MultiTransformation<>( new BlurTransformation(100, 4), new RoundedCornersTransformation(8, 0)))) .into(holder.binding.imageViewMarkdownImageAndGifBlock); } else { imageRequestBuilder .apply(RequestOptions.bitmapTransform( new MultiTransformation<>( new CenterInside(), new RoundedCornersTransformation(16, 0)))) .downsample(saveMemoryCenterInsideDownsampleStrategy) .into(holder.binding.imageViewMarkdownImageAndGifBlock); } if (node.mediaMetadata.caption != null) { holder.binding.captionTextViewMarkdownImageAndGifBlock.setVisibility(View.VISIBLE); holder.binding.captionTextViewMarkdownImageAndGifBlock.setText(node.mediaMetadata.caption); } } private void showImageAsUrl(@NonNull Holder holder, @NonNull ImageAndGifBlock node) { holder.binding.imageWrapperRelativeLayoutMarkdownImageAndGifBlock.setVisibility(View.GONE); holder.binding.captionTextViewMarkdownImageAndGifBlock.setVisibility(View.VISIBLE); holder.binding.captionTextViewMarkdownImageAndGifBlock.setGravity(Gravity.NO_GRAVITY); SpannableString spannableString = new SpannableString(node.mediaMetadata.caption == null ? node.mediaMetadata.original.url : node.mediaMetadata.caption); spannableString.setSpan(new URLSpan(node.mediaMetadata.original.url), 0, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); holder.binding.captionTextViewMarkdownImageAndGifBlock.setText(spannableString); } @Override public void onViewRecycled(@NonNull Holder holder) { super.onViewRecycled(holder); holder.binding.imageWrapperRelativeLayoutMarkdownImageAndGifBlock.setVisibility(View.VISIBLE); ViewGroup.LayoutParams params = holder.binding.imageViewMarkdownImageAndGifBlock.getLayoutParams(); params.width = ViewGroup.LayoutParams.MATCH_PARENT; holder.binding.imageViewMarkdownImageAndGifBlock.setLayoutParams(params); FrameLayout.LayoutParams progressBarParams = (FrameLayout.LayoutParams) holder.binding.progressBarMarkdownImageAndGifBlock.getLayoutParams(); progressBarParams.gravity = Gravity.CENTER; progressBarParams.leftMargin = (int) Utils.convertDpToPixel(8, baseActivity); holder.binding.progressBarMarkdownImageAndGifBlock.setLayoutParams(progressBarParams); glide.clear(holder.binding.imageViewMarkdownImageAndGifBlock); holder.binding.progressBarMarkdownImageAndGifBlock.setVisibility(View.GONE); holder.binding.loadImageErrorTextViewMarkdownImageAndGifBlock.setVisibility(View.GONE); holder.binding.captionTextViewMarkdownImageAndGifBlock.setVisibility(View.GONE); holder.binding.captionTextViewMarkdownImageAndGifBlock.setGravity(Gravity.CENTER_HORIZONTAL); } private String currentCommentId; private String currentPostId; public void setCurrentCommentId(String commentId) { this.currentCommentId = commentId; } public void setCurrentPostId(String postId) { this.currentPostId = postId; } public void setDataSavingMode(boolean dataSavingMode) { this.dataSavingMode = dataSavingMode; } public class Holder extends MarkwonAdapter.Holder { MarkdownImageAndGifBlockBinding binding; RequestListener requestListener; ImageAndGifBlock imageAndGifBlock; String commentId; String postId; public Holder(@NonNull MarkdownImageAndGifBlockBinding binding) { super(binding.getRoot()); this.binding = binding; binding.progressBarMarkdownImageAndGifBlock.setIndicatorColor(colorAccent); binding.loadImageErrorTextViewMarkdownImageAndGifBlock.setTextColor(primaryTextColor); binding.captionTextViewMarkdownImageAndGifBlock.setTextColor(postContentColor); binding.captionTextViewMarkdownImageAndGifBlock.setLinkTextColor(linkColor); if (baseActivity.typeface != null) { binding.loadImageErrorTextViewMarkdownImageAndGifBlock.setTypeface(baseActivity.typeface); } if (baseActivity.contentTypeface != null) { binding.captionTextViewMarkdownImageAndGifBlock.setTypeface(baseActivity.contentTypeface); } requestListener = new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { binding.progressBarMarkdownImageAndGifBlock.setVisibility(View.GONE); binding.loadImageErrorTextViewMarkdownImageAndGifBlock.setVisibility(View.VISIBLE); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { binding.progressBarMarkdownImageAndGifBlock.setVisibility(View.GONE); return false; } }; binding.imageViewMarkdownImageAndGifBlock.setOnClickListener(view -> { if (imageAndGifBlock != null) { onItemClickListener.onItemClick(imageAndGifBlock.mediaMetadata, commentId, postId); } }); binding.captionTextViewMarkdownImageAndGifBlock.setMovementMethod( BetterLinkMovementMethod.newInstance() .setOnLinkClickListener((textView, url) -> { Intent intent = new Intent(baseActivity, LinkResolverActivity.class); intent.setData(Uri.parse(url)); baseActivity.startActivity(intent); return true; }) .setOnLinkLongClickListener((textView, url) -> { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = UrlMenuBottomSheetFragment.newInstance(url); urlMenuBottomSheetFragment.show(baseActivity.getSupportFragmentManager(), urlMenuBottomSheetFragment.getTag()); return true; })); } } public interface OnItemClickListener { void onItemClick(MediaMetadata mediaMetadata, String commentId, String postId); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/ImageAndGifPlugin.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.NonNull; import org.commonmark.parser.Parser; import java.util.Map; import io.noties.markwon.AbstractMarkwonPlugin; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; public class ImageAndGifPlugin extends AbstractMarkwonPlugin { private final ImageAndGifBlockParser.Factory factory; public ImageAndGifPlugin() { this.factory = new ImageAndGifBlockParser.Factory(); } @NonNull @Override public String processMarkdown(@NonNull String markdown) { return super.processMarkdown(markdown); } @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customBlockParserFactory(factory); } public void setMediaMetadataMap(Map mediaMetadataMap) { factory.setMediaMetadataMap(mediaMetadataMap); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/MarkdownUtils.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.content.Context; import android.text.util.Linkify; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.ext.gfm.tables.TableBlock; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; import io.noties.markwon.inlineparser.BangInlineProcessor; import io.noties.markwon.inlineparser.CloseBracketInlineProcessor; import io.noties.markwon.inlineparser.HtmlInlineProcessor; import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.movement.MovementMethodPlugin; import io.noties.markwon.recycler.table.TableEntry; import io.noties.markwon.recycler.table.TableEntryPlugin; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.BaseActivity; public class MarkdownUtils { /** * Creates a Markwon instance with all the plugins required for processing Reddit's markdown. * @return configured Markwon instance */ @NonNull public static Markwon createFullRedditMarkwon(@NonNull Context context, @NonNull MarkwonPlugin miscPlugin, @NonNull EmoteCloseBracketInlineProcessor emoteCloseBracketInlineProcessor, @NonNull EmotePlugin emotePlugin, @NonNull ImageAndGifPlugin imageAndGifPlugin, int markdownColor, int spoilerBackgroundColor, @Nullable EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener) { return Markwon.builder(context) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); plugin.excludeInlineProcessor(CloseBracketInlineProcessor.class); plugin.addInlineProcessor(new EmoteInlineProcessor()); plugin.addInlineProcessor(emoteCloseBracketInlineProcessor); })) .usePlugin(miscPlugin) .usePlugin(SuperscriptPlugin.create()) .usePlugin(SpoilerParserPlugin.create(markdownColor, spoilerBackgroundColor)) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod() .setOnLinkLongClickListener(onLinkLongClickListener))) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(imageAndGifPlugin) .usePlugin(emotePlugin) .usePlugin(TableEntryPlugin.create(context)) .build(); } @NonNull public static Markwon createContentSubmissionRedditMarkwon(@NonNull Context context, @NonNull UploadedImagePlugin uploadedImagePlugin) { return Markwon.builder(context) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) .usePlugin(SuperscriptPlugin.create()) .usePlugin(SpoilerParserPlugin.create(0, 0)) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(uploadedImagePlugin) .usePlugin(TableEntryPlugin.create(context)) .build(); } @NonNull public static Markwon createContentSubmissionRedditMarkwon(@NonNull Context context, @NonNull UploadedImagePlugin uploadedImagePlugin, @NonNull GiphyGifPlugin giphyGifPlugin) { return Markwon.builder(context) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) .usePlugin(SuperscriptPlugin.create()) .usePlugin(SpoilerParserPlugin.create(0, 0)) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(giphyGifPlugin) .usePlugin(uploadedImagePlugin) .usePlugin(TableEntryPlugin.create(context)) .build(); } @NonNull public static Markwon createContentPreviewRedditMarkwon(@NonNull Context context, @NonNull MarkwonPlugin miscPlugin, int markdownColor, int spoilerBackgroundColor) { return Markwon.builder(context) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) .usePlugin(miscPlugin) .usePlugin(SuperscriptPlugin.create()) .usePlugin(SpoilerParserPlugin.create(markdownColor, spoilerBackgroundColor)) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod())) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(TableEntryPlugin.create(context)) .build(); } @NonNull public static Markwon createDescriptionMarkwon(Context context, MarkwonPlugin miscPlugin, EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener) { return Markwon.builder(context) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) .usePlugin(miscPlugin) .usePlugin(SuperscriptPlugin.create()) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod() .setOnLinkLongClickListener(onLinkLongClickListener))) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(TableEntryPlugin.create(context)) .build(); } /** * Creates a Markwon instance that processes only the links. * @return configured Markwon instance */ @NonNull public static Markwon createLinksOnlyMarkwon(@NonNull Context context, @NonNull MarkwonPlugin miscPlugin, @Nullable EvenBetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener) { return Markwon.builder(context) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) .usePlugin(miscPlugin) .usePlugin(MovementMethodPlugin.create(EvenBetterLinkMovementMethod.newInstance().setOnLinkLongClickListener(onLinkLongClickListener))) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .build(); } /** * Creates a CustomMarkwonAdapter configured with support for tables and images. */ @NonNull public static CustomMarkwonAdapter createCustomTablesAndImagesAdapter(@NonNull BaseActivity activity, ImageAndGifEntry imageAndGifEntry) { return CustomMarkwonAdapter.builder(activity, R.layout.adapter_default_entry, R.id.text) .include(TableBlock.class, TableEntry.create(builder -> builder .tableLayout(R.layout.adapter_table_block, R.id.table_layout) .textLayoutIsRoot(R.layout.view_table_entry_cell))) .include(ImageAndGifBlock.class, imageAndGifEntry) .build(); } @NonNull public static CustomMarkwonAdapter createCustomTablesAdapter(@NonNull BaseActivity activity) { return CustomMarkwonAdapter.builder(activity, R.layout.adapter_default_entry, R.id.text) .include(TableBlock.class, TableEntry.create(builder -> builder .tableLayout(R.layout.adapter_table_block, R.id.table_layout) .textLayoutIsRoot(R.layout.view_table_entry_cell))) .build(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/RedditHeadingParser.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.internal.util.Parsing; import org.commonmark.node.Block; import org.commonmark.node.Heading; import org.commonmark.parser.InlineParser; import org.commonmark.parser.block.AbstractBlockParser; import org.commonmark.parser.block.AbstractBlockParserFactory; import org.commonmark.parser.block.BlockContinue; import org.commonmark.parser.block.BlockStart; import org.commonmark.parser.block.MatchedBlockParser; import org.commonmark.parser.block.ParserState; /** * This is a copy of {@link org.commonmark.internal.HeadingParser} with a parsing change * in {@link #getAtxHeading} to account for differences between Reddit and CommonMark */ public class RedditHeadingParser extends AbstractBlockParser { private final Heading block = new Heading(); private final String content; public RedditHeadingParser(int level, String content) { block.setLevel(level); this.content = content; } @Override public Block getBlock() { return block; } @Override public BlockContinue tryContinue(ParserState parserState) { // In both ATX and Setext headings, once we have the heading markup, there's nothing more to parse. return BlockContinue.none(); } @Override public void parseInlines(InlineParser inlineParser) { inlineParser.parse(content, block); } public static class Factory extends AbstractBlockParserFactory { @Override public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { if (state.getIndent() >= Parsing.CODE_BLOCK_INDENT) { return BlockStart.none(); } CharSequence line = state.getLine(); int nextNonSpace = state.getNextNonSpaceIndex(); RedditHeadingParser atxHeading = getAtxHeading(line, nextNonSpace); if (atxHeading != null) { return BlockStart.of(atxHeading).atIndex(line.length()); } int setextHeadingLevel = getSetextHeadingLevel(line, nextNonSpace); if (setextHeadingLevel > 0) { CharSequence paragraph = matchedBlockParser.getParagraphContent(); if (paragraph != null) { String content = paragraph.toString(); return BlockStart.of(new RedditHeadingParser(setextHeadingLevel, content)) .atIndex(line.length()) .replaceActiveBlockParser(); } } return BlockStart.none(); } } // spec: An ATX heading consists of a string of characters, parsed as inline content, between an opening sequence of // 1–6 unescaped # characters and an optional closing sequence of any number of unescaped # characters. // The optional closing sequence of #s must be preceded by a space and may be followed by spaces only. // // Unlike CommonMark, the opening sequence of # characters does not have to be followed by a space or by the end of line. private static RedditHeadingParser getAtxHeading(CharSequence line, int index) { int level = Parsing.skip('#', line, index, line.length()) - index; if (level == 0 || level > 6) { return null; } int start = index + level; if (start >= line.length()) { // End of line after markers is an empty heading return new RedditHeadingParser(level, ""); } int beforeSpace = Parsing.skipSpaceTabBackwards(line, line.length() - 1, start); int beforeHash = Parsing.skipBackwards('#', line, beforeSpace, start); int beforeTrailer = Parsing.skipSpaceTabBackwards(line, beforeHash, start); if (beforeTrailer != beforeHash) { return new RedditHeadingParser(level, line.subSequence(start, beforeTrailer + 1).toString()); } else { return new RedditHeadingParser(level, line.subSequence(start, beforeSpace + 1).toString()); } } // spec: A setext heading underline is a sequence of = characters or a sequence of - characters, with no more than // 3 spaces indentation and any number of trailing spaces. private static int getSetextHeadingLevel(CharSequence line, int index) { switch (line.charAt(index)) { case '=': if (isSetextHeadingRest(line, index + 1, '=')) { return 1; } case '-': if (isSetextHeadingRest(line, index + 1, '-')) { return 2; } } return 0; } private static boolean isSetextHeadingRest(CharSequence line, int index, char marker) { int afterMarker = Parsing.skip(marker, line, index, line.length()); int afterSpace = Parsing.skipSpaceTab(line, afterMarker, line.length()); return afterSpace >= line.length(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/RedditHeadingPlugin.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.NonNull; import org.commonmark.parser.Parser; import io.noties.markwon.AbstractMarkwonPlugin; /** * Replaces CommonMark heading parsing with Reddit-style parsing that does not require space after # */ public class RedditHeadingPlugin extends AbstractMarkwonPlugin { @NonNull public static RedditHeadingPlugin create() { return new RedditHeadingPlugin(); } @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customBlockParserFactory(new RedditHeadingParser.Factory()); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/RichTextJSONConverter.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.content.Context; import androidx.annotation.Nullable; import org.commonmark.ext.gfm.strikethrough.Strikethrough; import org.commonmark.ext.gfm.tables.TableBlock; import org.commonmark.ext.gfm.tables.TableBody; import org.commonmark.ext.gfm.tables.TableCell; import org.commonmark.ext.gfm.tables.TableHead; import org.commonmark.ext.gfm.tables.TableRow; import org.commonmark.node.BlockQuote; import org.commonmark.node.BulletList; import org.commonmark.node.Code; import org.commonmark.node.CustomBlock; import org.commonmark.node.CustomNode; import org.commonmark.node.Document; import org.commonmark.node.Emphasis; import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; import org.commonmark.node.HtmlBlock; import org.commonmark.node.HtmlInline; import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; import org.commonmark.node.LinkReferenceDefinition; import org.commonmark.node.ListItem; import org.commonmark.node.Node; import org.commonmark.node.OrderedList; import org.commonmark.node.Paragraph; import org.commonmark.node.SoftLineBreak; import org.commonmark.node.StrongEmphasis; import org.commonmark.node.Text; import org.commonmark.node.ThematicBreak; import org.commonmark.node.Visitor; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonReducer; import ml.docilealligator.infinityforreddit.thing.GiphyGif; import ml.docilealligator.infinityforreddit.thing.UploadedImage; public class RichTextJSONConverter implements Visitor { private static final int BOLD = 1; private static final int ITALICS = 2; private static final int STRIKETHROUGH = 8; private static final int SUPERSCRIPT = 32; private static final int INLINE_CODE = 64; private static final String PARAGRAPH_E = "par"; private static final String TEXT_E = "text"; private static final String HEADING_E = "h"; private static final String LINK_E = "link"; private static final String LIST_E = "list"; private static final String LIST_ITEM_E = "li"; private static final String BLOCKQUOTE_E = "blockquote"; private static final String CODE_BLOCK_E = "code"; //For lines in code block private static final String RAW_E = "raw"; private static final String SPOILER_E = "spoilertext"; private static final String TABLE_E = "table"; private static final String IMAGE_E = "img"; private static final String GIF_E = "gif"; private static final String TYPE = "e"; private static final String CONTENT = "c"; private static final String TEXT = "t"; private static final String FORMAT = "f"; private static final String URL = "u"; private static final String LEVEL = "l"; private static final String IS_ORDERED_LIST = "o"; private static final String TABLE_HEADER_CONTENT = "h"; private static final String TABLE_CELL_ALIGNMENT = "a"; private static final String TABLE_CELL_ALIGNMENT_LEFT = "l"; private static final String TABLE_CELL_ALIGNMENT_CENTER = "c"; private static final String TABLE_CELL_ALIGNMENT_RIGHT = "r"; private static final String IMAGE_ID = "id"; private static final String DOCUMENT = "document"; private final Map formatMap; private final JSONArray document; private StringBuilder textSB; private List formats; private Stack contentArrayStack; public RichTextJSONConverter() { formatMap = new HashMap<>(); formatMap.put(StrongEmphasis.class.getName(), BOLD); formatMap.put(Emphasis.class.getName(), ITALICS); formatMap.put(Strikethrough.class.getName(), STRIKETHROUGH); formatMap.put(Superscript.class.getName(), SUPERSCRIPT); formatMap.put(Code.class.getName(), INLINE_CODE); document = new JSONArray(); textSB = new StringBuilder(); formats = new ArrayList<>(); contentArrayStack = new Stack<>(); contentArrayStack.push(document); } public String constructRichTextJSON(Context context, String markdown, List uploadedImages) throws JSONException { UploadedImagePlugin uploadedImagePlugin = new UploadedImagePlugin(); uploadedImagePlugin.setUploadedImages(uploadedImages); Markwon markwon = MarkdownUtils.createContentSubmissionRedditMarkwon( context, uploadedImagePlugin); List nodes = MarkwonReducer.directChildren().reduce(markwon.parse(markdown)); JSONObject richText = new JSONObject(); for (Node n : nodes) { n.accept(this); } richText.put(DOCUMENT, document); return richText.toString(); } /** * * @param context * @param markdown * @param uploadedImages * @param giphyGif * @param uploadedImagesMap this is for editing comment with giphy gifs. Too lazy to convert UploadedImage to GiphyGif. * @return * @throws JSONException */ public String constructRichTextJSON(Context context, String markdown, List uploadedImages, @Nullable GiphyGif giphyGif) throws JSONException { UploadedImagePlugin uploadedImagePlugin = new UploadedImagePlugin(); uploadedImagePlugin.setUploadedImages(uploadedImages); Markwon markwon = MarkdownUtils.createContentSubmissionRedditMarkwon( context, uploadedImagePlugin, new GiphyGifPlugin(giphyGif, uploadedImages)); List nodes = MarkwonReducer.directChildren().reduce(markwon.parse(markdown)); JSONObject richText = new JSONObject(); for (Node n : nodes) { n.accept(this); } richText.put(DOCUMENT, document); return richText.toString(); } public JSONObject constructRichTextJSON(List nodes) throws JSONException { JSONObject richText = new JSONObject(); for (Node n : nodes) { n.accept(this); } richText.put(DOCUMENT, document); return richText; } @Nullable private JSONArray getFormatArray(Node node) { int formatNum = 0; while (node != null && node.getFirstChild() != null) { String className = node.getClass().getName(); if (formatMap.containsKey(className)) { formatNum += formatMap.get(className); } node = node.getFirstChild(); } if (node instanceof Text) { int start = textSB.length(); textSB.append(((Text) node).getLiteral()); if (formatNum > 0) { JSONArray format = new JSONArray(); format.put(formatNum); format.put(start); format.put(((Text) node).getLiteral().length()); return format; } } return null; } private String getAllText(Node node) { node = node.getFirstChild(); while (node != null) { Node next = node; while (next != null && next.getFirstChild() != null) { next = next.getFirstChild(); } if (next instanceof Text) { textSB.append(((Text) next).getLiteral()); } else if (next instanceof Code) { textSB.append(((Code) next).getLiteral()); } node = node.getNext(); } String text = textSB.toString(); textSB.delete(0, text.length()); return text; } private void convertToRawTextJSONObject(JSONArray contentArray) throws JSONException { for (int i = 0; i < contentArray.length(); i++) { JSONObject content = contentArray.getJSONObject(i); if (TEXT_E.equals(content.get(TYPE))) { content.put(TYPE, RAW_E); } } } @Override public void visit(BlockQuote blockQuote) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, BLOCKQUOTE_E); contentArrayStack.push(new JSONArray()); Node child = blockQuote.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } JSONArray cArray = contentArrayStack.pop(); nodeJSON.put(CONTENT, cArray); contentArrayStack.peek().put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(BulletList bulletList) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, LIST_E); contentArrayStack.push(new JSONArray()); Node child = bulletList.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } JSONArray cArray = contentArrayStack.pop(); nodeJSON.put(CONTENT, cArray); nodeJSON.put(IS_ORDERED_LIST, false); contentArrayStack.peek().put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(Code code) { JSONArray format = new JSONArray(); format.put(INLINE_CODE); format.put(textSB.length()); format.put(code.getLiteral().length()); formats.add(format); textSB.append(code.getLiteral()); } @Override public void visit(Document document) { //Ignore } @Override public void visit(Emphasis emphasis) { JSONArray format = getFormatArray(emphasis); if (format != null) { formats.add(format); } } @Override public void visit(FencedCodeBlock fencedCodeBlock) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, CODE_BLOCK_E); JSONArray cArray = new JSONArray(); String codeLiteral = fencedCodeBlock.getLiteral(); String[] codeLines = codeLiteral.split("\n"); for (String c : codeLines) { JSONObject contentJSONObject = new JSONObject(); contentJSONObject.put(TYPE, RAW_E); contentJSONObject.put(TEXT, c); cArray.put(contentJSONObject); } nodeJSON.put(CONTENT, cArray); contentArrayStack.peek().put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(HardLineBreak hardLineBreak) { //Ignore } @Override public void visit(Heading heading) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, HEADING_E); nodeJSON.put(LEVEL, heading.getLevel()); contentArrayStack.push(new JSONArray()); Node child = heading.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } JSONArray cArray = contentArrayStack.pop(); if (textSB.length() > 0) { JSONObject content = new JSONObject(); content.put(TYPE, RAW_E); content.put(TEXT, textSB.toString()); cArray.put(content); } convertToRawTextJSONObject(cArray); nodeJSON.put(CONTENT, cArray); contentArrayStack.peek().put(nodeJSON); formats = new ArrayList<>(); textSB.delete(0, textSB.length()); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(ThematicBreak thematicBreak) { //Not supported by Reddit } @Override public void visit(HtmlInline htmlInline) { //Not supported by Reddit } @Override public void visit(HtmlBlock htmlBlock) { //Not supported by Reddit } @Override public void visit(Image image) { } @Override public void visit(IndentedCodeBlock indentedCodeBlock) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, CODE_BLOCK_E); JSONArray cArray = new JSONArray(); String codeLiteral = indentedCodeBlock.getLiteral(); String[] codeLines = codeLiteral.split("\n"); for (String c : codeLines) { JSONObject contentJSONObject = new JSONObject(); contentJSONObject.put(TYPE, RAW_E); contentJSONObject.put(TEXT, c); cArray.put(contentJSONObject); } nodeJSON.put(CONTENT, cArray); contentArrayStack.peek().put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(Link link) { try { if (textSB.length() > 0) { JSONObject content = new JSONObject(); content.put(TYPE, TEXT_E); content.put(TEXT, textSB.toString()); if (!formats.isEmpty()) { JSONArray formatsArray = new JSONArray(); for (JSONArray f : formats) { formatsArray.put(f); } content.put(FORMAT, formatsArray); } contentArrayStack.peek().put(content); formats = new ArrayList<>(); textSB.delete(0, textSB.length()); } //Construct link object JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, LINK_E); Node child = link.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } nodeJSON.put(TEXT, textSB.toString()); //It will automatically escape the string. nodeJSON.put(URL, link.getDestination()); if (!formats.isEmpty()) { JSONArray formatsArray = new JSONArray(); for (JSONArray f : formats) { formatsArray.put(f); } nodeJSON.put(FORMAT, formatsArray); } contentArrayStack.peek().put(nodeJSON); formats = new ArrayList<>(); textSB.delete(0, textSB.length()); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(ListItem listItem) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, LIST_ITEM_E); contentArrayStack.push(new JSONArray()); Node child = listItem.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } JSONArray cArray = contentArrayStack.pop(); nodeJSON.put(CONTENT, cArray); contentArrayStack.peek().put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(OrderedList orderedList) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, LIST_E); contentArrayStack.push(new JSONArray()); Node child = orderedList.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } JSONArray cArray = contentArrayStack.pop(); nodeJSON.put(CONTENT, cArray); nodeJSON.put(IS_ORDERED_LIST, true); contentArrayStack.peek().put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(Paragraph paragraph) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, PARAGRAPH_E); contentArrayStack.push(new JSONArray()); Node child = paragraph.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } JSONArray cArray = contentArrayStack.pop(); if (textSB.length() > 0) { JSONObject content = new JSONObject(); content.put(TYPE, TEXT_E); content.put(TEXT, textSB.toString()); if (!formats.isEmpty()) { JSONArray formatsArray = new JSONArray(); for (JSONArray f : formats) { formatsArray.put(f); } content.put(FORMAT, formatsArray); } cArray.put(content); } nodeJSON.put(CONTENT, cArray); contentArrayStack.peek().put(nodeJSON); formats = new ArrayList<>(); textSB.delete(0, textSB.length()); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void visit(SoftLineBreak softLineBreak) { //Ignore } @Override public void visit(StrongEmphasis strongEmphasis) { JSONArray format = getFormatArray(strongEmphasis); if (format != null) { formats.add(format); } } @Override public void visit(Text text) { textSB.append(text.getLiteral()); } @Override public void visit(LinkReferenceDefinition linkReferenceDefinition) { //Not supported by Reddit } @Override public void visit(CustomBlock customBlock) { if (customBlock instanceof TableBlock) { try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, TABLE_E); Node child = customBlock.getFirstChild(); while (child != null) { if (child instanceof TableHead) { contentArrayStack.push(new JSONArray()); child.accept(this); JSONArray hArray = contentArrayStack.pop(); nodeJSON.put(TABLE_HEADER_CONTENT, hArray); } else if (child instanceof TableBody) { contentArrayStack.push(new JSONArray()); child.accept(this); JSONArray cArray = contentArrayStack.pop(); nodeJSON.put(CONTENT, cArray); } child = child.getNext(); } contentArrayStack.peek().put(nodeJSON); formats = new ArrayList<>(); textSB.delete(0, textSB.length()); } catch (JSONException e) { throw new RuntimeException(e); } } else if (customBlock instanceof UploadedImageBlock) { //Nothing is allowed inside this block. try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, IMAGE_E); nodeJSON.put(IMAGE_ID, ((UploadedImageBlock) customBlock).uploadeImage.imageUrlOrKey); nodeJSON.put(CONTENT, ((UploadedImageBlock) customBlock).uploadeImage.getCaption()); document.put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } else if (customBlock instanceof GiphyGifBlock) { //Nothing is allowed inside this block. try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, GIF_E); nodeJSON.put(IMAGE_ID, ((GiphyGifBlock) customBlock).giphyGif.id); document.put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } } @Override public void visit(CustomNode customNode) { if (customNode instanceof Superscript) { /* Superscript can still has inline spans, thus checking children's next node until the end. Superscript must use ^(), not ^ right now. */ Node child = customNode.getFirstChild(); if (child == null) { //It may be ^Superscript } else { while (child != null) { JSONArray format = getFormatArray(customNode); if (format != null) { formats.add(format); } Node next = child.getNext(); child.unlink(); child = next; } } } else if (customNode instanceof SpoilerNode) { //Spoiler cannot have styles try { JSONObject nodeJSON = new JSONObject(); nodeJSON.put(TYPE, SPOILER_E); JSONArray cArray = new JSONArray(); JSONObject contentJSONObject = new JSONObject(); contentJSONObject.put(TYPE, TEXT_E); contentJSONObject.put(TEXT, getAllText(customNode)); cArray.put(contentJSONObject); nodeJSON.put(CONTENT, cArray); contentArrayStack.peek().put(nodeJSON); } catch (JSONException e) { throw new RuntimeException(e); } } else if (customNode instanceof TableHead) { Node child = customNode.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } } else if (customNode instanceof TableBody) { Node child = customNode.getFirstChild(); while (child != null) { if (child instanceof TableRow) { contentArrayStack.push(new JSONArray()); child.accept(this); JSONArray array = contentArrayStack.pop(); contentArrayStack.peek().put(array); } child = child.getNext(); } } else if(customNode instanceof TableRow) { Node child = customNode.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } } else if (customNode instanceof TableCell) { try { JSONObject nodeJSON = new JSONObject(); contentArrayStack.push(new JSONArray()); Node child = customNode.getFirstChild(); while (child != null) { child.accept(this); child = child.getNext(); } JSONArray cArray = contentArrayStack.pop(); if (textSB.length() > 0) { JSONObject content = new JSONObject(); content.put(TYPE, TEXT_E); content.put(TEXT, textSB.toString()); if (!formats.isEmpty()) { JSONArray formatsArray = new JSONArray(); for (JSONArray f : formats) { formatsArray.put(f); } content.put(FORMAT, formatsArray); } cArray.put(content); } nodeJSON.put(CONTENT, cArray); if (((TableCell) customNode).getAlignment() == null) { nodeJSON.put(TABLE_CELL_ALIGNMENT, TABLE_CELL_ALIGNMENT_LEFT); } else { switch (((TableCell) customNode).getAlignment()) { case CENTER: nodeJSON.put(TABLE_CELL_ALIGNMENT, TABLE_CELL_ALIGNMENT_CENTER); break; case RIGHT: nodeJSON.put(TABLE_CELL_ALIGNMENT, TABLE_CELL_ALIGNMENT_RIGHT); break; default: nodeJSON.put(TABLE_CELL_ALIGNMENT, TABLE_CELL_ALIGNMENT_LEFT); break; } } contentArrayStack.peek().put(nodeJSON); formats = new ArrayList<>(); textSB.delete(0, textSB.length()); } catch (JSONException e) { throw new RuntimeException(e); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerAwareMovementMethod.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.graphics.RectF; import android.text.Layout; import android.text.Spannable; import android.text.style.ClickableSpan; import android.view.MotionEvent; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** * Extension of {@link EvenBetterLinkMovementMethod} that handles {@link SpoilerSpan}s */ public class SpoilerAwareMovementMethod extends EvenBetterLinkMovementMethod { private final RectF touchedLineBounds = new RectF(); @Override protected ClickableSpan findClickableSpanUnderTouch(TextView textView, Spannable text, MotionEvent event) { // A copy of super method. Logic for selecting correct clickable span was moved to selectClickableSpan // So we need to find the location in text where touch was made, regardless of whether the TextView // has scrollable text. That is, not the entire text is currently visible. int touchX = (int) event.getX(); int touchY = (int) event.getY(); // Ignore padding. touchX -= textView.getTotalPaddingLeft(); touchY -= textView.getTotalPaddingTop(); // Account for scrollable text. touchX += textView.getScrollX(); touchY += textView.getScrollY(); final Layout layout = textView.getLayout(); final int touchedLine = layout.getLineForVertical(touchY); final int touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX); touchedLineBounds.left = layout.getLineLeft(touchedLine); touchedLineBounds.top = layout.getLineTop(touchedLine); touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left; touchedLineBounds.bottom = layout.getLineBottom(touchedLine); if (touchedLineBounds.contains(touchX, touchY)) { // Find a ClickableSpan that lies under the touched area. final Object[] spans = text.getSpans(touchOffset, touchOffset, ClickableSpan.class); // BEGIN Infinity changed return selectClickableSpan(spans); // END Infinity changed } else { // Touch lies outside the line's horizontal bounds where no spans should exist. return null; } } /** * Select a span according to priorities: * 1. Hidden spoiler * 2. Non-spoiler span (i.e. link) * 3. Shown spoiler */ @Nullable private ClickableSpan selectClickableSpan(@NonNull Object[] spans) { SpoilerSpan spoilerSpan = null; ClickableSpan nonSpoilerSpan = null; for (int i = spans.length - 1; i >= 0; i--) { final Object span = spans[i]; if (span instanceof SpoilerSpan) { spoilerSpan = (SpoilerSpan) span; } else if (span instanceof ClickableSpan) { nonSpoilerSpan = (ClickableSpan) span; } } if (spoilerSpan != null && !spoilerSpan.isShowing()) { return spoilerSpan; } else if (nonSpoilerSpan != null){ return nonSpoilerSpan; } else { return spoilerSpan; } } @Override protected boolean dispatchUrlLongClick(TextView textView, ClickableSpan clickableSpan) { if (clickableSpan instanceof SpoilerSpan) { ((SpoilerSpan) clickableSpan).onLongClick(textView); return false; } return super.dispatchUrlLongClick(textView, clickableSpan); } @Override protected void highlightUrl(TextView textView, ClickableSpan clickableSpan, Spannable text) { if (clickableSpan instanceof SpoilerSpan) { return; } super.highlightUrl(textView, clickableSpan, text); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerClosingInlineProcessor.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.node.Node; import io.noties.markwon.inlineparser.InlineProcessor; /** * Parses spoiler closing markdown ({@code !<}) and creates {@link SpoilerNode SpoilerNodes}. * Relies on {@link SpoilerOpeningInlineProcessor} to handle opening. * * Implementation inspired by {@link io.noties.markwon.inlineparser.CloseBracketInlineProcessor} */ public class SpoilerClosingInlineProcessor extends InlineProcessor { @NonNull private final SpoilerOpeningBracketStorage spoilerOpeningBracketStorage; public SpoilerClosingInlineProcessor(@NonNull SpoilerOpeningBracketStorage spoilerOpeningBracketStorage) { this.spoilerOpeningBracketStorage = spoilerOpeningBracketStorage; } @Override public char specialCharacter() { return '!'; } @Nullable @Override protected Node parse() { index++; if (peek() != '<') { return null; } index++; SpoilerOpeningBracket spoilerOpeningBracket = spoilerOpeningBracketStorage.pop(block); if (spoilerOpeningBracket == null) { return null; } SpoilerNode spoilerNode = new SpoilerNode(); Node node = spoilerOpeningBracket.node.getNext(); while (node != null) { Node next = node.getNext(); spoilerNode.appendChild(node); node = next; } // Process delimiters such as emphasis inside spoiler processDelimiters(spoilerOpeningBracket.previousDelimiter); mergeChildTextNodes(spoilerNode); // We don't need the corresponding text node anymore, we turned it into a spoiler node spoilerOpeningBracket.node.unlink(); return spoilerNode; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerNode.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.node.CustomNode; public class SpoilerNode extends CustomNode { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerOpeningBracket.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.internal.Delimiter; import org.commonmark.node.Node; public class SpoilerOpeningBracket { /** * Node that contains spoiler opening markdown ({@code >!}). */ public final Node node; /** * Previous bracket. */ public final SpoilerOpeningBracket previous; /** * Previous delimiter (emphasis, etc) before this bracket. */ public final Delimiter previousDelimiter; public SpoilerOpeningBracket(Node node, SpoilerOpeningBracket previous, Delimiter previousDelimiter) { this.node = node; this.previous = previous; this.previousDelimiter = previousDelimiter; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerOpeningBracketStorage.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.Nullable; import org.commonmark.internal.Delimiter; import org.commonmark.node.Node; public class SpoilerOpeningBracketStorage { @Nullable private SpoilerOpeningBracket lastBracket; private Node currentBlock; public void clear() { lastBracket = null; } public void add(Node block, Node node, Delimiter lastDelimiter) { updateBlock(block); lastBracket = new SpoilerOpeningBracket(node, lastBracket, lastDelimiter); } @Nullable public SpoilerOpeningBracket pop(Node block) { updateBlock(block); SpoilerOpeningBracket bracket = lastBracket; if (bracket != null) { lastBracket = bracket.previous; } return bracket; } private void updateBlock(Node block) { if (block != currentBlock) { clear(); } currentBlock = block; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerOpeningInlineProcessor.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.node.Node; import org.commonmark.node.Text; import io.noties.markwon.inlineparser.InlineProcessor; /** * Parses spoiler opening markdown ({@code >!}). Relies on {@link SpoilerClosingInlineProcessor} * to handle closing and create {@link SpoilerNode SpoilerNodes}. */ public class SpoilerOpeningInlineProcessor extends InlineProcessor { @NonNull private final SpoilerOpeningBracketStorage spoilerOpeningBracketStorage; public SpoilerOpeningInlineProcessor(@NonNull SpoilerOpeningBracketStorage spoilerOpeningBracketStorage) { this.spoilerOpeningBracketStorage = spoilerOpeningBracketStorage; } @Override public char specialCharacter() { return '>'; } @Nullable @Override protected Node parse() { index++; if (peek() == '!') { index++; Text node = text(">!"); spoilerOpeningBracketStorage.add(block, node, lastDelimiter()); return node; } return null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerParserPlugin.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.widget.TextView; import androidx.annotation.NonNull; import org.commonmark.node.Block; import org.commonmark.node.BlockQuote; import org.commonmark.node.HtmlBlock; import org.commonmark.node.Node; import org.commonmark.parser.Parser; import java.util.ArrayList; import java.util.List; import java.util.Set; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.core.CorePlugin; import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; public class SpoilerParserPlugin extends AbstractMarkwonPlugin { private final int textColor; private final int backgroundColor; private final SpoilerOpeningBracketStorage spoilerOpeningBracketStorage = new SpoilerOpeningBracketStorage(); SpoilerParserPlugin(int textColor, int backgroundColor) { this.textColor = textColor; this.backgroundColor = backgroundColor; } public static SpoilerParserPlugin create(int textColor, int backgroundColor) { return new SpoilerParserPlugin(textColor, backgroundColor); } @Override public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { builder.setFactory(SpoilerNode.class, (config, renderProps) -> new SpoilerSpan(textColor, backgroundColor)); } @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder.on(SpoilerNode.class, new MarkwonVisitor.NodeVisitor<>() { int depth = 0; @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull SpoilerNode spoilerNode) { int start = visitor.length(); depth++; visitor.visitChildren(spoilerNode); depth--; if (depth == 0) { // don't add SpoilerSpans inside other SpoilerSpans visitor.setSpansForNode(spoilerNode, start); } } }); } @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { spoilerOpeningBracketStorage.clear(); } @Override public void configure(@NonNull Registry registry) { registry.require(MarkwonInlineParserPlugin.class, plugin -> { plugin.factoryBuilder() .addInlineProcessor(new SpoilerOpeningInlineProcessor(spoilerOpeningBracketStorage)); plugin.factoryBuilder() .addInlineProcessor(new SpoilerClosingInlineProcessor(spoilerOpeningBracketStorage)); } ); } @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customBlockParserFactory(new BlockQuoteWithExceptionParser.Factory()); Set> blocks = CorePlugin.enabledBlockTypes(); blocks.remove(HtmlBlock.class); blocks.remove(BlockQuote.class); builder.enabledBlockTypes(blocks); } @Override public void afterSetText(@NonNull TextView textView) { CharSequence text = textView.getText(); if (text instanceof Spanned) { Spanned spannedText = (Spanned) text; SpoilerSpan[] spans = spannedText.getSpans(0, text.length(), SpoilerSpan.class); if (spans.length == 0) { return; } // This is a workaround for Markwon's behavior. // Markwon adds spans in reversed order so SpoilerSpan is applied first // and other things (i.e. links, code, etc.) get drawn over it. // We fix it by removing all SpoilerSpans and adding them again // so they are applied last. List spanInfo = new ArrayList<>(spans.length); for (SpoilerSpan span : spans) { spanInfo.add(new SpanInfo( span, spannedText.getSpanStart(span), spannedText.getSpanEnd(span), spannedText.getSpanFlags(span) )); } SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text); for (SpanInfo info : spanInfo) { spannableStringBuilder.removeSpan(info.span); spannableStringBuilder.setSpan(info.span, info.start, info.end, info.flags); } textView.setText(spannableStringBuilder); } } private static class SpanInfo { public final SpoilerSpan span; public final int start; public final int end; public final int flags; private SpanInfo(SpoilerSpan span, int start, int end, int flags) { this.span = span; this.start = start; this.end = end; this.flags = flags; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SpoilerSpan.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.text.Layout; import android.text.Spannable; import android.text.TextPaint; import android.text.style.ClickableSpan; import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; import ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView; public class SpoilerSpan extends ClickableSpan { final int textColor; final int backgroundColor; private boolean isShowing = false; public SpoilerSpan(@NonNull int textColor, @NonNull int backgroundColor) { this.textColor = textColor; this.backgroundColor = backgroundColor; } @Override public void onClick(@NonNull View widget) { if (!(widget instanceof TextView)) { return; } final TextView textView = (TextView) widget; final Spannable spannable = (Spannable) textView.getText(); final int end = spannable.getSpanEnd(this); if (end < 0) { return; } final Layout layout = textView.getLayout(); if (layout == null) { return; } if (widget instanceof SpoilerOnClickTextView) { ((SpoilerOnClickTextView) textView).setSpoilerOnClick(true); } isShowing = !isShowing; widget.invalidate(); } public void onLongClick(@NonNull View widget) { if (widget instanceof SpoilerOnClickTextView) { ((SpoilerOnClickTextView) widget).setSpoilerOnClick(true); } } public boolean isShowing() { return isShowing; } @Override public void updateDrawState(@NonNull TextPaint ds) { if (isShowing) { ds.bgColor = backgroundColor & 0x0D000000; //Slightly darker background color for revealed spoiler super.updateDrawState(ds); } else { ds.bgColor = backgroundColor; } ds.setColor(textColor); ds.setUnderlineText(false); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/Superscript.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.node.CustomNode; import org.commonmark.node.Visitor; public class Superscript extends CustomNode { private boolean isBracketed; @Override public void accept(Visitor visitor) { visitor.visit(this); } public boolean isBracketed() { return isBracketed; } public void setBracketed(boolean bracketed) { isBracketed = bracketed; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptClosingInlineProcessor.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.node.Node; import io.noties.markwon.inlineparser.InlineProcessor; public class SuperscriptClosingInlineProcessor extends InlineProcessor { @NonNull private final SuperscriptOpeningStorage superscriptOpeningStorage; public SuperscriptClosingInlineProcessor(@NonNull SuperscriptOpeningStorage superscriptOpeningStorage) { this.superscriptOpeningStorage = superscriptOpeningStorage; } @Override public char specialCharacter() { return ')'; } @Nullable @Override protected Node parse() { SuperscriptOpeningBracket superscriptOpening = superscriptOpeningStorage.pop(block); if (superscriptOpening == null) { return null; } index++; Superscript superscript = new Superscript(); superscript.setBracketed(true); Node node = superscriptOpening.node.getNext(); while (node != null) { Node next = node.getNext(); superscript.appendChild(node); node = next; } // Process delimiters such as emphasis inside spoiler processDelimiters(superscriptOpening.previousDelimiter); mergeChildTextNodes(superscript); // We don't need the corresponding text node anymore, we turned it into a spoiler node superscriptOpening.node.unlink(); return superscript; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpening.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.node.Node; public class SuperscriptOpening { /** * Node that contains non-bracketed superscript opening markdown ({@code ^}). */ public final Node node; public final Integer start; public SuperscriptOpening(Node node, int start) { this.node = node; this.start = start; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningBracket.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.internal.Delimiter; import org.commonmark.node.Node; public class SuperscriptOpeningBracket { /** * Node that contains superscript opening bracket markdown ({@code ^(}). */ public final Node node; /** * Previous superscript opening bracket. */ public final SuperscriptOpeningBracket previous; /** * Previous delimiter (emphasis, etc) before this bracket. */ public final Delimiter previousDelimiter; public final Integer start; public SuperscriptOpeningBracket(Node node, SuperscriptOpeningBracket previous, Delimiter previousDelimiter) { this.node = node; this.previous = previous; this.previousDelimiter = previousDelimiter; this.start = null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningInlineProcessor.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.node.Node; import org.commonmark.node.Text; import io.noties.markwon.inlineparser.InlineProcessor; public class SuperscriptOpeningInlineProcessor extends InlineProcessor { @NonNull private final SuperscriptOpeningStorage superscriptOpeningStorage; public SuperscriptOpeningInlineProcessor(@NonNull SuperscriptOpeningStorage superscriptOpeningStorage) { this.superscriptOpeningStorage = superscriptOpeningStorage; } @Override public char specialCharacter() { return '^'; } @Nullable @Override protected Node parse() { index++; char c = peek(); if (c != '\0' && !Character.isWhitespace(c)) { if (c == '(') { index++; Text node = text("^("); superscriptOpeningStorage.add(block, node, lastDelimiter()); return node; } if (lastDelimiter() != null && lastDelimiter().canOpen && block.getLastChild() != null) { if (lastDelimiter().node == this.block.getLastChild()) { if (lastDelimiter().delimiterChar == peek()) { index--; return null; } } } return new Superscript(); } return null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningStorage.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.Nullable; import org.commonmark.internal.Delimiter; import org.commonmark.node.Node; public class SuperscriptOpeningStorage { @Nullable private SuperscriptOpeningBracket lastBracket; private Node currentBlock; public void clear() { lastBracket = null; } public void add(Node block, Node node, Delimiter lastDelimiter) { updateBlock(block); lastBracket = new SuperscriptOpeningBracket(node, lastBracket, lastDelimiter); } @Nullable public SuperscriptOpeningBracket pop(Node block) { updateBlock(block); SuperscriptOpeningBracket opening = lastBracket; if (opening != null) { lastBracket = opening.previous; } return opening; } private void updateBlock(Node block) { if (block != currentBlock) { clear(); } currentBlock = block; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptPlugin.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.text.Spannable; import android.text.Spanned; import android.widget.TextView; import androidx.annotation.NonNull; import org.commonmark.ext.gfm.tables.TableCell; import org.commonmark.node.Link; import org.commonmark.node.Node; import org.commonmark.node.Paragraph; import org.commonmark.node.Text; import java.util.ArrayList; import java.util.List; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.core.spans.CodeSpan; import io.noties.markwon.core.spans.TextViewSpan; import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; public class SuperscriptPlugin extends AbstractMarkwonPlugin { private final SuperscriptOpeningStorage superscriptOpeningBracketStorage; private final List superscriptOpeningList; SuperscriptPlugin() { this.superscriptOpeningBracketStorage = new SuperscriptOpeningStorage(); this.superscriptOpeningList = new ArrayList<>(); } @NonNull public static SuperscriptPlugin create() { return new SuperscriptPlugin(); } private static char peek(int index, CharSequence input) { return index >= 0 && index < input.length() ? input.charAt(index) : '\0'; } private static List getSpans(Spannable spannable, int start, int end) { var spanArray = spannable.getSpans(start, end, Object.class); List spanList = new ArrayList<>(); for (int i = spanArray.length - 1; i >= 0; i--) { Object span = spanArray[i]; int spanStart = spannable.getSpanStart(span); int spanEnd = spannable.getSpanEnd(span); int spanFlags = spannable.getSpanFlags(span); spanList.add(new SpanInfo(span, spanStart, spanEnd, spanFlags)); } return spanList; } private static SpanInfo matchSuperscriptAtPosition(List spans, int value) { for (var span : spans) if (span.what.getClass() == SuperscriptSpan.class && !((SuperscriptSpan) span.what).isBracketed && span.start <= value && value <= span.end) return span; return null; } private static SpanInfo matchSpanAtPosition(List spans, int value, Object spanClass) { for (var span : spans) if (span.what.getClass() == spanClass && span.start <= value && value <= span.end) return span; return null; } private static SpanInfo matchNonTextSpanAtBoundary(List spans, int value) { for (var span : spans) if ((span.end == value || span.start == value) && span.what.getClass() != CodeSpan.class && span.what.getClass() != SuperscriptSpan.class && span.what.getClass() != TextViewSpan.class) return span; return null; } @Override public void configure(@NonNull Registry registry) { registry.require(MarkwonInlineParserPlugin.class, plugin -> { plugin.factoryBuilder().addInlineProcessor(new SuperscriptOpeningInlineProcessor(superscriptOpeningBracketStorage)); plugin.factoryBuilder().addInlineProcessor(new SuperscriptClosingInlineProcessor(superscriptOpeningBracketStorage)); } ); } @Override public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { builder.setFactory(Superscript.class, (config, renderProps) -> new SuperscriptSpan(true)); } @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder.on(Superscript.class, new MarkwonVisitor.NodeVisitor<>() { int depth = 0; @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull Superscript superscript) { int start = visitor.length(); if (!notEmptySuperscript(superscript)) { return; } if (!superscript.isBracketed()) { visitor.builder().setSpan(new SuperscriptSpan(false), start, start + 1); // Workaround for Table Plugin superscriptOpeningList.add(new SuperscriptOpening(superscript, start)); return; } depth++; visitor.visitChildren(superscript); depth--; if (depth == 0) { int end = visitor.builder().length(); var spans = visitor.builder().getSpans(start, end); for (var span : spans) { if (span.what instanceof CodeSpan) { if (span.end <= end) { visitor.builder().setSpan(new SuperscriptSpan(true), start, span.start); } start = span.end; } } if (start < end) { visitor.setSpansForNode(superscript, start); } } } }); } private boolean notEmptyLink(Link link) { Node next = link.getFirstChild(); while (next != null) { if (next instanceof Text) { return true; } else if (next instanceof Superscript) { if (notEmptySuperscript((Superscript) next)) { return true; } } else if (next instanceof Link) { if (notEmptyLink((Link) next)) { return true; } } else if (next instanceof SpoilerNode) { if (notEmptySpoilerNode((SpoilerNode) next)) { return true; } } else { return true; } next = next.getNext(); } return false; } private boolean notEmptySpoilerNode(SpoilerNode spoilerNode) { Node next = spoilerNode.getFirstChild(); while (next != null) { if (next instanceof Text) { return true; } else if (next instanceof Superscript) { if (notEmptySuperscript((Superscript) next)) { return true; } } else if (next instanceof Link) { if (notEmptyLink((Link) next)) { return true; } } else if (next instanceof SpoilerNode) { if (notEmptySpoilerNode((SpoilerNode) next)) { return true; } } else { return true; } next = next.getNext(); } return false; } private boolean notEmptySuperscript(Superscript superscript) { Node next; if (superscript.isBracketed()) { next = superscript.getFirstChild(); } else { next = superscript.getNext(); } while (next != null) { if (next instanceof Link) { if (notEmptyLink((Link) next)) { return true; } } else if (next instanceof SpoilerNode) { if (notEmptySpoilerNode((SpoilerNode) next)) { return true; } } else if (!(next instanceof Superscript)) { return true; } else { if (notEmptySuperscript((Superscript) next)) { return true; } } next = next.getNext(); } return false; } @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { superscriptOpeningBracketStorage.clear(); } @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (superscriptOpeningList.isEmpty() || !(markdown instanceof Spannable)) { return; } var spannable = (Spannable) markdown; var spans = getSpans(spannable, 0, spannable.length()); final String text = spannable.toString(); outerLoop: for (int i = 0; i < superscriptOpeningList.size(); i++) { SuperscriptOpening opening = superscriptOpeningList.get(i); SuperscriptOpening nextOpening = i + 1 < superscriptOpeningList.size() ? superscriptOpeningList.get(i + 1) : null; // Workaround for Table Plugin var superscriptMarker = matchSuperscriptAtPosition(spans, opening.start); if (superscriptMarker == null) return; spannable.removeSpan(superscriptMarker.what); spans.remove(superscriptMarker); boolean isNextOpeningOfLocalNode = nextOpening != null && opening.node.getParent().equals(nextOpening.node.getParent()); if (opening.start >= text.length() || (matchSpanAtPosition(spans, opening.start, CodeSpan.class) == null && Character.isWhitespace(text.charAt(opening.start))) || (isNextOpeningOfLocalNode && opening.start.equals(nextOpening.start))) { superscriptOpeningList.remove(i); i--; continue; } boolean isChildOfDelimited = !(opening.node.getParent() == null || opening.node.getParent() instanceof Paragraph || opening.node.getParent() instanceof TableCell); int openingStart = opening.start; for (int j = opening.start; j <= text.length(); j++) { char currentChar = peek(j, text); SpanInfo codeSpanAtPosition = matchSpanAtPosition(spans, j, CodeSpan.class); SpanInfo nonTextSpanAtBoundary = matchNonTextSpanAtBoundary(spans, j); // When we reach the end position of, for example, an Emphasis // Check whether the superscript originated from inside this Emphasis // If so, stop further spanning of the current Superscript boolean isInsideDelimited = nonTextSpanAtBoundary != null && openingStart != j && j == nonTextSpanAtBoundary.end && (openingStart > nonTextSpanAtBoundary.start || isChildOfDelimited); if (codeSpanAtPosition != null) { if (openingStart < j) { spannable.setSpan(new SuperscriptSpan(false), openingStart, j, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } //Skip to end of CodeSpan j = codeSpanAtPosition.end; currentChar = peek(j, text); if (currentChar == '\0' || Character.isWhitespace(currentChar) || (isNextOpeningOfLocalNode && j == nextOpening.start) || isInsideDelimited) { superscriptOpeningList.remove(i); i--; continue outerLoop; } openingStart = j; } else if (currentChar == '\0' || Character.isWhitespace(currentChar) || isInsideDelimited) { spannable.setSpan(new SuperscriptSpan(false), openingStart, j, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); superscriptOpeningList.remove(i); i--; continue outerLoop; } } } } private static class SpanInfo { public final Object what; public final int start; public final int end; public final int flags; private SpanInfo(Object what, int start, int end, int flags) { this.what = what; this.start = start; this.end = end; this.flags = flags; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptSpan.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import android.text.TextPaint; import android.text.style.MetricAffectingSpan; import androidx.annotation.NonNull; public class SuperscriptSpan extends MetricAffectingSpan { private static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; public final boolean isBracketed; public SuperscriptSpan() { this.isBracketed = false; } public SuperscriptSpan(boolean isBracketed) { this.isBracketed = isBracketed; } @Override public void updateDrawState(TextPaint tp) { apply(tp); } @Override public void updateMeasureState(@NonNull TextPaint tp) { apply(tp); } private void apply(TextPaint paint) { paint.setTextSize(paint.getTextSize() * SCRIPT_DEF_TEXT_SIZE_RATIO); paint.baselineShift += (int) (paint.ascent() / 2); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/UploadedImageBlock.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import org.commonmark.node.CustomBlock; import ml.docilealligator.infinityforreddit.thing.UploadedImage; public class UploadedImageBlock extends CustomBlock { public UploadedImage uploadeImage; public UploadedImageBlock(UploadedImage uploadeImage) { this.uploadeImage = uploadeImage; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/UploadedImageBlockParser.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.Nullable; import org.commonmark.node.Block; import org.commonmark.parser.block.AbstractBlockParser; import org.commonmark.parser.block.AbstractBlockParserFactory; import org.commonmark.parser.block.BlockContinue; import org.commonmark.parser.block.BlockStart; import org.commonmark.parser.block.MatchedBlockParser; import org.commonmark.parser.block.ParserState; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import ml.docilealligator.infinityforreddit.thing.UploadedImage; public class UploadedImageBlockParser extends AbstractBlockParser { private final UploadedImageBlock uploadedImageBlock; UploadedImageBlockParser(UploadedImage uploadedImage) { this.uploadedImageBlock = new UploadedImageBlock(uploadedImage); } @Override public Block getBlock() { return uploadedImageBlock; } @Override public BlockContinue tryContinue(ParserState parserState) { return null; } public static class Factory extends AbstractBlockParserFactory { private final Pattern pattern = Pattern.compile("!\\[.*]\\(\\w+\\)"); @Nullable private Map uploadedImageMap; @Override public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { if (uploadedImageMap == null || uploadedImageMap.isEmpty()) { return BlockStart.none(); } String line = state.getLine().toString(); Matcher matcher = pattern.matcher(line); if (matcher.find()) { int startIndex = line.lastIndexOf('('); if (startIndex > 0) { int endIndex = line.indexOf(')', startIndex); String id = line.substring(startIndex + 1, endIndex); UploadedImage uploadedImage = uploadedImageMap.get(id); if (uploadedImage != null) { //![caption](id) String caption = line.substring(matcher.start() + 2, startIndex - 1); uploadedImage.setCaption(caption); return BlockStart.of(new UploadedImageBlockParser(uploadedImage)); } } } return BlockStart.none(); } public void setUploadedImages(@Nullable List uploadedImages) { if (uploadedImages == null) { return; } uploadedImageMap = new HashMap<>(); for (UploadedImage u : uploadedImages) { uploadedImageMap.put(u.imageUrlOrKey, u); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/markdown/UploadedImagePlugin.java ================================================ package ml.docilealligator.infinityforreddit.markdown; import androidx.annotation.NonNull; import org.commonmark.parser.Parser; import java.util.List; import io.noties.markwon.AbstractMarkwonPlugin; import ml.docilealligator.infinityforreddit.thing.UploadedImage; public class UploadedImagePlugin extends AbstractMarkwonPlugin { private final UploadedImageBlockParser.Factory factory; public UploadedImagePlugin() { this.factory = new UploadedImageBlockParser.Factory(); } @NonNull @Override public String processMarkdown(@NonNull String markdown) { return super.processMarkdown(markdown); } @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customBlockParserFactory(factory); } public void setUploadedImages(List uploadedImages) { factory.setUploadedImages(uploadedImages); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/ComposeMessage.java ================================================ package ml.docilealligator.infinityforreddit.message; import android.os.Handler; import java.io.IOException; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Response; import retrofit2.Retrofit; public class ComposeMessage { public static void composeMessage(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, Locale locale, String username, String subject, String message, ComposeMessageListener composeMessageListener) { executor.execute(() -> { Map headers = APIUtils.getOAuthHeader(accessToken); Map params = new HashMap<>(); params.put(APIUtils.API_TYPE_KEY, "json"); params.put(APIUtils.RETURN_RTJSON_KEY, "true"); params.put(APIUtils.SUBJECT_KEY, subject); params.put(APIUtils.TEXT_KEY, message); params.put(APIUtils.TO_KEY, username); try { Response response = oauthRetrofit.create(RedditAPI.class).composePrivateMessage(headers, params).execute(); if (response.isSuccessful()) { String errorMessage = ParseMessage.parseRepliedMessageErrorMessage(response.body()); if (errorMessage == null) { handler.post(composeMessageListener::composeMessageSuccess); } else { handler.post(() -> composeMessageListener.composeMessageFailed(errorMessage)); } } else { handler.post(() -> composeMessageListener.composeMessageFailed(response.message())); } } catch (IOException e) { e.printStackTrace(); handler.post(() -> composeMessageListener.composeMessageFailed(e.getMessage())); } }); } public interface ComposeMessageListener { void composeMessageSuccess(); void composeMessageFailed(String errorMessage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/FetchMessage.java ================================================ package ml.docilealligator.infinityforreddit.message; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.List; import java.util.Locale; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchMessage { public static final String WHERE_INBOX = "inbox"; public static final String WHERE_UNREAD = "unread"; public static final String WHERE_SENT = "sent"; public static final String WHERE_COMMENTS = "comments"; public static final String WHERE_MESSAGES = "messages"; public static final String WHERE_MESSAGES_DETAIL = "messages_detail"; public static final int MESSAGE_TYPE_INBOX = 0; public static final int MESSAGE_TYPE_PRIVATE_MESSAGE = 1; public static final int MESSAGE_TYPE_NOTIFICATION = 2; static void fetchInbox(Executor executor, Handler handler, Retrofit oauthRetrofit, Locale locale, String accessToken, String where, String after, int messageType, FetchMessagesListener fetchMessagesListener) { oauthRetrofit.create(RedditAPI.class).getMessages(APIUtils.getOAuthHeader(accessToken), where, after).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { try { JSONObject jsonResponse = new JSONObject(response.body()); JSONArray messageArray = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); List messages = ParseMessage.parseMessages(messageArray, locale, messageType); String newAfter = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.AFTER_KEY); handler.post(() -> fetchMessagesListener.fetchSuccess(messages, newAfter)); } catch (JSONException e) { e.printStackTrace(); handler.post(fetchMessagesListener::fetchFailed); } }); } else { fetchMessagesListener.fetchFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { fetchMessagesListener.fetchFailed(); } }); } interface FetchMessagesListener { void fetchSuccess(List messages, @Nullable String after); void fetchFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/Message.java ================================================ package ml.docilealligator.infinityforreddit.message; import android.os.Parcel; import android.os.Parcelable; import java.util.ArrayList; public class Message implements Parcelable { public static final String TYPE_COMMENT = "t1"; public static final String TYPE_ACCOUNT = "t2"; public static final String TYPE_LINK = "t3"; public static final String TYPE_MESSAGE = "t4"; public static final String TYPE_SUBREDDIT = "t5"; static final String TYPE_AWARD = "t6"; private final String kind; private final String subredditName; private final String subredditNamePrefixed; private final String id; private final String fullname; private final String subject; private final String author; private final String destination; private final String parentFullName; private final String title; private final String body; private final String context; private final String distinguished; private final String formattedTime; private final boolean wasComment; private boolean isNew; private final int score; private final int nComments; private final long timeUTC; private ArrayList replies; Message(String kind, String subredditName, String subredditNamePrefixed, String id, String fullname, String subject, String author, String destination, String parentFullName, String title, String body, String context, String distinguished, String formattedTime, boolean wasComment, boolean isNew, int score, int nComments, long timeUTC) { this.kind = kind; this.subredditName = subredditName; this.subredditNamePrefixed = subredditNamePrefixed; this.id = id; this.fullname = fullname; this.subject = subject; this.author = author; this.destination = destination; this.parentFullName = parentFullName; this.title = title; this.body = body; this.context = context; this.distinguished = distinguished; this.formattedTime = formattedTime; this.wasComment = wasComment; this.isNew = isNew; this.score = score; this.nComments = nComments; this.timeUTC = timeUTC; } protected Message(Parcel in) { kind = in.readString(); subredditName = in.readString(); subredditNamePrefixed = in.readString(); id = in.readString(); fullname = in.readString(); subject = in.readString(); author = in.readString(); destination = in.readString(); parentFullName = in.readString(); title = in.readString(); body = in.readString(); context = in.readString(); distinguished = in.readString(); formattedTime = in.readString(); wasComment = in.readByte() != 0; isNew = in.readByte() != 0; score = in.readInt(); nComments = in.readInt(); timeUTC = in.readLong(); replies = in.createTypedArrayList(Message.CREATOR); } public static final Creator CREATOR = new Creator() { @Override public Message createFromParcel(Parcel in) { return new Message(in); } @Override public Message[] newArray(int size) { return new Message[size]; } }; public String getKind() { return kind; } public String getSubredditName() { return subredditName; } public String getSubredditNamePrefixed() { return subredditNamePrefixed; } public String getId() { return id; } public String getFullname() { return fullname; } public String getSubject() { return subject; } public String getAuthor() { return author; } public String getRecipient(String accountName) { if (author == null || author.equals("null")) { if (subredditName == null || subredditName.equals("null")) { return destination; } else { return subredditName; } } else { return author.equals(accountName) ? (!destination.startsWith("#") ? destination : subredditName) : author; } } public boolean isRecipientASubreddit() { if (author == null || author.equals("null") || destination.startsWith("#")) { return (subredditName != null && !subredditName.equals("null")); } return false; } public boolean isAuthorDeleted() { return author != null && author.equals("[deleted]"); } public String getDestination() { return destination; } public boolean isDestinationDeleted() { return destination != null && destination.equals("[deleted]"); } public String getParentFullName() { return parentFullName; } public String getTitle() { return title; } public String getBody() { return body; } public String getContext() { return context; } public String getDistinguished() { return distinguished; } public String getFormattedTime() { return formattedTime; } public boolean wasComment() { return wasComment; } public boolean isNew() { return isNew; } public void setNew(boolean isNew) { this.isNew = isNew; } public int getScore() { return score; } public int getnComments() { return nComments; } public long getTimeUTC() { return timeUTC; } public ArrayList getReplies() { return replies; } public void setReplies(ArrayList replies) { this.replies = replies; } public void addReply(Message reply) { if (replies == null) { replies = new ArrayList<>(); } replies.add(reply); } public Message getDisplayedMessage() { if (replies != null && !replies.isEmpty() && replies.get(replies.size() - 1) != null) { return replies.get(replies.size() - 1); } else { return this; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(kind); parcel.writeString(subredditName); parcel.writeString(subredditNamePrefixed); parcel.writeString(id); parcel.writeString(fullname); parcel.writeString(subject); parcel.writeString(author); parcel.writeString(destination); parcel.writeString(parentFullName); parcel.writeString(title); parcel.writeString(body); parcel.writeString(context); parcel.writeString(distinguished); parcel.writeString(formattedTime); parcel.writeByte((byte) (wasComment ? 1 : 0)); parcel.writeByte((byte) (isNew ? 1 : 0)); parcel.writeInt(score); parcel.writeInt(nComments); parcel.writeLong(timeUTC); parcel.writeTypedList(replies); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/MessageDataSource.java ================================================ package ml.docilealligator.infinityforreddit.message; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.MutableLiveData; import androidx.paging.PageKeyedDataSource; import java.util.List; import java.util.Locale; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import retrofit2.Retrofit; class MessageDataSource extends PageKeyedDataSource { private final Executor executor; private final Handler handler; private final Retrofit oauthRetrofit; private final Locale locale; private final String accessToken; private final String where; private final int messageType; private final MutableLiveData paginationNetworkStateLiveData; private final MutableLiveData initialLoadStateLiveData; private final MutableLiveData hasPostLiveData; private LoadParams params; private LoadCallback callback; MessageDataSource(Executor executor, Handler handler, Retrofit oauthRetrofit, Locale locale, String accessToken, String where) { this.executor = executor; this.handler = handler; this.oauthRetrofit = oauthRetrofit; this.locale = locale; this.accessToken = accessToken; this.where = where; if (where.equals(FetchMessage.WHERE_MESSAGES)) { messageType = FetchMessage.MESSAGE_TYPE_PRIVATE_MESSAGE; } else { messageType = FetchMessage.MESSAGE_TYPE_INBOX; } paginationNetworkStateLiveData = new MutableLiveData<>(); initialLoadStateLiveData = new MutableLiveData<>(); hasPostLiveData = new MutableLiveData<>(); } MutableLiveData getPaginationNetworkStateLiveData() { return paginationNetworkStateLiveData; } MutableLiveData getInitialLoadStateLiveData() { return initialLoadStateLiveData; } MutableLiveData hasPostLiveData() { return hasPostLiveData; } void retryLoadingMore() { loadAfter(params, callback); } @Override public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) { initialLoadStateLiveData.postValue(NetworkState.LOADING); FetchMessage.fetchInbox(executor, handler, oauthRetrofit, locale, accessToken, where, null, messageType, new FetchMessage.FetchMessagesListener() { @Override public void fetchSuccess(List messages, @Nullable String after) { if (messages.isEmpty()) { hasPostLiveData.postValue(false); } else { hasPostLiveData.postValue(true); } if (after == null || after.isEmpty() || after.equals("null")) { callback.onResult(messages, null, null); } else { callback.onResult(messages, null, after); } initialLoadStateLiveData.postValue(NetworkState.LOADED); } @Override public void fetchFailed() { initialLoadStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetch messages")); } }); } @Override public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) { } @Override public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) { this.params = params; this.callback = callback; paginationNetworkStateLiveData.postValue(NetworkState.LOADING); FetchMessage.fetchInbox(executor, handler, oauthRetrofit, locale, accessToken, where, params.key, messageType, new FetchMessage.FetchMessagesListener() { @Override public void fetchSuccess(List messages, @Nullable String after) { if (after == null || after.isEmpty() || after.equals("null")) { callback.onResult(messages, null); } else { callback.onResult(messages, after); } paginationNetworkStateLiveData.postValue(NetworkState.LOADED); } @Override public void fetchFailed() { paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetching data")); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/MessageDataSourceFactory.java ================================================ package ml.docilealligator.infinityforreddit.message; import android.os.Handler; import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; import androidx.paging.DataSource; import java.util.Locale; import java.util.concurrent.Executor; import retrofit2.Retrofit; class MessageDataSourceFactory extends DataSource.Factory { private final Executor executor; private final Handler handler; private final Retrofit oauthRetrofit; private final Locale locale; private final String accessToken; private String where; private MessageDataSource messageDataSource; private final MutableLiveData messageDataSourceLiveData; MessageDataSourceFactory(Executor executor, Handler handler, Retrofit oauthRetrofit, Locale locale, String accessToken, String where) { this.executor = executor; this.handler = handler; this.oauthRetrofit = oauthRetrofit; this.locale = locale; this.accessToken = accessToken; this.where = where; messageDataSourceLiveData = new MutableLiveData<>(); } @NonNull @Override public DataSource create() { messageDataSource = new MessageDataSource(executor, handler, oauthRetrofit, locale, accessToken, where); messageDataSourceLiveData.postValue(messageDataSource); return messageDataSource; } public MutableLiveData getMessageDataSourceLiveData() { return messageDataSourceLiveData; } MessageDataSource getMessageDataSource() { return messageDataSource; } void changeWhere(String where) { this.where = where; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/MessageViewModel.java ================================================ package ml.docilealligator.infinityforreddit.message; import android.os.Handler; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; import java.util.Locale; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import retrofit2.Retrofit; public class MessageViewModel extends ViewModel { private final MessageDataSourceFactory messageDataSourceFactory; private final LiveData paginationNetworkState; private final LiveData initialLoadingState; private final LiveData hasMessageLiveData; private final LiveData> messages; private final MutableLiveData whereLiveData; public MessageViewModel(Executor executor, Handler handler, Retrofit retrofit, Locale locale, String accessToken, String where) { messageDataSourceFactory = new MessageDataSourceFactory(executor, handler, retrofit, locale, accessToken, where); initialLoadingState = Transformations.switchMap(messageDataSourceFactory.getMessageDataSourceLiveData(), MessageDataSource::getInitialLoadStateLiveData); paginationNetworkState = Transformations.switchMap(messageDataSourceFactory.getMessageDataSourceLiveData(), MessageDataSource::getPaginationNetworkStateLiveData); hasMessageLiveData = Transformations.switchMap(messageDataSourceFactory.getMessageDataSourceLiveData(), MessageDataSource::hasPostLiveData); whereLiveData = new MutableLiveData<>(where); PagedList.Config pagedListConfig = (new PagedList.Config.Builder()) .setEnablePlaceholders(false) .setPageSize(25) .build(); messages = Transformations.switchMap(whereLiveData, newWhere -> { messageDataSourceFactory.changeWhere(whereLiveData.getValue()); return (new LivePagedListBuilder(messageDataSourceFactory, pagedListConfig)).build(); }); } public LiveData> getMessages() { return messages; } public LiveData getPaginationNetworkState() { return paginationNetworkState; } public LiveData getInitialLoadingState() { return initialLoadingState; } public LiveData hasMessage() { return hasMessageLiveData; } public void refresh() { messageDataSourceFactory.getMessageDataSource().invalidate(); } public void retryLoadingMore() { messageDataSourceFactory.getMessageDataSource().retryLoadingMore(); } void changeWhere(String where) { whereLiveData.postValue(where); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final Locale locale; private final String accessToken; private final String where; public Factory(Executor executor, Handler handler, Retrofit retrofit, Locale locale, String accessToken, String where) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.locale = locale; this.accessToken = accessToken; this.where = where; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new MessageViewModel(executor, handler, retrofit, locale, accessToken, where); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/ParseMessage.java ================================================ package ml.docilealligator.infinityforreddit.message; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Locale; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ParseMessage { @WorkerThread public static ArrayList parseMessages(JSONArray messageArray, Locale locale, int messageType) { ArrayList messages = new ArrayList<>(); for (int i = 0; i < messageArray.length(); i++) { try { Message message = parseSingleMessage(messageArray.getJSONObject(i), locale, messageType); if (message != null) { messages.add(message); } } catch (JSONException e) { e.printStackTrace(); } } return messages; } @WorkerThread @Nullable public static Message parseSingleMessage(JSONObject messageJSON, Locale locale, int messageType) throws JSONException { String kind = messageJSON.getString(JSONUtils.KIND_KEY); if ((messageType == FetchMessage.MESSAGE_TYPE_INBOX && kind.equals("t4")) || (messageType == FetchMessage.MESSAGE_TYPE_PRIVATE_MESSAGE && !kind.equals("t4"))) { return null; } JSONObject rawMessageJSON = messageJSON.getJSONObject(JSONUtils.DATA_KEY); String subredditName = rawMessageJSON.getString(JSONUtils.SUBREDDIT_KEY); String subredditNamePrefixed = rawMessageJSON.getString(JSONUtils.SUBREDDIT_NAME_PREFIX_KEY); String id = rawMessageJSON.getString(JSONUtils.ID_KEY); String fullname = rawMessageJSON.getString(JSONUtils.NAME_KEY); String subject = rawMessageJSON.getString(JSONUtils.SUBJECT_KEY); String author = rawMessageJSON.getString(JSONUtils.AUTHOR_KEY); String destination = rawMessageJSON.getString(JSONUtils.DEST_KEY); String parentFullname = rawMessageJSON.getString(JSONUtils.PARENT_ID_KEY); String title = rawMessageJSON.has(JSONUtils.LINK_TITLE_KEY) ? rawMessageJSON.getString(JSONUtils.LINK_TITLE_KEY) : null; String body = Utils.modifyMarkdown(rawMessageJSON.getString(JSONUtils.BODY_KEY)); String context = rawMessageJSON.getString(JSONUtils.CONTEXT_KEY); String distinguished = rawMessageJSON.getString(JSONUtils.DISTINGUISHED_KEY); boolean wasComment = rawMessageJSON.getBoolean(JSONUtils.WAS_COMMENT_KEY); boolean isNew = rawMessageJSON.getBoolean(JSONUtils.NEW_KEY); int score = rawMessageJSON.getInt(JSONUtils.SCORE_KEY); int nComments = rawMessageJSON.isNull(JSONUtils.NUM_COMMENTS_KEY) ? -1 : rawMessageJSON.getInt(JSONUtils.NUM_COMMENTS_KEY); long timeUTC = rawMessageJSON.getLong(JSONUtils.CREATED_UTC_KEY) * 1000; Calendar submitTimeCalendar = Calendar.getInstance(); submitTimeCalendar.setTimeInMillis(timeUTC); String formattedTime = new SimpleDateFormat("MMM d, yyyy, HH:mm", locale).format(submitTimeCalendar.getTime()); ArrayList replies = null; if (!rawMessageJSON.isNull(JSONUtils.REPLIES_KEY) && rawMessageJSON.get(JSONUtils.REPLIES_KEY) instanceof JSONObject) { JSONArray repliesArray = rawMessageJSON.getJSONObject(JSONUtils.REPLIES_KEY).getJSONObject(JSONUtils.DATA_KEY) .getJSONArray(JSONUtils.CHILDREN_KEY); replies = parseMessages(repliesArray, locale, messageType); } Message message = new Message(kind, subredditName, subredditNamePrefixed, id, fullname, subject, author, destination, parentFullname, title, body, context, distinguished, formattedTime, wasComment, isNew, score, nComments, timeUTC); if (replies != null) { message.setReplies(replies); } return message; } @WorkerThread @Nullable public static String parseRepliedMessageErrorMessage(String response) { try { JSONObject responseObject = new JSONObject(response).getJSONObject(JSONUtils.JSON_KEY); if (responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() != 0) { JSONArray error = responseObject.getJSONArray(JSONUtils.ERRORS_KEY) .getJSONArray(responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() - 1); if (error.length() != 0) { String errorString; if (error.length() >= 2) { errorString = error.getString(1); } else { errorString = error.getString(0); } return errorString.substring(0, 1).toUpperCase() + errorString.substring(1); } else { return null; } } else { return null; } } catch (JSONException e) { e.printStackTrace(); } return null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/ReadMessage.java ================================================ package ml.docilealligator.infinityforreddit.message; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ReadMessage { public static void readMessage(Retrofit oauthRetrofit, String accessToken, String commaSeparatedFullnames, ReadMessageListener readMessageListener) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, commaSeparatedFullnames); oauthRetrofit.create(RedditAPI.class).readMessage(APIUtils.getOAuthHeader(accessToken), params) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { readMessageListener.readSuccess(); } else { readMessageListener.readFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { readMessageListener.readFailed(); } }); } public interface ReadMessageListener { void readSuccess(); void readFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/message/ReplyMessage.java ================================================ package ml.docilealligator.infinityforreddit.message; import android.os.Handler; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Response; import retrofit2.Retrofit; public class ReplyMessage { public static void replyMessage(Executor executor, Handler handler, String messageMarkdown, String thingFullname, Locale locale, Retrofit oauthRetrofit, String accessToken, ReplyMessageListener replyMessageListener) { executor.execute(() -> { Map headers = APIUtils.getOAuthHeader(accessToken); Map params = new HashMap<>(); params.put(APIUtils.API_TYPE_KEY, "json"); params.put(APIUtils.RETURN_RTJSON_KEY, "true"); params.put(APIUtils.TEXT_KEY, messageMarkdown); params.put(APIUtils.THING_ID_KEY, thingFullname); try { Response response = oauthRetrofit.create(RedditAPI.class).sendCommentOrReplyToMessage(headers, params).execute(); if (response.isSuccessful()) { try { JSONObject messageJSON = new JSONObject(response.body()).getJSONObject(JSONUtils.JSON_KEY) .getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.THINGS_KEY).getJSONObject(0); Message message = ParseMessage.parseSingleMessage(messageJSON, locale, FetchMessage.MESSAGE_TYPE_PRIVATE_MESSAGE); handler.post(() -> replyMessageListener.replyMessageSuccess(message)); } catch (JSONException e) { e.printStackTrace(); handler.post(() -> replyMessageListener.replyMessageFailed(ParseMessage.parseRepliedMessageErrorMessage(response.body()))); } } else { handler.post(() -> replyMessageListener.replyMessageFailed(response.message())); } } catch (IOException e) { e.printStackTrace(); handler.post(() -> replyMessageListener.replyMessageFailed(e.getMessage())); } }); } public interface ReplyMessageListener { void replyMessageSuccess(Message message); void replyMessageFailed(String errorMessage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/moderation/CommentModerationEvent.kt ================================================ package ml.docilealligator.infinityforreddit.moderation import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.comment.Comment sealed class CommentModerationEvent(open val comment: Comment, open val position: Int, val toastMessageResId: Int) { data class Approved(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.approved) data class ApproveFailed(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.approve_failed) data class Removed(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.removed) data class RemoveFailed(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.remove_failed) data class MarkedAsSpam(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.marked_as_spam) data class MarkAsSpamFailed(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.mark_as_spam_failed) data class Locked(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.locked) data class LockFailed(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.lock_failed) data class Unlocked(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.unlocked) data class UnlockFailed(override val comment: Comment, override val position: Int) : CommentModerationEvent(comment, position, R.string.unlock_failed) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/moderation/PostModerationEvent.kt ================================================ package ml.docilealligator.infinityforreddit.moderation import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.post.Post sealed class PostModerationEvent(open val post: Post, open val position: Int, val toastMessageResId: Int) { data class Approved(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.approved) data class ApproveFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.approve_failed) data class Removed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.removed) data class RemoveFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.remove_failed) data class MarkedAsSpam(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.marked_as_spam) data class MarkAsSpamFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.mark_as_spam_failed) data class SetStickyPost(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.set_sticky_post) data class SetStickyPostFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.set_sticky_post_failed) data class UnsetStickyPost(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unset_sticky_post) data class UnsetStickyPostFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unset_sticky_post_failed) data class Locked(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.locked) data class LockFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.lock_failed) data class Unlocked(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unlocked) data class UnlockFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unlock_failed) data class MarkedNSFW(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.mark_nsfw_success) data class MarkNSFWFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.mark_nsfw_failed) data class UnmarkedNSFW(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unmark_nsfw_success) data class UnmarkNSFWFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unmark_nsfw_failed) data class MarkedSpoiler(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.mark_spoiler_success) data class MarkSpoilerFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.mark_spoiler_failed) data class UnmarkedSpoiler(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unmark_spoiler_success) data class UnmarkSpoilerFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.unmark_spoiler_failed) data class DistinguishedAsMod(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.distinguished_as_mod) data class DistinguishAsModFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.distinguish_as_mod_failed) data class UndistinguishedAsMod(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.undistinguished_as_mod) data class UndistinguishAsModFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.undistinguish_as_mod_failed) data class SetReceiveNotification(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.reply_notifications_enabled) data class SetReceiveNotificationFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.toggle_reply_notifications_failed) data class UnsetReceiveNotification(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.reply_notifications_disabled) data class UnsetReceiveNotificationFailed(override val post: Post, override val position: Int) : PostModerationEvent(post, position, R.string.toggle_reply_notifications_failed) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/AnonymousMultiredditSubreddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import ml.docilealligator.infinityforreddit.account.Account; @Entity(tableName = "anonymous_multireddit_subreddits", primaryKeys = {"path", "username", "subreddit_name"}, foreignKeys = @ForeignKey(entity = MultiReddit.class, parentColumns = {"path", "username"}, childColumns = {"path", "username"}, onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE)) public class AnonymousMultiredditSubreddit { @NonNull @ColumnInfo(name = "path") private String path; @NonNull @ColumnInfo(name = "username") public String username = Account.ANONYMOUS_ACCOUNT; @NonNull @ColumnInfo(name = "subreddit_name") private String subredditName; @Nullable @ColumnInfo(name = "icon_url") private String iconUrl; public AnonymousMultiredditSubreddit(@NonNull String path, @NonNull String subredditName, @Nullable String iconUrl) { this.path = path; this.subredditName = subredditName; this.iconUrl = iconUrl; } @NonNull public String getPath() { return path; } public void setPath(@NonNull String path) { this.path = path; } @NonNull public String getSubredditName() { return subredditName; } public void setSubredditName(@NonNull String subredditName) { this.subredditName = subredditName; } @Nullable public String getIconUrl() { return iconUrl; } public void setIconUrl(@Nullable String iconUrl) { this.iconUrl = iconUrl; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/AnonymousMultiredditSubredditDao.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; @Dao public interface AnonymousMultiredditSubredditDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(AnonymousMultiredditSubreddit anonymousMultiredditSubreddit); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List anonymousMultiredditSubreddits); @Query("SELECT * FROM anonymous_multireddit_subreddits WHERE path = :path ORDER BY subreddit_name COLLATE NOCASE ASC") List getAllAnonymousMultiRedditSubreddits(String path); @Query("SELECT * FROM anonymous_multireddit_subreddits") List getAllSubreddits(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/AnonymousMultiredditSubredditDaoKt.kt ================================================ package ml.docilealligator.infinityforreddit.multireddit import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy @Dao interface AnonymousMultiredditSubredditDaoKt { @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) suspend fun insertAll(anonymousMultiredditSubreddits: MutableList) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/CreateMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class CreateMultiReddit { public interface CreateMultiRedditListener { void success(); void failed(int errorType); } public static void createMultiReddit(Executor executor, Handler handler, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, String accessToken, String multipath, String model, CreateMultiRedditListener createMultiRedditListener) { Map params = new HashMap<>(); params.put(APIUtils.MULTIPATH_KEY, multipath); params.put(APIUtils.MODEL_KEY, model); oauthRetrofit.create(RedditAPI.class).createMultiReddit(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ParseMultiReddit.parseAndSaveMultiReddit(executor, handler, response.body(), redditDataRoomDatabase, new ParseMultiReddit.ParseMultiRedditListener() { @Override public void success() { createMultiRedditListener.success(); } @Override public void failed() { createMultiRedditListener.failed(1); } }); } else { createMultiRedditListener.failed(response.code()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { createMultiRedditListener.failed(0); } }); } public static void anonymousCreateMultiReddit(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String multipath, String name, String description, List subreddits, CreateMultiRedditListener createMultiRedditListener) { executor.execute(() -> { if (!redditDataRoomDatabase.accountDao().isAnonymousAccountInserted()) { redditDataRoomDatabase.accountDao().insert(Account.getAnonymousAccount()); } if (redditDataRoomDatabase.multiRedditDao().getMultiReddit(multipath, Account.ANONYMOUS_ACCOUNT) == null) { redditDataRoomDatabase.multiRedditDao().insert(new MultiReddit(multipath, name, name, description, null, null, "private", Account.ANONYMOUS_ACCOUNT, 0, System.currentTimeMillis(), true, false, false)); List anonymousMultiredditSubreddits = new ArrayList<>(); for (ExpandedSubredditInMultiReddit s : subreddits) { anonymousMultiredditSubreddits.add(new AnonymousMultiredditSubreddit(multipath, s.getName(), s.getIconUrl())); } redditDataRoomDatabase.anonymousMultiredditSubredditDao().insertAll(anonymousMultiredditSubreddits); handler.post(createMultiRedditListener::success); } else { handler.post(() -> createMultiRedditListener.failed(0)); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/DeleteMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.DeleteMultiredditInDatabase; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class DeleteMultiReddit { public interface DeleteMultiRedditListener { void success(); void failed(); } public static void deleteMultiReddit(Executor executor, Handler handler, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, String multipath, DeleteMultiRedditListener deleteMultiRedditListener) { oauthRetrofit.create(RedditAPI.class).deleteMultiReddit(APIUtils.getOAuthHeader(accessToken), multipath).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { DeleteMultiredditInDatabase.deleteMultiredditInDatabase(executor, handler, redditDataRoomDatabase, accountName, multipath, deleteMultiRedditListener::success); } else { deleteMultiRedditListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { deleteMultiRedditListener.failed(); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/EditMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class EditMultiReddit { public interface EditMultiRedditListener { void success(); void failed(); } public static void editMultiReddit(Retrofit oauthRetrofit, String accessToken, String multipath, String model, EditMultiRedditListener editMultiRedditListener) { Map params = new HashMap<>(); params.put(APIUtils.MULTIPATH_KEY, multipath); params.put(APIUtils.MODEL_KEY, model); oauthRetrofit.create(RedditAPI.class).updateMultiReddit(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { editMultiRedditListener.success(); } else { editMultiRedditListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { editMultiRedditListener.failed(); } }); } public static void anonymousEditMultiReddit(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, MultiReddit multiReddit, EditMultiRedditListener editMultiRedditListener) { executor.execute(() -> { ArrayList anonymousMultiredditSubreddits = new ArrayList<>(); ArrayList subreddits = multiReddit.getSubreddits(); redditDataRoomDatabase.multiRedditDao().insert(multiReddit); for (ExpandedSubredditInMultiReddit s : subreddits) { anonymousMultiredditSubreddits.add(new AnonymousMultiredditSubreddit(multiReddit.getPath(), s.getName(), s.getIconUrl())); } redditDataRoomDatabase.anonymousMultiredditSubredditDao().insertAll(anonymousMultiredditSubreddits); handler.post(editMultiRedditListener::success); }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/ExpandedSubredditInMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; public class ExpandedSubredditInMultiReddit implements Parcelable { private String name; private String iconUrl; public ExpandedSubredditInMultiReddit(String name, String iconUrl) { this.name = name; this.iconUrl = iconUrl; } protected ExpandedSubredditInMultiReddit(Parcel in) { name = in.readString(); iconUrl = in.readString(); } public static final Creator CREATOR = new Creator<>() { @Override public ExpandedSubredditInMultiReddit createFromParcel(Parcel in) { return new ExpandedSubredditInMultiReddit(in); } @Override public ExpandedSubredditInMultiReddit[] newArray(int size) { return new ExpandedSubredditInMultiReddit[size]; } }; public String getName() { return name; } public String getIconUrl() { return iconUrl; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(name); dest.writeString(iconUrl); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/FavoriteMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Handler; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.InsertMultireddit; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FavoriteMultiReddit { public interface FavoriteMultiRedditListener { void success(); void failed(); } public static void favoriteMultiReddit(Executor executor, Handler handler, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, String accessToken, boolean makeFavorite, MultiReddit multiReddit, FavoriteMultiRedditListener favoriteMultiRedditListener) { Map params = new HashMap<>(); params.put(APIUtils.MULTIPATH_KEY, multiReddit.getPath()); params.put(APIUtils.MAKE_FAVORITE_KEY, String.valueOf(makeFavorite)); params.put(APIUtils.API_TYPE_KEY, APIUtils.API_TYPE_JSON); oauthRetrofit.create(RedditAPI.class).favoriteMultiReddit(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { multiReddit.setFavorite(makeFavorite); InsertMultireddit.insertMultireddit(executor, handler, redditDataRoomDatabase, multiReddit, favoriteMultiRedditListener::success); } else { favoriteMultiRedditListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { favoriteMultiRedditListener.failed(); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/FetchMultiRedditInfo.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchMultiRedditInfo { public interface FetchMultiRedditInfoListener { void success(MultiReddit multiReddit); void failed(); } public static void fetchMultiRedditInfo(Executor executor, Handler handler, Retrofit retrofit, String accessToken, String multipath, FetchMultiRedditInfoListener fetchMultiRedditInfoListener) { retrofit.create(RedditAPI.class).getMultiRedditInfo(APIUtils.getOAuthHeader(accessToken), multipath).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { MultiReddit multiReddit = parseMultiRedditInfo(response.body()); if (multiReddit != null) { handler.post(() -> fetchMultiRedditInfoListener.success(multiReddit)); } else { handler.post(fetchMultiRedditInfoListener::failed); } }); } else { fetchMultiRedditInfoListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchMultiRedditInfoListener.failed(); } }); } public static void anonymousFetchMultiRedditInfo(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String multipath, FetchMultiRedditInfoListener fetchMultiRedditInfoListener) { executor.execute(() -> { MultiReddit multiReddit = redditDataRoomDatabase.multiRedditDao().getMultiReddit(multipath, Account.ANONYMOUS_ACCOUNT); ArrayList anonymousMultiredditSubreddits = (ArrayList) redditDataRoomDatabase.anonymousMultiredditSubredditDao().getAllAnonymousMultiRedditSubreddits(multipath); ArrayList subreddits = new ArrayList<>(); for (AnonymousMultiredditSubreddit a : anonymousMultiredditSubreddits) { subreddits.add(new ExpandedSubredditInMultiReddit(a.getSubredditName(), a.getIconUrl())); } multiReddit.setSubreddits(subreddits); handler.post(() -> fetchMultiRedditInfoListener.success(multiReddit)); }); } @WorkerThread @Nullable public static MultiReddit parseMultiRedditInfo(String response) { try { JSONObject object = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY); String path = object.getString(JSONUtils.PATH_KEY); String displayName = object.getString(JSONUtils.DISPLAY_NAME_KEY); String name = object.getString(JSONUtils.NAME_KEY); String description = object.getString(JSONUtils.DESCRIPTION_MD_KEY); String copiedFrom = object.getString(JSONUtils.COPIED_FROM_KEY); String iconUrl = object.getString(JSONUtils.ICON_URL_KEY); String visibility = object.getString(JSONUtils.VISIBILITY_KEY); String owner = object.getString(JSONUtils.OWNER_KEY); int nSubscribers = object.getInt(JSONUtils.NUM_SUBSCRIBERS_KEY); long createdUTC = object.getLong(JSONUtils.CREATED_UTC_KEY); boolean over18 = object.getBoolean(JSONUtils.OVER_18_KEY); boolean isSubscriber = object.getBoolean(JSONUtils.IS_SUBSCRIBER_KEY); boolean isFavorite = object.getBoolean(JSONUtils.IS_FAVORITED_KEY); ArrayList subreddits = new ArrayList<>(); JSONArray subredditsArray = object.getJSONArray(JSONUtils.SUBREDDITS_KEY); for (int i = 0; i < subredditsArray.length(); i++) { try { JSONObject subredditData = subredditsArray.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); subreddits.add( new ExpandedSubredditInMultiReddit( subredditsArray.getJSONObject(i).getString(JSONUtils.NAME_KEY), subredditData.isNull(JSONUtils.COMMUNITY_ICON_KEY) ? subredditData.getString(JSONUtils.NAME_KEY) : subredditData.getString(JSONUtils.COMMUNITY_ICON_KEY) ) ); } catch (JSONException e) { e.printStackTrace(); } } return new MultiReddit(path, displayName, name, description, copiedFrom, iconUrl, visibility, owner, nSubscribers, createdUTC, over18, isSubscriber, isFavorite, subreddits); } catch (JSONException e) { e.printStackTrace(); return null; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/FetchMyMultiReddits.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchMyMultiReddits { public interface FetchMyMultiRedditsListener { void success(ArrayList multiReddits); void failed(); } public static void fetchMyMultiReddits(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, FetchMyMultiRedditsListener fetchMyMultiRedditsListener) { oauthRetrofit.create(RedditAPI.class) .getMyMultiReddits(APIUtils.getOAuthHeader(accessToken)).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ParseMultiReddit.parseMultiRedditsList(executor, handler, response.body(), new ParseMultiReddit.ParseMultiRedditsListListener() { @Override public void success(ArrayList multiReddits) { fetchMyMultiRedditsListener.success(multiReddits); } @Override public void failed() { fetchMyMultiRedditsListener.failed(); } }); } else { fetchMyMultiRedditsListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchMyMultiRedditsListener.failed(); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/MultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; import androidx.room.Index; import java.util.ArrayList; import java.util.stream.Collectors; import ml.docilealligator.infinityforreddit.account.Account; @Entity(tableName = "multi_reddits", primaryKeys = {"path", "username"}, foreignKeys = @ForeignKey(entity = Account.class, parentColumns = "username", childColumns = "username", onDelete = ForeignKey.CASCADE), indices = {@Index(value = "username")}) public class MultiReddit implements Parcelable { @NonNull @ColumnInfo(name = "path") private String path; @NonNull @ColumnInfo(name = "display_name") private String displayName; @NonNull @ColumnInfo(name = "name") private String name; @ColumnInfo(name = "description") private String description; @ColumnInfo(name = "copied_from") private String copiedFrom; @ColumnInfo(name = "icon_url") private String iconUrl; @ColumnInfo(name = "visibility") private String visibility; @NonNull @ColumnInfo(name = "username") private String owner; @ColumnInfo(name = "n_subscribers") private int nSubscribers; @ColumnInfo(name = "created_UTC") private long createdUTC; @ColumnInfo(name = "over_18") private boolean over18; @ColumnInfo(name = "is_subscriber") private boolean isSubscriber; @ColumnInfo(name = "is_favorite") private boolean isFavorite; @Ignore private ArrayList subreddits; public MultiReddit(@NonNull String path, @NonNull String displayName, @NonNull String name, String description, String copiedFrom, String iconUrl, String visibility, @NonNull String owner, int nSubscribers, long createdUTC, boolean over18, boolean isSubscriber, boolean isFavorite) { this.displayName = displayName; this.name = name; this.description = description; this.copiedFrom = copiedFrom; this.iconUrl = iconUrl; this.visibility = visibility; this.path = path; this.owner = owner; this.nSubscribers = nSubscribers; this.createdUTC = createdUTC; this.over18 = over18; this.isSubscriber = isSubscriber; this.isFavorite = isFavorite; } public MultiReddit(@NonNull String path, @NonNull String displayName, @NonNull String name, String description, String copiedFrom, String iconUrl, String visibility, @NonNull String owner, int nSubscribers, long createdUTC, boolean over18, boolean isSubscriber, boolean isFavorite, ArrayList subreddits) { this.displayName = displayName; this.name = name; this.description = description; this.copiedFrom = copiedFrom; this.iconUrl = iconUrl; this.visibility = visibility; this.path = path; this.owner = owner; this.nSubscribers = nSubscribers; this.createdUTC = createdUTC; this.over18 = over18; this.isSubscriber = isSubscriber; this.isFavorite = isFavorite; this.subreddits = subreddits; } protected MultiReddit(Parcel in) { path = in.readString(); displayName = in.readString(); name = in.readString(); description = in.readString(); copiedFrom = in.readString(); iconUrl = in.readString(); visibility = in.readString(); owner = in.readString(); nSubscribers = in.readInt(); createdUTC = in.readLong(); over18 = in.readByte() != 0; isSubscriber = in.readByte() != 0; isFavorite = in.readByte() != 0; subreddits = in.createTypedArrayList(ExpandedSubredditInMultiReddit.CREATOR); } public static final Creator CREATOR = new Creator<>() { @Override public MultiReddit createFromParcel(Parcel in) { return new MultiReddit(in); } @Override public MultiReddit[] newArray(int size) { return new MultiReddit[size]; } }; @NonNull public String getPath() { return path; } public void setPath(@NonNull String path) { this.path = path; } @NonNull public String getDisplayName() { return displayName; } public void setDisplayName(@NonNull String displayName) { this.displayName = displayName; } @NonNull public String getName() { return name; } public void setName(@NonNull String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getCopiedFrom() { return copiedFrom; } public void setCopiedFrom(String copiedFrom) { this.copiedFrom = copiedFrom; } public String getIconUrl() { return iconUrl; } public void setIconUrl(String iconUrl) { this.iconUrl = iconUrl; } public String getVisibility() { return visibility; } public void setVisibility(String visibility) { this.visibility = visibility; } public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } public int getNSubscribers() { return nSubscribers; } public void setNSubscribers(int nSubscribers) { this.nSubscribers = nSubscribers; } public long getCreatedUTC() { return createdUTC; } public void setCreatedUTC(long createdUTC) { this.createdUTC = createdUTC; } public boolean isOver18() { return over18; } public void setOver18(boolean over18) { this.over18 = over18; } public boolean isSubscriber() { return isSubscriber; } public void setSubscriber(boolean subscriber) { isSubscriber = subscriber; } public boolean isFavorite() { return isFavorite; } public void setFavorite(boolean favorite) { isFavorite = favorite; } public ArrayList getSubreddits() { return subreddits; } public void setSubreddits(ArrayList subreddits) { this.subreddits = subreddits; } public void setSubredditNames(ArrayList subredditNames) { this.subreddits = new ArrayList<>(subredditNames.stream().map(name -> new ExpandedSubredditInMultiReddit(name, null)).collect(Collectors.toList())); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(path); parcel.writeString(displayName); parcel.writeString(name); parcel.writeString(description); parcel.writeString(copiedFrom); parcel.writeString(iconUrl); parcel.writeString(visibility); parcel.writeString(owner); parcel.writeInt(nSubscribers); parcel.writeLong(createdUTC); parcel.writeByte((byte) (over18 ? 1 : 0)); parcel.writeByte((byte) (isSubscriber ? 1 : 0)); parcel.writeByte((byte) (isFavorite ? 1 : 0)); parcel.writeTypedList(subreddits); } @Nullable public static MultiReddit getDummyMultiReddit(@Nullable String multiPath) { if (multiPath == null) { return null; } return new MultiReddit(multiPath, multiPath.substring(multiPath.lastIndexOf("/", multiPath.length() - 2) + 1), multiPath, null, null, null, null, Account.ANONYMOUS_ACCOUNT, 0, 0, true, false, false); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/MultiRedditDao.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; @Dao public interface MultiRedditDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(MultiReddit multiReddit); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List multiReddits); @Query("SELECT * FROM multi_reddits WHERE username = :username AND display_name LIKE '%' || :searchQuery || '%' ORDER BY name COLLATE NOCASE ASC") LiveData> getAllMultiRedditsWithSearchQuery(String username, String searchQuery); @Query("SELECT * FROM multi_reddits WHERE username = :username ORDER BY name COLLATE NOCASE ASC") List getAllMultiRedditsList(String username); @Query("SELECT * FROM multi_reddits WHERE username = :username AND is_favorite AND display_name LIKE '%' || :searchQuery || '%' ORDER BY name COLLATE NOCASE ASC") LiveData> getAllFavoriteMultiRedditsWithSearchQuery(String username, String searchQuery); @Query("SELECT * FROM multi_reddits WHERE path = :path AND username = :username COLLATE NOCASE LIMIT 1") MultiReddit getMultiReddit(String path, String username); @Query("DELETE FROM multi_reddits WHERE name = :name AND username = :username") void deleteMultiReddit(String name, String username); @Query("DELETE FROM multi_reddits WHERE path = :path") void anonymousDeleteMultiReddit(String path); @Query("DELETE FROM multi_reddits WHERE username = :username") void deleteAllUserMultiReddits(String username); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/MultiRedditDaoKt.kt ================================================ package ml.docilealligator.infinityforreddit.multireddit import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @Dao interface MultiRedditDaoKt { @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) suspend fun insert(multiReddit: MultiReddit) @Query("SELECT * FROM multi_reddits WHERE path = :path AND username = :username COLLATE NOCASE LIMIT 1") suspend fun getMultiReddit(path: String, username: String): MultiReddit? } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/MultiRedditJSONModel.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import com.google.gson.Gson; import java.util.ArrayList; public class MultiRedditJSONModel { private String display_name; private String description_md; private String visibility; private SubredditInMultiReddit[] subreddits; public MultiRedditJSONModel() {} public MultiRedditJSONModel(String display_name, String description_md, boolean isPrivate, ArrayList subreddits) { this.display_name = display_name; this.description_md = description_md; if (isPrivate) { visibility = "private"; } else { visibility = "public"; } if (subreddits != null) { this.subreddits = new SubredditInMultiReddit[subreddits.size()]; for (int i = 0; i < subreddits.size(); i++) { SubredditInMultiReddit subredditInMultiReddit = new SubredditInMultiReddit(subreddits.get(i).getName()); this.subreddits[i] = subredditInMultiReddit; } } } public String createJSONModel() { Gson gson = new Gson(); return gson.toJson(this); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/MultiRedditRepository.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import androidx.lifecycle.LiveData; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class MultiRedditRepository { private final MultiRedditDao mMultiRedditDao; private final String mAccountName; MultiRedditRepository(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mMultiRedditDao = redditDataRoomDatabase.multiRedditDao(); mAccountName = accountName; } LiveData> getAllMultiRedditsWithSearchQuery(String searchQuery) { return mMultiRedditDao.getAllMultiRedditsWithSearchQuery(mAccountName, searchQuery); } LiveData> getAllFavoriteMultiRedditsWithSearchQuery(String searchQuery) { return mMultiRedditDao.getAllFavoriteMultiRedditsWithSearchQuery(mAccountName, searchQuery); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/MultiRedditViewModel.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class MultiRedditViewModel extends ViewModel { private final MultiRedditRepository mMultiRedditRepository; private final LiveData> mAllMultiReddits; private final LiveData> mAllFavoriteMultiReddits; private final MutableLiveData searchQueryLiveData; public MultiRedditViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mMultiRedditRepository = new MultiRedditRepository(redditDataRoomDatabase, accountName); searchQueryLiveData = new MutableLiveData<>(""); mAllMultiReddits = Transformations.switchMap(searchQueryLiveData, searchQuery -> mMultiRedditRepository.getAllMultiRedditsWithSearchQuery(searchQuery)); mAllFavoriteMultiReddits = Transformations.switchMap(searchQueryLiveData, searchQuery -> mMultiRedditRepository.getAllFavoriteMultiRedditsWithSearchQuery(searchQuery)); } public LiveData> getAllMultiReddits() { return mAllMultiReddits; } public LiveData> getAllFavoriteMultiReddits() { return mAllFavoriteMultiReddits; } public void setSearchQuery(String searchQuery) { searchQueryLiveData.postValue(searchQuery); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mAccountName; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mRedditDataRoomDatabase = redditDataRoomDatabase; mAccountName = accountName; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new MultiRedditViewModel(mRedditDataRoomDatabase, mAccountName); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/ParseMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; import android.os.Handler; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.utils.JSONUtils; public class ParseMultiReddit { interface ParseMultiRedditsListListener { void success(ArrayList multiReddits); void failed(); } interface ParseMultiRedditListener { void success(); void failed(); } public static void parseMultiRedditsList(Executor executor, Handler handler, String response, ParseMultiRedditsListListener parseMultiRedditsListListener) { executor.execute(() -> { try { JSONArray arrayResponse = new JSONArray(response); ArrayList multiReddits = new ArrayList<>(); for (int i = 0; i < arrayResponse.length(); i++) { try { multiReddits.add(parseMultiReddit(arrayResponse.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY))); } catch (JSONException e) { e.printStackTrace(); } } handler.post(() -> parseMultiRedditsListListener.success(multiReddits)); } catch (JSONException e) { e.printStackTrace(); handler.post(parseMultiRedditsListListener::failed); } }); } public static void parseAndSaveMultiReddit(Executor executor, Handler handler, String response, RedditDataRoomDatabase redditDataRoomDatabase, ParseMultiRedditListener parseMultiRedditListener) { executor.execute(() -> { try { MultiReddit multiReddit = parseMultiReddit(new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY)); redditDataRoomDatabase.multiRedditDao().insert(multiReddit); handler.post(parseMultiRedditListener::success); } catch (JSONException e) { e.printStackTrace(); handler.post(parseMultiRedditListener::failed); } }); } private static MultiReddit parseMultiReddit(JSONObject singleMultiRedditJSON) throws JSONException { String displayName = singleMultiRedditJSON.getString(JSONUtils.DISPLAY_NAME_KEY); String name = singleMultiRedditJSON.getString(JSONUtils.NAME_KEY); String description = singleMultiRedditJSON.getString(JSONUtils.DESCRIPTION_MD_KEY); int nSubscribers = singleMultiRedditJSON.getInt(JSONUtils.NUM_SUBSCRIBERS_KEY); String copiedFrom = singleMultiRedditJSON.getString(JSONUtils.COPIED_FROM_KEY); String iconUrl = singleMultiRedditJSON.getString(JSONUtils.ICON_URL_KEY); long createdUTC = singleMultiRedditJSON.getLong(JSONUtils.CREATED_UTC_KEY); String visibility = singleMultiRedditJSON.getString(JSONUtils.VISIBILITY_KEY); boolean over18 = singleMultiRedditJSON.getBoolean(JSONUtils.OVER_18_KEY); String path = singleMultiRedditJSON.getString(JSONUtils.PATH_KEY); String owner = singleMultiRedditJSON.getString(JSONUtils.OWNER_KEY); boolean isSubscriber = singleMultiRedditJSON.getBoolean(JSONUtils.IS_SUBSCRIBER_KEY); boolean isFavorited = singleMultiRedditJSON.getBoolean(JSONUtils.IS_FAVORITED_KEY); JSONArray subredditsArray = singleMultiRedditJSON.getJSONArray(JSONUtils.SUBREDDITS_KEY); ArrayList subreddits = new ArrayList<>(); for (int i = 0; i < subredditsArray.length(); i++) { JSONObject subredditData = subredditsArray.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); subreddits.add( new ExpandedSubredditInMultiReddit( subredditsArray.getJSONObject(i).getString(JSONUtils.NAME_KEY), subredditData.isNull(JSONUtils.COMMUNITY_ICON_KEY) ? subredditData.getString(JSONUtils.NAME_KEY) : subredditData.getString(JSONUtils.COMMUNITY_ICON_KEY) ) ); } return new MultiReddit(path, displayName, name, description, copiedFrom, iconUrl, visibility, owner, nSubscribers, createdUTC, over18, isSubscriber, isFavorited, subreddits); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/multireddit/SubredditInMultiReddit.java ================================================ package ml.docilealligator.infinityforreddit.multireddit; public class SubredditInMultiReddit { String name; SubredditInMultiReddit() {} SubredditInMultiReddit(String subredditName) { name = subredditName; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/network/AccessTokenAuthenticator.java ================================================ package ml.docilealligator.infinityforreddit.network; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.Authenticator; import okhttp3.Headers; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; import retrofit2.Call; import retrofit2.Retrofit; public class AccessTokenAuthenticator implements Authenticator { private static final String TAG = "AccessTokenAuth"; private final Retrofit mRetrofit; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final SharedPreferences mCurrentAccountSharedPreferences; private final String mClientId; public AccessTokenAuthenticator(String clientId, Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences currentAccountSharedPreferences) { mClientId = clientId; mRetrofit = retrofit; mRedditDataRoomDatabase = redditDataRoomDatabase; mCurrentAccountSharedPreferences = currentAccountSharedPreferences; } @Nullable @Override public Request authenticate(Route route, @NonNull Response response) { if (response.code() == 401) { String accessTokenHeader = response.request().header(APIUtils.AUTHORIZATION_KEY); if (accessTokenHeader == null) { return null; } String accessToken = accessTokenHeader.substring(APIUtils.AUTHORIZATION_BASE.length()); synchronized (this) { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); if (account == null) { return null; } String accessTokenFromDatabase = account.getAccessToken(); if (accessToken.equals(accessTokenFromDatabase)) { String newAccessToken = refreshAccessToken(account); if (!newAccessToken.isEmpty()) { return response.request().newBuilder().headers(Headers.of(APIUtils.getOAuthHeader(newAccessToken))).build(); } else { return null; } } else { return response.request().newBuilder().headers(Headers.of(APIUtils.getOAuthHeader(accessTokenFromDatabase))).build(); } } } return null; } private String refreshAccessToken(Account account) { String refreshToken = mRedditDataRoomDatabase.accountDao().getCurrentAccount().getRefreshToken(); RedditAPI api = mRetrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.GRANT_TYPE_KEY, APIUtils.GRANT_TYPE_REFRESH_TOKEN); params.put(APIUtils.REFRESH_TOKEN_KEY, refreshToken); // Construct header directly using the stored clientId Map authHeader = new HashMap<>(); String credentials = String.format("%s:%s", mClientId, ""); String auth = "Basic " + android.util.Base64.encodeToString(credentials.getBytes(), android.util.Base64.NO_WRAP); authHeader.put(APIUtils.AUTHORIZATION_KEY, auth); Call accessTokenCall = api.getAccessToken(authHeader, params); try { retrofit2.Response response = accessTokenCall.execute(); if (response.isSuccessful() && response.body() != null) { JSONObject jsonObject = new JSONObject(response.body()); String newAccessToken = jsonObject.getString(APIUtils.ACCESS_TOKEN_KEY); String newRefreshToken = jsonObject.has(APIUtils.REFRESH_TOKEN_KEY) ? jsonObject.getString(APIUtils.REFRESH_TOKEN_KEY) : null; if (newRefreshToken == null) { mRedditDataRoomDatabase.accountDao().updateAccessToken(account.getAccountName(), newAccessToken); } else { mRedditDataRoomDatabase.accountDao().updateAccessTokenAndRefreshToken(account.getAccountName(), newAccessToken, newRefreshToken); } if (mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCOUNT_NAME, Account.ANONYMOUS_ACCOUNT).equals(account.getAccountName())) { mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.ACCESS_TOKEN, newAccessToken).apply(); } return newAccessToken; } return ""; } catch (IOException | JSONException e) { e.printStackTrace(); } return ""; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/network/AnyAccountAccessTokenAuthenticator.java ================================================ package ml.docilealligator.infinityforreddit.network; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.Authenticator; import okhttp3.Headers; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; import retrofit2.Call; import retrofit2.Retrofit; public class AnyAccountAccessTokenAuthenticator implements Authenticator { private final Retrofit mRetrofit; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final Account mAccount; private final SharedPreferences mCurrentAccountSharedPreferences; private final String mClientId; public AnyAccountAccessTokenAuthenticator(String clientId, Retrofit retrofit, RedditDataRoomDatabase accountRoomDatabase, Account account, SharedPreferences currentAccountSharedPreferences) { mClientId = clientId; mRetrofit = retrofit; mRedditDataRoomDatabase = accountRoomDatabase; mAccount = account; mCurrentAccountSharedPreferences = currentAccountSharedPreferences; } @Nullable @Override public Request authenticate(Route route, @NonNull Response response) { if (response.code() == 401) { String accessTokenHeader = response.request().header(APIUtils.AUTHORIZATION_KEY); if (accessTokenHeader == null) { return null; } String accessToken = accessTokenHeader.substring(APIUtils.AUTHORIZATION_BASE.length()); synchronized (this) { if (mAccount == null) { return null; } String accessTokenFromDatabase = mAccount.getAccessToken(); if (accessToken.equals(accessTokenFromDatabase)) { String newAccessToken = refreshAccessToken(mAccount); if (!newAccessToken.equals("")) { return response.request().newBuilder().headers(Headers.of(APIUtils.getOAuthHeader(newAccessToken))).build(); } else { return null; } } else { return response.request().newBuilder().headers(Headers.of(APIUtils.getOAuthHeader(accessTokenFromDatabase))).build(); } } } return null; } private String refreshAccessToken(Account account) { String refreshToken = account.getRefreshToken(); RedditAPI api = mRetrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.GRANT_TYPE_KEY, APIUtils.GRANT_TYPE_REFRESH_TOKEN); params.put(APIUtils.REFRESH_TOKEN_KEY, refreshToken); Map authHeader = new HashMap<>(); String credentials = String.format("%s:%s", mClientId, ""); String auth = "Basic " + android.util.Base64.encodeToString(credentials.getBytes(), android.util.Base64.NO_WRAP); authHeader.put(APIUtils.AUTHORIZATION_KEY, auth); Call accessTokenCall = api.getAccessToken(authHeader, params); try { retrofit2.Response response = accessTokenCall.execute(); if (response.isSuccessful() && response.body() != null) { JSONObject jsonObject = new JSONObject(response.body()); String newAccessToken = jsonObject.getString(APIUtils.ACCESS_TOKEN_KEY); String newRefreshToken = jsonObject.has(APIUtils.REFRESH_TOKEN_KEY) ? jsonObject.getString(APIUtils.REFRESH_TOKEN_KEY) : null; if (newRefreshToken == null) { mRedditDataRoomDatabase.accountDao().updateAccessToken(account.getAccountName(), newAccessToken); } else { mRedditDataRoomDatabase.accountDao().updateAccessTokenAndRefreshToken(account.getAccountName(), newAccessToken, newRefreshToken); } Account currentAccount = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); if (currentAccount != null && mAccount.getAccountName().equals(currentAccount.getAccountName()) && mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCOUNT_NAME, Account.ANONYMOUS_ACCOUNT).equals(account.getAccountName())) { mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.ACCESS_TOKEN, newAccessToken).apply(); } return newAccessToken; } return ""; } catch (IOException | JSONException e) { e.printStackTrace(); } return ""; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/network/RedgifsAccessTokenAuthenticator.java ================================================ package ml.docilealligator.infinityforreddit.network; import android.content.SharedPreferences; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import ml.docilealligator.infinityforreddit.apis.RedgifsAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.Headers; import okhttp3.Interceptor; import okhttp3.Response; import retrofit2.Call; import retrofit2.Retrofit; import retrofit2.converter.scalars.ScalarsConverterFactory; public class RedgifsAccessTokenAuthenticator implements Interceptor { private final SharedPreferences mCurrentAccountSharedPreferences; public RedgifsAccessTokenAuthenticator(SharedPreferences currentAccountSharedPreferences) { this.mCurrentAccountSharedPreferences = currentAccountSharedPreferences; } private String refreshAccessToken() { // Check if existing token is valid APIUtils.RedgifsAuthToken currentToken = APIUtils.REDGIFS_TOKEN.get(); if (currentToken.isValid()) { return currentToken.token; } // Get new token from API Retrofit retrofit = new Retrofit.Builder() .baseUrl(APIUtils.REDGIFS_API_BASE_URI) .addConverterFactory(ScalarsConverterFactory.create()) .build(); RedgifsAPI api = retrofit.create(RedgifsAPI.class); Call accessTokenCall = api.getRedgifsTemporaryToken(); try { retrofit2.Response response = accessTokenCall.execute(); if (response.isSuccessful() && response.body() != null) { String newAccessToken = new JSONObject(response.body()).getString("token"); // Update both the atomic reference and shared preferences APIUtils.RedgifsAuthToken newToken = APIUtils.RedgifsAuthToken.expireIn1day(newAccessToken); APIUtils.REDGIFS_TOKEN.set(newToken); mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, newAccessToken).apply(); return newAccessToken; } return ""; } catch (IOException | JSONException e) { e.printStackTrace(); } return ""; } @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { Response response = chain.proceed(chain.request()); if (response.code() == 401 || response.code() == 400) { String accessTokenHeader = response.request().header(APIUtils.AUTHORIZATION_KEY); if (accessTokenHeader == null) { return response; } String accessToken = accessTokenHeader.substring(APIUtils.AUTHORIZATION_BASE.length() - 1).trim(); synchronized (this) { String accessTokenFromSharedPreferences = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, ""); if (accessToken.equals(accessTokenFromSharedPreferences)) { String newAccessToken = refreshAccessToken(); if (!newAccessToken.equals("")) { response.close(); return chain.proceed(response.request().newBuilder().headers(Headers.of(APIUtils.getRedgifsOAuthHeader(newAccessToken))).build()); } else { return response; } } else { response.close(); return chain.proceed(response.request().newBuilder().headers(Headers.of(APIUtils.getRedgifsOAuthHeader(accessTokenFromSharedPreferences))).build()); } } } return response; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/network/ServerAccessTokenAuthenticator.java ================================================ package ml.docilealligator.infinityforreddit.network; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.ServerAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.Authenticator; import okhttp3.Headers; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; import retrofit2.Call; import retrofit2.Retrofit; import retrofit2.converter.scalars.ScalarsConverterFactory; public class ServerAccessTokenAuthenticator implements Authenticator { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final SharedPreferences mCurrentAccountSharedPreferences; public ServerAccessTokenAuthenticator(RedditDataRoomDatabase redditDataRoomDatabase, SharedPreferences currentAccountSharedPreferences) { mRedditDataRoomDatabase = redditDataRoomDatabase; mCurrentAccountSharedPreferences = currentAccountSharedPreferences; } @Nullable @Override public Request authenticate(@Nullable Route route, @NonNull Response response) throws IOException { if (response.code() == 401) { String accessTokenHeader = response.request().header(APIUtils.AUTHORIZATION_KEY); if (accessTokenHeader == null) { return null; } String accessToken = accessTokenHeader.substring(APIUtils.AUTHORIZATION_BASE.length()); synchronized (this) { Account account = mRedditDataRoomDatabase.accountDao().getCurrentAccount(); if (account == null) { return null; } // TODO server access token String accessTokenFromDatabase = account.getAccessToken(); if (accessToken.equals(accessTokenFromDatabase)) { String newAccessToken = refreshAccessToken(account); if (!newAccessToken.isEmpty()) { return response.request().newBuilder().headers(Headers.of(APIUtils.getOAuthHeader(newAccessToken))).build(); } else { return null; } } else { return response.request().newBuilder().headers(Headers.of(APIUtils.getOAuthHeader(accessTokenFromDatabase))).build(); } } } return null; } private String refreshAccessToken(Account account) { Retrofit retrofit = new Retrofit.Builder() .baseUrl(APIUtils.SERVER_API_BASE_URI) .addConverterFactory(ScalarsConverterFactory.create()) .build(); // TODO server refresh token String refreshToken = mRedditDataRoomDatabase.accountDao().getCurrentAccount().getRefreshToken(); Call accessTokenCall = retrofit.create(ServerAPI.class).refreshAccessToken(account.getAccountName(), refreshToken); try { retrofit2.Response response = accessTokenCall.execute(); if (response.isSuccessful() && response.body() != null) { String newAccessToken = new JSONObject(response.body()).getString(APIUtils.ACCESS_TOKEN_KEY); mRedditDataRoomDatabase.accountDao().updateAccessToken(account.getAccountName(), newAccessToken); if (mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCOUNT_NAME, Account.ANONYMOUS_ACCOUNT).equals(account.getAccountName())) { // TODO server access token mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.ACCESS_TOKEN, newAccessToken).apply(); } return newAccessToken; } return ""; } catch (IOException | JSONException e) { e.printStackTrace(); } return ""; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/network/SortTypeConverter.java ================================================ package ml.docilealligator.infinityforreddit.network; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Converter; /** * A {@link Converter} for {@link SortType.Type sort type} and {@link SortType.Time sort time} to * {@link String} parameters */ public class SortTypeConverter implements Converter { /* package */ static SortTypeConverter INSTANCE = new SortTypeConverter<>(); @Nullable @Override public String convert(@NonNull T value) throws IOException { if (value instanceof SortType.Type) { return ((SortType.Type) value).value; } else if (value instanceof SortType.Time) { return ((SortType.Time) value).value; } else { return null; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/network/SortTypeConverterFactory.java ================================================ package ml.docilealligator.infinityforreddit.network; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Converter; import retrofit2.Retrofit; /** * A {@link Converter.Factory} for {@link SortType.Type sort type} and {@link SortType.Time sort time} to * {@link String} parameters */ public class SortTypeConverterFactory extends Converter.Factory { public static SortTypeConverterFactory create() { return new SortTypeConverterFactory(); } @Nullable @Override public Converter stringConverter(@NonNull Type type, @NonNull Annotation[] annotations, @NonNull Retrofit retrofit) { if (type == SortType.Type.class || type == SortType.Time.class) { return SortTypeConverter.INSTANCE; } return null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/FetchPost.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.IOException; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchPost { public static void fetchPost(Executor executor, Handler handler, Retrofit retrofit, String id, @Nullable String accessToken, @NonNull String accountName, FetchPostListener fetchPostListener) { Call postCall; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { postCall = retrofit.create(RedditAPI.class).getPost(id); } else { postCall = retrofit.create(RedditAPI.class).getPostOauth(id, APIUtils.getOAuthHeader(accessToken)); } postCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ParsePost.parsePost(executor, handler, response.body(), new ParsePost.ParsePostListener() { @Override public void onParsePostSuccess(Post post) { fetchPostListener.fetchPostSuccess(post); } @Override public void onParsePostFail() { fetchPostListener.fetchPostFailed(); } }); } else { fetchPostListener.fetchPostFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchPostListener.fetchPostFailed(); } }); } @WorkerThread @Nullable public static Post fetchPostSync(Retrofit retrofit, String id, @Nullable String accessToken, @NonNull String accountName) { Call postCall; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { postCall = retrofit.create(RedditAPI.class).getPost(id); } else { postCall = retrofit.create(RedditAPI.class).getPostOauth(id, APIUtils.getOAuthHeader(accessToken)); } try { Response response = postCall.execute(); if (response.isSuccessful()) { return ParsePost.parsePostSync(response.body()); } else { return null; } } catch (IOException e) { return null; } } public static void fetchRandomPost(Executor executor, Handler handler, Retrofit retrofit, boolean isNSFW, FetchRandomPostListener fetchRandomPostListener) { Call call; if (isNSFW) { call = retrofit.create(RedditAPI.class).getRandomNSFWPost(); } else { call = retrofit.create(RedditAPI.class).getRandomPost(); } call.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ParsePost.parseRandomPost(executor, handler, response.body(), isNSFW, new ParsePost.ParseRandomPostListener() { @Override public void onParseRandomPostSuccess(String postId, String subredditName) { fetchRandomPostListener.fetchRandomPostSuccess(postId, subredditName); } @Override public void onParseRandomPostFailed() { fetchRandomPostListener.fetchRandomPostFailed(); } }); } else { fetchRandomPostListener.fetchRandomPostFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchRandomPostListener.fetchRandomPostFailed(); } }); } public interface FetchPostListener { void fetchPostSuccess(Post post); void fetchPostFailed(); } public interface FetchRandomPostListener { void fetchRandomPostSuccess(String postId, String subredditName); void fetchRandomPostFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/FetchRules.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.subreddit.Rule; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchRules { public interface FetchRulesListener { void success(ArrayList rules); void failed(); } public static void fetchRules(Executor executor, Handler handler, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, String subredditName, FetchRulesListener fetchRulesListener) { RedditAPI api = retrofit.create(RedditAPI.class); Call rulesCall = accountName.equals(Account.ANONYMOUS_ACCOUNT) ? api.getRules(subredditName) : api.getRulesOauth(APIUtils.getOAuthHeader(accessToken), subredditName); rulesCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { parseRules(executor, handler, response.body(), new FetchRulesListener() { @Override public void success(ArrayList rules) { fetchRulesListener.success(rules); } @Override public void failed() { fetchRulesListener.failed(); } }); } else { fetchRulesListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fetchRulesListener.failed(); } }); } private static void parseRules(Executor executor, Handler handler, String response, FetchRulesListener fetchRulesListener) { executor.execute(() -> { try { JSONArray rulesArray = new JSONObject(response).getJSONArray(JSONUtils.RULES_KEY); ArrayList rules = new ArrayList<>(); for (int i = 0; i < rulesArray.length(); i++) { String shortName = rulesArray.getJSONObject(i).getString(JSONUtils.SHORT_NAME_KEY); String description = null; if (rulesArray.getJSONObject(i).has(JSONUtils.DESCRIPTION_KEY)) { description = Utils.modifyMarkdown(rulesArray.getJSONObject(i).getString(JSONUtils.DESCRIPTION_KEY)); } rules.add(new Rule(shortName, description)); } handler.post(() -> fetchRulesListener.success(rules)); } catch (JSONException e) { e.printStackTrace(); handler.post(fetchRulesListener::failed); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/FetchStreamableVideo.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.os.Handler; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.concurrent.Executor; import javax.inject.Provider; import ml.docilealligator.infinityforreddit.FetchVideoLinkListener; import ml.docilealligator.infinityforreddit.thing.StreamableVideo; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Response; public class FetchStreamableVideo { public static void fetchStreamableVideo(Executor executor, Handler handler, Provider streamableApiProvider, String videoUrl, FetchVideoLinkListener fetchVideoLinkListener) { executor.execute(() -> { try { Response response = streamableApiProvider.get().getStreamableData(videoUrl).execute(); if (response.isSuccessful()) { JSONObject jsonObject = new JSONObject(response.body()); String title = jsonObject.getString(JSONUtils.TITLE_KEY); JSONObject filesObject = jsonObject.getJSONObject(JSONUtils.FILES_KEY); StreamableVideo.Media mp4 = parseMedia(filesObject.getJSONObject(JSONUtils.MP4_KEY)); StreamableVideo.Media mp4MobileTemp = null; if (filesObject.has(JSONUtils.MP4_MOBILE_KEY)) { mp4MobileTemp = parseMedia(filesObject.getJSONObject(JSONUtils.MP4_MOBILE_KEY)); } if (mp4 == null && mp4MobileTemp == null) { handler.post(() -> fetchVideoLinkListener.failed(null)); return; } StreamableVideo.Media mp4Mobile = mp4MobileTemp; handler.post(() -> fetchVideoLinkListener.onFetchStreamableVideoLinkSuccess(new StreamableVideo(title, mp4, mp4Mobile))); } else { handler.post(() -> fetchVideoLinkListener.failed(null)); } } catch (IOException | JSONException e) { e.printStackTrace(); handler.post(() -> fetchVideoLinkListener.failed(null)); } }); } @WorkerThread @Nullable public static StreamableVideo fetchStreamableVideoSync(Provider streamableApiProvider, String videoUrl) { try { Response response = streamableApiProvider.get().getStreamableData(videoUrl).execute(); if (response.isSuccessful()) { JSONObject jsonObject = new JSONObject(response.body()); String title = jsonObject.getString(JSONUtils.TITLE_KEY); JSONObject filesObject = jsonObject.getJSONObject(JSONUtils.FILES_KEY); StreamableVideo.Media mp4 = parseMedia(filesObject.getJSONObject(JSONUtils.MP4_KEY)); StreamableVideo.Media mp4MobileTemp = null; if (filesObject.has(JSONUtils.MP4_MOBILE_KEY)) { mp4MobileTemp = parseMedia(filesObject.getJSONObject(JSONUtils.MP4_MOBILE_KEY)); } if (mp4 == null && mp4MobileTemp == null) { return null; } StreamableVideo.Media mp4Mobile = mp4MobileTemp; return new StreamableVideo(title, mp4, mp4Mobile); } else { return null; } } catch (IOException | JSONException e) { e.printStackTrace(); return null; } } public static void fetchStreamableVideoInRecyclerViewAdapter(Executor executor, Handler handler, Call streamableCall, FetchVideoLinkListener fetchVideoLinkListener) { executor.execute(() -> { try { Response response = streamableCall.execute(); if (response.isSuccessful()) { JSONObject jsonObject = new JSONObject(response.body()); String title = jsonObject.getString(JSONUtils.TITLE_KEY); JSONObject filesObject = jsonObject.getJSONObject(JSONUtils.FILES_KEY); StreamableVideo.Media mp4 = parseMedia(filesObject.getJSONObject(JSONUtils.MP4_KEY)); StreamableVideo.Media mp4MobileTemp = null; if (filesObject.has(JSONUtils.MP4_MOBILE_KEY)) { mp4MobileTemp = parseMedia(filesObject.getJSONObject(JSONUtils.MP4_MOBILE_KEY)); } if (mp4 == null && mp4MobileTemp == null) { handler.post(() -> fetchVideoLinkListener.failed(null)); return; } StreamableVideo.Media mp4Mobile = mp4MobileTemp; handler.post(() -> fetchVideoLinkListener.onFetchStreamableVideoLinkSuccess(new StreamableVideo(title, mp4, mp4Mobile))); } else { handler.post(() -> fetchVideoLinkListener.failed(null)); } } catch (IOException | JSONException e) { e.printStackTrace(); handler.post(() -> fetchVideoLinkListener.failed(null)); } }); } @Nullable private static StreamableVideo.Media parseMedia(JSONObject jsonObject) { try { return new StreamableVideo.Media( jsonObject.getString(JSONUtils.URL_KEY), jsonObject.getInt(JSONUtils.WIDTH_KEY), jsonObject.getInt(JSONUtils.HEIGHT_KEY)); } catch (JSONException e) { e.printStackTrace(); return null; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/HidePost.java ================================================ package ml.docilealligator.infinityforreddit.post; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class HidePost { public static void hidePost(Retrofit oauthRetrofit, String accessToken, String fullname, HidePostListener hidePostListener) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, fullname); oauthRetrofit.create(RedditAPI.class).hide(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { hidePostListener.success(); } else { hidePostListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { hidePostListener.failed(); } }); } public static void unhidePost(Retrofit oauthRetrofit, String accessToken, String fullname, HidePostListener hidePostListener) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, fullname); oauthRetrofit.create(RedditAPI.class).unhide(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { hidePostListener.success(); } else { hidePostListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { hidePostListener.failed(); } }); } public interface HidePostListener { void success(); void failed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/HistoryPostPagingSource.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.paging.ListenableFuturePagingSource; import androidx.paging.PagingState; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.readpost.NullReadPostsList; import ml.docilealligator.infinityforreddit.readpost.ReadPost; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.HttpException; import retrofit2.Response; import retrofit2.Retrofit; public class HistoryPostPagingSource extends ListenableFuturePagingSource { public static final int TYPE_READ_POSTS = 100; private final Retrofit retrofit; private final Executor executor; private final RedditDataRoomDatabase redditDataRoomDatabase; private final String accessToken; private final String accountName; private final SharedPreferences sharedPreferences; private final String username; private final int postType; private final PostFilter postFilter; public HistoryPostPagingSource(Retrofit retrofit, Executor executor, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, String username, int postType, PostFilter postFilter) { this.retrofit = retrofit; this.executor = executor; this.redditDataRoomDatabase = redditDataRoomDatabase; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.username = username; this.postType = postType; this.postFilter = postFilter; } @Nullable @Override public String getRefreshKey(@NonNull PagingState pagingState) { return null; } @NonNull @Override public ListenableFuture> loadFuture(@NonNull LoadParams loadParams) { if (postType == TYPE_READ_POSTS) { return loadHomePosts(loadParams, redditDataRoomDatabase); } else { return loadHomePosts(loadParams, redditDataRoomDatabase); } } public LoadResult transformData(List readPosts) { StringBuilder ids = new StringBuilder(); long lastItem = 0; for (ReadPost readPost : readPosts) { ids.append("t3_").append(readPost.getId()).append(","); lastItem = readPost.getTime(); } if (ids.length() > 0) { ids.deleteCharAt(ids.length() - 1); } Call historyPosts; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { historyPosts = retrofit.create(RedditAPI.class).getInfo(ids.toString()); } else { historyPosts = retrofit.create(RedditAPI.class).getInfoOauth(ids.toString(), APIUtils.getOAuthHeader(accessToken)); } try { Response response = historyPosts.execute(); if (response.isSuccessful()) { String responseString = response.body(); LinkedHashSet newPosts = ParsePost.parsePostsSync(responseString, -1, postFilter, NullReadPostsList.getInstance()); if (newPosts == null) { return new LoadResult.Error<>(new Exception("Error parsing posts")); } else { if (newPosts.size() < 25) { return new LoadResult.Page<>(new ArrayList<>(newPosts), null, null); } return new LoadResult.Page<>(new ArrayList<>(newPosts), null, Long.toString(lastItem)); } } else { return new LoadResult.Error<>(new Exception("Response failed")); } } catch (IOException e) { e.printStackTrace(); return new LoadResult.Error<>(new Exception("Response failed")); } } private ListenableFuture> loadHomePosts(@NonNull LoadParams loadParams, RedditDataRoomDatabase redditDataRoomDatabase) { Long before = loadParams.getKey() != null ? Long.parseLong(loadParams.getKey()) : null; ListenableFuture> readPosts = redditDataRoomDatabase.readPostDao().getAllReadPostsListenableFuture(username, before); ListenableFuture> pageFuture = Futures.transform(readPosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/HistoryPostViewModel.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelKt; import androidx.lifecycle.ViewModelProvider; import androidx.paging.Pager; import androidx.paging.PagingConfig; import androidx.paging.PagingData; import androidx.paging.PagingLiveData; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import retrofit2.Retrofit; public class HistoryPostViewModel extends ViewModel { private final Executor executor; private final Retrofit retrofit; private final RedditDataRoomDatabase redditDataRoomDatabase; private final String accessToken; private final String accountName; private final SharedPreferences sharedPreferences; private final int postType; private final PostFilter postFilter; private final LiveData> posts; private final MutableLiveData postFilterLiveData; public HistoryPostViewModel(Executor executor, Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, int postType, PostFilter postFilter) { this.executor = executor; this.retrofit = retrofit; this.redditDataRoomDatabase = redditDataRoomDatabase; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postType = postType; this.postFilter = postFilter; postFilterLiveData = new MutableLiveData<>(postFilter); Pager pager = new Pager<>(new PagingConfig(25, 4, false, 10), this::returnPagingSource); posts = Transformations.switchMap(postFilterLiveData, postFilterValue -> PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), ViewModelKt.getViewModelScope(this))); } public LiveData> getPosts() { return posts; } public HistoryPostPagingSource returnPagingSource() { HistoryPostPagingSource historyPostPagingSource; switch (postType) { case HistoryPostPagingSource.TYPE_READ_POSTS: historyPostPagingSource = new HistoryPostPagingSource(retrofit, executor, redditDataRoomDatabase, accessToken, accountName, sharedPreferences, accountName, postType, postFilter); break; default: historyPostPagingSource = new HistoryPostPagingSource(retrofit, executor, redditDataRoomDatabase, accessToken, accountName, sharedPreferences, accountName, postType, postFilter); break; } return historyPostPagingSource; } public void changePostFilter(PostFilter postFilter) { postFilterLiveData.postValue(postFilter); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Executor executor; private final Retrofit retrofit; private final RedditDataRoomDatabase redditDataRoomDatabase; private final String accessToken; private final String accountName; private final SharedPreferences sharedPreferences; private final int postType; private final PostFilter postFilter; public Factory(Executor executor, Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, int postType, PostFilter postFilter) { this.executor = executor; this.retrofit = retrofit; this.redditDataRoomDatabase = redditDataRoomDatabase; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postType = postType; this.postFilter = postFilter; } @NonNull @Override public T create(@NonNull Class modelClass) { if (postType == HistoryPostPagingSource.TYPE_READ_POSTS) { return (T) new HistoryPostViewModel(executor, retrofit, redditDataRoomDatabase, accessToken, accountName, sharedPreferences, postType, postFilter); } else { return (T) new HistoryPostViewModel(executor, retrofit, redditDataRoomDatabase, accessToken, accountName, sharedPreferences, postType, postFilter); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/ImgurMedia.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.os.Parcel; import android.os.Parcelable; public class ImgurMedia implements Parcelable { public static final int TYPE_IMAGE = 0; public static final int TYPE_VIDEO = 1; private String id; private final String title; private final String description; private final String link; private int type; public ImgurMedia(String id, String title, String description, String type, String link) { this.id = id; this.title = title; this.description = description; if (type.contains("mp4")) { this.type = TYPE_VIDEO; } else { this.type = TYPE_IMAGE; } this.link = link; } protected ImgurMedia(Parcel in) { title = in.readString(); description = in.readString(); link = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public ImgurMedia createFromParcel(Parcel in) { return new ImgurMedia(in); } @Override public ImgurMedia[] newArray(int size) { return new ImgurMedia[size]; } }; public String getId() { return id; } public String getTitle() { return title; } public String getDescription() { return description; } public int getType() { return type; } public String getLink() { return link; } public String getFileName() { if (type == TYPE_VIDEO) { return "Imgur-" + id + ".mp4"; } return "Imgur-" + id + ".jpg"; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(title); parcel.writeString(description); parcel.writeString(link); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/LoadingMorePostsStatus.java ================================================ package ml.docilealligator.infinityforreddit.post; import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @IntDef({LoadingMorePostsStatus.LOADING, LoadingMorePostsStatus.FAILED, LoadingMorePostsStatus.NO_MORE_POSTS, LoadingMorePostsStatus.NOT_LOADING}) @Retention(RetentionPolicy.SOURCE) public @interface LoadingMorePostsStatus { int LOADING = 0; int FAILED = 1; int NO_MORE_POSTS = 2; int NOT_LOADING = 3; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/MarkPostAsReadInterface.java ================================================ package ml.docilealligator.infinityforreddit.post; public interface MarkPostAsReadInterface { void markPostAsRead(Post post); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/ParsePost.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.net.Uri; import android.os.Handler; import android.text.Html; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.Utils; /** * Created by alex on 3/21/18. */ public class ParsePost { @WorkerThread public static LinkedHashSet parsePostsSync(String response, int nPosts, PostFilter postFilter, @Nullable ReadPostsListInterface readPostsList) { LinkedHashSet newPosts = new LinkedHashSet<>(); try { JSONObject jsonResponse = new JSONObject(response); JSONArray allPostsData = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); //Posts listing int numberOfPosts = (nPosts < 0 || nPosts > allPostsData.length()) ? allPostsData.length() : nPosts; ArrayList newPostsIds = new ArrayList<>(); for (int i = 0; i < numberOfPosts; i++) { try { if (!allPostsData.getJSONObject(i).getString(JSONUtils.KIND_KEY).equals("t3")) { continue; } JSONObject data = allPostsData.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); Post post = parseBasicData(data); if (PostFilter.isPostAllowed(post, postFilter)) { newPosts.add(post); newPostsIds.add(post.getId()); } } catch (JSONException e) { e.printStackTrace(); } } if (readPostsList != null) { Set readPostsIds = readPostsList.getReadPostsIdsByIds(newPostsIds); for (Post post: newPosts) { if (readPostsIds.contains(post.getId())) { post.markAsRead(); } } } return newPosts; } catch (JSONException e) { e.printStackTrace(); return null; } } public static String getLastItem(String response) { try { JSONObject object = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY); return object.isNull(JSONUtils.AFTER_KEY) ? null : object.getString(JSONUtils.AFTER_KEY); } catch (JSONException e) { e.printStackTrace(); return null; } } public static void parsePost(Executor executor, Handler handler, String response, ParsePostListener parsePostListener) { executor.execute(() -> { try { JSONArray allData = new JSONArray(response).getJSONObject(0).getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); if (allData.length() == 0) { handler.post(parsePostListener::onParsePostFail); return; } JSONObject data = allData.getJSONObject(0).getJSONObject(JSONUtils.DATA_KEY); Post post = parseBasicData(data); handler.post(() -> parsePostListener.onParsePostSuccess(post)); } catch (JSONException e) { e.printStackTrace(); handler.post(parsePostListener::onParsePostFail); } }); } @WorkerThread @Nullable public static Post parsePostSync(String response) { try { JSONArray allData = new JSONArray(response).getJSONObject(0).getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); if (allData.length() == 0) { return null; } JSONObject data = allData.getJSONObject(0).getJSONObject(JSONUtils.DATA_KEY); return parseBasicData(data); } catch (JSONException e) { e.printStackTrace(); return null; } } public static void parseRandomPost(Executor executor, Handler handler, String response, boolean isNSFW, ParseRandomPostListener parseRandomPostListener) { executor.execute(() -> { try { JSONArray postsArray = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); if (postsArray.length() == 0) { handler.post(parseRandomPostListener::onParseRandomPostFailed); } else { JSONObject post = postsArray.getJSONObject(0).getJSONObject(JSONUtils.DATA_KEY); String subredditName = post.getString(JSONUtils.SUBREDDIT_KEY); String postId; if (isNSFW) { postId = post.getString(JSONUtils.ID_KEY); } else { postId = post.getString(JSONUtils.LINK_ID_KEY).substring("t3_".length()); } handler.post(() -> parseRandomPostListener.onParseRandomPostSuccess(postId, subredditName)); } } catch (JSONException e) { e.printStackTrace(); handler.post(parseRandomPostListener::onParseRandomPostFailed); } }); } @WorkerThread public static Post parseBasicData(JSONObject data) throws JSONException { String id = data.getString(JSONUtils.ID_KEY); String fullName = data.getString(JSONUtils.NAME_KEY); String subredditName = data.getString(JSONUtils.SUBREDDIT_KEY); String subredditNamePrefixed = data.getString(JSONUtils.SUBREDDIT_NAME_PREFIX_KEY); String author = data.getString(JSONUtils.AUTHOR_KEY); StringBuilder authorFlairHTMLBuilder = new StringBuilder(); if (data.has(JSONUtils.AUTHOR_FLAIR_RICHTEXT_KEY)) { JSONArray flairArray = data.getJSONArray(JSONUtils.AUTHOR_FLAIR_RICHTEXT_KEY); for (int i = 0; i < flairArray.length(); i++) { JSONObject flairObject = flairArray.getJSONObject(i); String e = flairObject.getString(JSONUtils.E_KEY); if (e.equals("text")) { authorFlairHTMLBuilder.append(flairObject.getString(JSONUtils.T_KEY)); } else if (e.equals("emoji")) { authorFlairHTMLBuilder.append(""); } } } String authorFlair = data.isNull(JSONUtils.AUTHOR_FLAIR_TEXT_KEY) ? "" : data.getString(JSONUtils.AUTHOR_FLAIR_TEXT_KEY); String distinguished = data.getString(JSONUtils.DISTINGUISHED_KEY); String suggestedSort = data.has(JSONUtils.SUGGESTED_SORT_KEY) ? data.getString(JSONUtils.SUGGESTED_SORT_KEY) : null; long postTime = data.getLong(JSONUtils.CREATED_UTC_KEY) * 1000; String title = data.getString(JSONUtils.TITLE_KEY); int score = data.getInt(JSONUtils.SCORE_KEY); int voteType; int nComments = data.getInt(JSONUtils.NUM_COMMENTS_KEY); int upvoteRatio = (int) (data.getDouble(JSONUtils.UPVOTE_RATIO_KEY) * 100); boolean hidden = data.getBoolean(JSONUtils.HIDDEN_KEY); boolean spoiler = data.getBoolean(JSONUtils.SPOILER_KEY); boolean nsfw = data.getBoolean(JSONUtils.NSFW_KEY); boolean stickied = data.getBoolean(JSONUtils.STICKIED_KEY); boolean archived = data.getBoolean(JSONUtils.ARCHIVED_KEY); boolean locked = data.getBoolean(JSONUtils.LOCKED_KEY); boolean saved = data.getBoolean(JSONUtils.SAVED_KEY); boolean sendReplies = data.getBoolean(JSONUtils.SEND_REPLIES_KEY); boolean deleted = !data.isNull(JSONUtils.REMOVED_BY_CATEGORY_KEY) && data.getString(JSONUtils.REMOVED_BY_CATEGORY_KEY).equals("deleted"); boolean removed = !data.isNull(JSONUtils.REMOVED_BY_CATEGORY_KEY) && data.getString(JSONUtils.REMOVED_BY_CATEGORY_KEY).equals("moderator"); boolean canModPost = data.getBoolean(JSONUtils.CAN_MOD_POST_KEY); boolean approved = data.has(JSONUtils.APPROVED_KEY) && data.getBoolean(JSONUtils.APPROVED_KEY); long approvedAtUTC = data.has(JSONUtils.APPROVED_AT_UTC_KEY) ? (data.isNull(JSONUtils.APPROVED_AT_UTC_KEY) ? 0 : data.getLong(JSONUtils.APPROVED_AT_UTC_KEY) * 1000) : 0; String approvedBy = data.has(JSONUtils.APPROVED_BY_KEY) ? data.getString(JSONUtils.APPROVED_BY_KEY) : null; boolean spam = data.has(JSONUtils.SPAM_KEY) && data.getBoolean(JSONUtils.SPAM_KEY); StringBuilder postFlairHTMLBuilder = new StringBuilder(); String flair = ""; if (data.has(JSONUtils.LINK_FLAIR_RICHTEXT_KEY)) { JSONArray flairArray = data.getJSONArray(JSONUtils.LINK_FLAIR_RICHTEXT_KEY); for (int i = 0; i < flairArray.length(); i++) { JSONObject flairObject = flairArray.getJSONObject(i); String e = flairObject.getString(JSONUtils.E_KEY); if (e.equals("text")) { postFlairHTMLBuilder.append(Html.escapeHtml(flairObject.getString(JSONUtils.T_KEY))); } else if (e.equals("emoji")) { postFlairHTMLBuilder.append(""); } } flair = postFlairHTMLBuilder.toString(); } if (flair.equals("") && data.has(JSONUtils.LINK_FLAIR_TEXT_KEY) && !data.isNull(JSONUtils.LINK_FLAIR_TEXT_KEY)) { flair = data.getString(JSONUtils.LINK_FLAIR_TEXT_KEY); } if (data.isNull(JSONUtils.LIKES_KEY)) { voteType = 0; } else { voteType = data.getBoolean(JSONUtils.LIKES_KEY) ? 1 : -1; score -= voteType; } String permalink = Html.fromHtml(data.getString(JSONUtils.PERMALINK_KEY)).toString(); String thumbnailUrl = data.isNull(JSONUtils.THUMBNAIL_KEY) ? "" : data.getString(JSONUtils.THUMBNAIL_KEY); ArrayList previews = new ArrayList<>(); if (data.has(JSONUtils.PREVIEW_KEY)) { JSONObject images = data.getJSONObject(JSONUtils.PREVIEW_KEY).getJSONArray(JSONUtils.IMAGES_KEY).getJSONObject(0); String previewUrl = images.getJSONObject(JSONUtils.SOURCE_KEY).getString(JSONUtils.URL_KEY); int previewWidth = images.getJSONObject(JSONUtils.SOURCE_KEY).getInt(JSONUtils.WIDTH_KEY); int previewHeight = images.getJSONObject(JSONUtils.SOURCE_KEY).getInt(JSONUtils.HEIGHT_KEY); previews.add(new Post.Preview(previewUrl, previewWidth, previewHeight, "", "")); JSONArray thumbnailPreviews = images.getJSONArray(JSONUtils.RESOLUTIONS_KEY); for (int i = 0; i < thumbnailPreviews.length(); i++) { JSONObject thumbnailPreview = thumbnailPreviews.getJSONObject(i); String thumbnailPreviewUrl = thumbnailPreview.getString(JSONUtils.URL_KEY); int thumbnailPreviewWidth = thumbnailPreview.getInt(JSONUtils.WIDTH_KEY); int thumbnailPreviewHeight = thumbnailPreview.getInt(JSONUtils.HEIGHT_KEY); previews.add(new Post.Preview(thumbnailPreviewUrl, thumbnailPreviewWidth, thumbnailPreviewHeight, "", "")); } } Map mediaMetadataMap = JSONUtils.parseMediaMetadata(data); if (data.has(JSONUtils.CROSSPOST_PARENT_LIST) && data.getJSONArray(JSONUtils.CROSSPOST_PARENT_LIST).length() > 0) { //Cross post JSONObject parentData = data.getJSONArray(JSONUtils.CROSSPOST_PARENT_LIST).getJSONObject(0); // Extract previews from parent post if available ArrayList parentPreviews = new ArrayList<>(); if (parentData.has(JSONUtils.PREVIEW_KEY)) { JSONObject images = parentData.getJSONObject(JSONUtils.PREVIEW_KEY).getJSONArray(JSONUtils.IMAGES_KEY).getJSONObject(0); String previewUrl = images.getJSONObject(JSONUtils.SOURCE_KEY).getString(JSONUtils.URL_KEY); int previewWidth = images.getJSONObject(JSONUtils.SOURCE_KEY).getInt(JSONUtils.WIDTH_KEY); int previewHeight = images.getJSONObject(JSONUtils.SOURCE_KEY).getInt(JSONUtils.HEIGHT_KEY); parentPreviews.add(new Post.Preview(previewUrl, previewWidth, previewHeight, "", "")); JSONArray thumbnailPreviews = images.getJSONArray(JSONUtils.RESOLUTIONS_KEY); for (int i = 0; i < thumbnailPreviews.length(); i++) { JSONObject thumbnailPreview = thumbnailPreviews.getJSONObject(i); String thumbnailPreviewUrl = thumbnailPreview.getString(JSONUtils.URL_KEY); int thumbnailPreviewWidth = thumbnailPreview.getInt(JSONUtils.WIDTH_KEY); int thumbnailPreviewHeight = thumbnailPreview.getInt(JSONUtils.HEIGHT_KEY); parentPreviews.add(new Post.Preview(thumbnailPreviewUrl, thumbnailPreviewWidth, thumbnailPreviewHeight, "", "")); } } // Use parent previews if current post doesn't have any if (previews.isEmpty() && !parentPreviews.isEmpty()) { previews = parentPreviews; } // Use parent thumbnail if current post doesn't have a valid thumbnail String parentThumbnailUrl = parentData.isNull(JSONUtils.THUMBNAIL_KEY) ? "" : parentData.getString(JSONUtils.THUMBNAIL_KEY); if ((thumbnailUrl == null || thumbnailUrl.isEmpty() || thumbnailUrl.equals("self") || thumbnailUrl.equals("default")) && parentThumbnailUrl != null && !parentThumbnailUrl.isEmpty() && !parentThumbnailUrl.equals("self") && !parentThumbnailUrl.equals("default")) { thumbnailUrl = parentThumbnailUrl; } Post crosspostParent = parseBasicData(parentData); Post post = parseData(parentData, permalink, id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTMLBuilder.toString(), postTime, title, previews, mediaMetadataMap, score, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, deleted, removed, true, canModPost, approved, approvedAtUTC, approvedBy, spam, distinguished, suggestedSort, thumbnailUrl); post.setCrosspostParentId(crosspostParent.getId()); return post; } else { return parseData(data, permalink, id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTMLBuilder.toString(), postTime, title, previews, mediaMetadataMap, score, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, deleted, removed, false, canModPost, approved, approvedAtUTC, approvedBy, spam, distinguished, suggestedSort, thumbnailUrl); } } private static Post parseData(JSONObject data, String permalink, String id, String fullName, String subredditName, String subredditNamePrefixed, String author, String authorFlair, String authorFlairHTML, long postTimeMillis, String title, ArrayList previews, Map mediaMetadataMap, int score, int voteType, int nComments, int upvoteRatio, String flair, boolean hidden, boolean spoiler, boolean nsfw, boolean stickied, boolean archived, boolean locked, boolean saved, boolean sendReplies, boolean deleted, boolean removed, boolean isCrosspost, boolean canModPost, boolean approved, long approvedAtUTC, String approvedBy, boolean spam, String distinguished, String suggestedSort, String thumbnailUrl) throws JSONException { Post post; boolean isVideo = data.getBoolean(JSONUtils.IS_VIDEO_KEY); String url = Html.fromHtml(data.getString(JSONUtils.URL_KEY)).toString(); Uri uri = Uri.parse(url); String path = uri.getPath(); if (!data.has(JSONUtils.PREVIEW_KEY) && previews.isEmpty()) { if (url.contains(permalink)) { //Text post int postType = Post.TEXT_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); } else { if (path.endsWith(".jpg") || path.endsWith(".png") || path.endsWith(".jpeg")) { //Image post int postType = Post.IMAGE_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); if (previews.isEmpty()) { if ("i.redgifs.com".equals(uri.getAuthority())) { //No preview link (Not able to load redgifs image) post.setPostType(Post.NO_PREVIEW_LINK_TYPE); } else { previews.add(new Post.Preview(url, 0, 0, "", "")); } } else if ("i.redgifs.com".equals(uri.getAuthority())) { post.setUrl(previews.get(previews.size() - 1).getPreviewUrl()); } post.setPreviews(previews); } else { if (isVideo) { //No preview video post /* TODO a removed crosspost may not have media JSONObject. This happens in crosspost_parent_list e.g. https://www.reddit.com/r/hitmanimals/comments/1l6pv0m/mission_failed_agent_47/ */ JSONObject redditVideoObject = data.getJSONObject(JSONUtils.MEDIA_KEY).getJSONObject(JSONUtils.REDDIT_VIDEO_KEY); int postType = Post.VIDEO_TYPE; String videoUrl = Html.fromHtml(redditVideoObject.getString(JSONUtils.HLS_URL_KEY)).toString(); String videoDownloadUrl = redditVideoObject.getString(JSONUtils.FALLBACK_URL_KEY); post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setVideoUrl(videoUrl); post.setVideoDownloadUrl(videoDownloadUrl); } else { //No preview link post int postType = Post.NO_PREVIEW_LINK_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); if (data.isNull(JSONUtils.SELFTEXT_KEY)) { post.setSelfText(""); } else { post.setSelfText(Utils.parseRedditImagesBlock(Utils.modifyMarkdown(Utils.trimTrailingWhitespace(data.getString(JSONUtils.SELFTEXT_KEY))), mediaMetadataMap)); } String authority = uri.getAuthority(); if (authority != null) { if (authority.contains("redgifs.com")) { String redgifsId = getRedgifsId(data); if (redgifsId != null) { post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(getRedgifsVideoUrl(redgifsId)); post.setVideoDownloadUrl(post.getVideoUrl()); post.setRedgifsId(redgifsId); } /*try { String redgifsId = data.getJSONObject(JSONUtils.MEDIA_KEY).getJSONObject(JSONUtils.O_EMBED_KEY).getString(JSONUtils.THUMBNAIL_URL_KEY); redgifsId = redgifsId.substring(redgifsId.lastIndexOf("/") + 1); int dashIndex = redgifsId.lastIndexOf("-"); if (dashIndex >= 0) { redgifsId = redgifsId.substring(0, dashIndex); post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(url); post.setRedgifsId(redgifsId); } } catch (JSONException e) { }*/ } else if (authority.equals("streamable.com")) { String shortCode = url.substring(url.lastIndexOf("/") + 1); post.setPostType(Post.VIDEO_TYPE); post.setIsStreamable(true); post.setVideoUrl(url); post.setStreamableShortCode(shortCode); } else if (authority.contains("tumblr.com") && path.endsWith(".mp4")) { post.setPostType(Post.VIDEO_TYPE); post.setIsTumblr(true); post.setVideoUrl(url); } } } } } } else { if (previews.isEmpty()) { if (data.has(JSONUtils.PREVIEW_KEY)) { JSONObject images = data.getJSONObject(JSONUtils.PREVIEW_KEY).getJSONArray(JSONUtils.IMAGES_KEY).getJSONObject(0); String previewUrl = images.getJSONObject(JSONUtils.SOURCE_KEY).getString(JSONUtils.URL_KEY); int previewWidth = images.getJSONObject(JSONUtils.SOURCE_KEY).getInt(JSONUtils.WIDTH_KEY); int previewHeight = images.getJSONObject(JSONUtils.SOURCE_KEY).getInt(JSONUtils.HEIGHT_KEY); previews.add(new Post.Preview(previewUrl, previewWidth, previewHeight, "", "")); JSONArray thumbnailPreviews = images.getJSONArray(JSONUtils.RESOLUTIONS_KEY); for (int i = 0; i < thumbnailPreviews.length(); i++) { JSONObject thumbnailPreview = images.getJSONArray(JSONUtils.RESOLUTIONS_KEY).getJSONObject(i); String thumbnailPreviewUrl = thumbnailPreview.getString(JSONUtils.URL_KEY); int thumbnailPreviewWidth = thumbnailPreview.getInt(JSONUtils.WIDTH_KEY); int thumbnailPreviewHeight = thumbnailPreview.getInt(JSONUtils.HEIGHT_KEY); previews.add(new Post.Preview(thumbnailPreviewUrl, thumbnailPreviewWidth, thumbnailPreviewHeight, "", "")); } } } if (isVideo) { //Video post JSONObject redditVideoObject = data.getJSONObject(JSONUtils.MEDIA_KEY).getJSONObject(JSONUtils.REDDIT_VIDEO_KEY); int postType = Post.VIDEO_TYPE; String videoUrl = Html.fromHtml(redditVideoObject.getString(JSONUtils.HLS_URL_KEY)).toString(); String videoDownloadUrl = redditVideoObject.getString(JSONUtils.FALLBACK_URL_KEY); post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setPreviews(previews); post.setVideoUrl(videoUrl); post.setVideoDownloadUrl(videoDownloadUrl); } else if (data.has(JSONUtils.PREVIEW_KEY)) { if (data.getJSONObject(JSONUtils.PREVIEW_KEY).has(JSONUtils.REDDIT_VIDEO_PREVIEW_KEY)) { int postType = Post.VIDEO_TYPE; String authority = uri.getAuthority(); // The hls stream inside REDDIT_VIDEO_PREVIEW_KEY can sometimes lack an audio track if (authority.contains("imgur.com") && (path.endsWith(".gifv") || path.endsWith(".mp4"))) { if (path.endsWith(".gifv")) { url = url.substring(0, url.length() - 5) + ".mp4"; } post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setPreviews(previews); post.setVideoUrl(url); post.setVideoDownloadUrl(url); post.setIsImgur(true); } else { //Gif video post (HLS) and maybe Redgifs String videoUrl = Html.fromHtml(data.getJSONObject(JSONUtils.PREVIEW_KEY) .getJSONObject(JSONUtils.REDDIT_VIDEO_PREVIEW_KEY).getString(JSONUtils.HLS_URL_KEY)).toString(); String videoDownloadUrl = data.getJSONObject(JSONUtils.PREVIEW_KEY) .getJSONObject(JSONUtils.REDDIT_VIDEO_PREVIEW_KEY).getString(JSONUtils.FALLBACK_URL_KEY); post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setPreviews(previews); post.setVideoUrl(videoUrl); post.setVideoDownloadUrl(videoDownloadUrl); } post.setVideoFallBackDirectUrl(Html.fromHtml(data.getJSONObject(JSONUtils.PREVIEW_KEY) .getJSONObject(JSONUtils.REDDIT_VIDEO_PREVIEW_KEY).getString(JSONUtils.FALLBACK_URL_KEY)).toString()); } else { if (path.endsWith(".jpg") || path.endsWith(".png") || path.endsWith(".jpeg")) { //Image post int postType = Post.IMAGE_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); if (previews.isEmpty()) { if ("i.redgifs.com".equals(uri.getAuthority())) { //No preview link (Not able to load redgifs image) post.setPostType(Post.NO_PREVIEW_LINK_TYPE); } else { previews.add(new Post.Preview(url, 0, 0, "", "")); } } else if ("i.redgifs.com".equals(uri.getAuthority())) { post.setUrl(previews.get(previews.size() - 1).getPreviewUrl()); } post.setPreviews(previews); } else if (path.endsWith(".gif")) { //Gif post int postType = Post.GIF_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setPreviews(previews); post.setVideoUrl(url); try { String mp4Variant = data.getJSONObject(JSONUtils.PREVIEW_KEY) .getJSONArray(JSONUtils.IMAGES_KEY).getJSONObject(0) .getJSONObject(JSONUtils.VARIANTS_KEY).getJSONObject(JSONUtils.MP4_KEY) .getJSONObject(JSONUtils.SOURCE_KEY).getString(JSONUtils.URL_KEY); if (!mp4Variant.isEmpty()) { post.setMp4Variant(mp4Variant); } } catch (Exception ignore) {} } else if (uri.getAuthority().contains("imgur.com") && (path.endsWith(".gifv") || path.endsWith(".mp4"))) { // Imgur gifv/mp4 int postType = Post.VIDEO_TYPE; if (url.endsWith("gifv")) { url = url.substring(0, url.length() - 5) + ".mp4"; } post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setPreviews(previews); post.setVideoUrl(url); post.setVideoDownloadUrl(url); post.setIsImgur(true); } else if (path.endsWith(".mp4")) { //Video post int postType = Post.VIDEO_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setPreviews(previews); post.setVideoUrl(url); post.setVideoDownloadUrl(url); } else { if (url.contains(permalink)) { //Text post but with a preview int postType = Post.TEXT_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); //Need attention post.setPreviews(previews); } else { //Link post int postType = Post.LINK_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); if (data.isNull(JSONUtils.SELFTEXT_KEY)) { post.setSelfText(""); } else { post.setSelfText(Utils.parseRedditImagesBlock(Utils.modifyMarkdown(Utils.trimTrailingWhitespace(data.getString(JSONUtils.SELFTEXT_KEY))), mediaMetadataMap)); } post.setPreviews(previews); String authority = uri.getAuthority(); if (authority != null) { if (authority.contains("redgifs.com")) { String redgifsId = getRedgifsId(data); if (redgifsId != null) { post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(getRedgifsVideoUrl(redgifsId)); post.setVideoDownloadUrl(post.getVideoUrl()); post.setRedgifsId(redgifsId); } /*String redgifsId = url.substring(url.lastIndexOf("/") + 1).toLowerCase(); post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(url); post.setRedgifsId(redgifsId);*/ } else if (authority.equals("streamable.com")) { String shortCode = url.substring(url.lastIndexOf("/") + 1); post.setPostType(Post.VIDEO_TYPE); post.setIsStreamable(true); post.setVideoUrl(url); post.setStreamableShortCode(shortCode); } } } } } } else { if (path.endsWith(".jpg") || path.endsWith(".png") || path.endsWith(".jpeg")) { //Image post int postType = Post.IMAGE_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); if (previews.isEmpty()) { if ("i.redgifs.com".equals(uri.getAuthority())) { //No preview link (Not able to load redgifs image) post.setPostType(Post.NO_PREVIEW_LINK_TYPE); } else { previews.add(new Post.Preview(url, 0, 0, "", "")); } } else if ("i.redgifs.com".equals(uri.getAuthority())) { post.setUrl(previews.get(previews.size() - 1).getPreviewUrl()); } post.setPreviews(previews); } else if (path.endsWith(".mp4")) { //Video post int postType = Post.VIDEO_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); post.setPreviews(previews); post.setVideoUrl(url); post.setVideoDownloadUrl(url); } else { //CP No Preview Link post int postType = Post.NO_PREVIEW_LINK_TYPE; post = new Post(id, fullName, subredditName, subredditNamePrefixed, author, authorFlair, authorFlairHTML, postTimeMillis, title, url, permalink, score, postType, voteType, nComments, upvoteRatio, flair, hidden, spoiler, nsfw, stickied, archived, locked, saved, sendReplies, isCrosspost, canModPost, approved, approvedAtUTC, approvedBy, removed, spam, distinguished, suggestedSort); //Need attention if (data.isNull(JSONUtils.SELFTEXT_KEY)) { post.setSelfText(""); } else { post.setSelfText(Utils.parseRedditImagesBlock(Utils.modifyMarkdown(Utils.trimTrailingWhitespace(data.getString(JSONUtils.SELFTEXT_KEY))), mediaMetadataMap)); } String authority = uri.getAuthority(); if (authority != null) { if (authority.contains("redgifs.com")) { String redgifsId = getRedgifsId(data); if (redgifsId != null) { post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(getRedgifsVideoUrl(redgifsId)); post.setVideoDownloadUrl(post.getVideoUrl()); post.setRedgifsId(redgifsId); } /*String redgifsId = url.substring(url.lastIndexOf("/") + 1).toLowerCase(); post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(url); post.setRedgifsId(redgifsId);*/ } else if (authority.equals("streamable.com")) { String shortCode = url.substring(url.lastIndexOf("/") + 1); post.setPostType(Post.VIDEO_TYPE); post.setIsStreamable(true); post.setVideoUrl(url); post.setStreamableShortCode(shortCode); } } } } } if (post.getPostType() == Post.VIDEO_TYPE) { try { String authority = uri.getAuthority(); if (authority != null) { if (authority.contains("redgifs.com")) { String redgifsId = getRedgifsId(data); if (redgifsId != null) { post.setIsRedgifs(true); post.setVideoUrl(getRedgifsVideoUrl(redgifsId)); post.setVideoDownloadUrl(post.getVideoUrl()); post.setRedgifsId(redgifsId); } /*String redgifsId = url.substring(url.lastIndexOf("/") + 1); if (redgifsId.contains("-")) { redgifsId = redgifsId.substring(0, redgifsId.indexOf('-')); } post.setIsRedgifs(true); post.setVideoUrl(url); post.setRedgifsId(redgifsId.toLowerCase());*/ } else if (authority.equals("streamable.com")) { String shortCode = url.substring(url.lastIndexOf("/") + 1); post.setPostType(Post.VIDEO_TYPE); post.setIsStreamable(true); post.setVideoUrl(url); post.setStreamableShortCode(shortCode); } } } catch (IllegalArgumentException ignore) { } } else if (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE) { if (!data.isNull(JSONUtils.GALLERY_DATA_KEY)) { try { JSONArray galleryIdsArray = data.getJSONObject(JSONUtils.GALLERY_DATA_KEY).getJSONArray(JSONUtils.ITEMS_KEY); JSONObject galleryObject = data.getJSONObject(JSONUtils.MEDIA_METADATA_KEY); ArrayList gallery = new ArrayList<>(); for (int i = 0; i < galleryIdsArray.length(); i++) { String galleryId = galleryIdsArray.getJSONObject(i).getString(JSONUtils.MEDIA_ID_KEY); JSONObject singleGalleryObject = galleryObject.getJSONObject(galleryId); String mimeType = singleGalleryObject.getString(JSONUtils.M_KEY); String galleryItemUrl; if (mimeType.contains("jpg") || mimeType.contains("png")) { galleryItemUrl = singleGalleryObject.getJSONObject(JSONUtils.S_KEY).getString(JSONUtils.U_KEY); } else { JSONObject sourceObject = singleGalleryObject.getJSONObject(JSONUtils.S_KEY); if (mimeType.contains("gif")) { galleryItemUrl = sourceObject.getString(JSONUtils.GIF_KEY); } else { galleryItemUrl = sourceObject.getString(JSONUtils.MP4_KEY); } } JSONObject galleryItem = galleryIdsArray.getJSONObject(i); String galleryItemCaption = ""; String galleryItemCaptionUrl = ""; if (galleryItem.has(JSONUtils.CAPTION_KEY)) { galleryItemCaption = galleryItem.getString(JSONUtils.CAPTION_KEY).trim(); } if (galleryItem.has(JSONUtils.CAPTION_URL_KEY)) { galleryItemCaptionUrl = galleryItem.getString(JSONUtils.CAPTION_URL_KEY).trim(); } if (previews.isEmpty() && (mimeType.contains("jpg") || mimeType.contains("png"))) { previews.add(new Post.Preview(galleryItemUrl, singleGalleryObject.getJSONObject(JSONUtils.S_KEY).getInt(JSONUtils.X_KEY), singleGalleryObject.getJSONObject(JSONUtils.S_KEY).getInt(JSONUtils.Y_KEY), galleryItemCaption, galleryItemCaptionUrl)); } Post.Gallery postGalleryItem = new Post.Gallery(mimeType, galleryItemUrl, "", subredditName + "-" + galleryId + "." + mimeType.substring(mimeType.lastIndexOf("/") + 1), galleryItemCaption, galleryItemCaptionUrl); // For issue #558 // Construct a fallback image url if (!TextUtils.isEmpty(galleryItemUrl) && !TextUtils.isEmpty(mimeType) && (mimeType.contains("jpg") || mimeType.contains("png"))) { postGalleryItem.setFallbackUrl("https://i.redd.it/" + galleryId + "." + mimeType.substring(mimeType.lastIndexOf("/") + 1)); postGalleryItem.setHasFallback(true); } gallery.add(postGalleryItem); } if (!gallery.isEmpty()) { post.setPostType(Post.GALLERY_TYPE); post.setGallery(gallery); post.setPreviews(previews); } } catch (JSONException e) { /* https://www.reddit.com/r/Leathercraft/comments/1qo3jrv/one_year_of_patina/.json?raw_json=1 "gallery_data": { "items": [ { "media_id": "2ik58hyditfg1", "id": 849724223 }, { "media_id": "1a9oi91fitfg1", "id": 849724224 } ] } */ e.printStackTrace(); } } else if (post.getPostType() == Post.LINK_TYPE) { String authority = uri.getAuthority(); if (authority != null) { if (authority.contains("redgifs.com")) { String redgifsId = getRedgifsId(data); if (redgifsId != null) { post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(getRedgifsVideoUrl(redgifsId)); post.setVideoDownloadUrl(post.getVideoUrl()); post.setRedgifsId(redgifsId); } /*String redgifsId = url.substring(url.lastIndexOf("/") + 1).toLowerCase(); post.setPostType(Post.VIDEO_TYPE); post.setIsRedgifs(true); post.setVideoUrl(url); post.setRedgifsId(redgifsId);*/ } else if (authority.equals("streamable.com")) { String shortCode = url.substring(url.lastIndexOf("/") + 1); post.setPostType(Post.VIDEO_TYPE); post.setIsStreamable(true); post.setVideoUrl(url); post.setStreamableShortCode(shortCode); } } } } if (post.getPostType() != Post.LINK_TYPE && post.getPostType() != Post.NO_PREVIEW_LINK_TYPE) { if (data.isNull(JSONUtils.SELFTEXT_KEY)) { post.setSelfText(""); } else { String selfText = Utils.parseRedditImagesBlock(Utils.modifyMarkdown(Utils.trimTrailingWhitespace(data.getString(JSONUtils.SELFTEXT_KEY))), mediaMetadataMap); post.setSelfText(selfText); if (data.isNull(JSONUtils.SELFTEXT_HTML_KEY)) { post.setSelfTextPlainTrimmed(""); } else { String selfTextPlain = Utils.trimTrailingWhitespace( Html.fromHtml(data.getString(JSONUtils.SELFTEXT_HTML_KEY))).toString(); post.setSelfTextPlain(selfTextPlain); if (selfTextPlain.length() > 250) { selfTextPlain = selfTextPlain.substring(0, 250); } if (!selfText.equals("")) { Pattern p = Pattern.compile(">!.+!<"); Matcher m = p.matcher(selfText.substring(0, Math.min(selfText.length(), 400))); if (m.find()) { post.setSelfTextPlainTrimmed(""); } else { post.setSelfTextPlainTrimmed(selfTextPlain); } } else { post.setSelfTextPlainTrimmed(selfTextPlain); } } } } post.setThumbnailUrl(thumbnailUrl); post.setMediaMetadataMap(mediaMetadataMap); return post; } @Nullable private static String getRedgifsId(JSONObject data) { try { String redgifsId = data.getJSONObject(JSONUtils.MEDIA_KEY).getJSONObject(JSONUtils.O_EMBED_KEY).getString(JSONUtils.THUMBNAIL_URL_KEY); redgifsId = redgifsId.substring(redgifsId.lastIndexOf("/") + 1); int dashIndex = redgifsId.lastIndexOf("-"); if (dashIndex >= 0) { return redgifsId.substring(0, dashIndex); } return null; } catch (JSONException e) { e.printStackTrace(); return null; } } private static String getRedgifsVideoUrl(String redgifsId) { return "https://media.redgifs.com/" + redgifsId + ".mp4"; } public interface ParsePostsListingListener { void onParsePostsListingSuccess(LinkedHashSet newPostData, String lastItem); void onParsePostsListingFail(); } public interface ParsePostListener { void onParsePostSuccess(Post post); void onParsePostFail(); } public interface ParseRandomPostListener { void onParseRandomPostSuccess(String postId, String subredditName); void onParseRandomPostFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/PollPayload.java ================================================ package ml.docilealligator.infinityforreddit.post; import com.google.gson.annotations.SerializedName; import ml.docilealligator.infinityforreddit.subreddit.Flair; public class PollPayload { @SerializedName("api_type") public String apiType = "json"; @SerializedName("duration") public int duration; @SerializedName("nsfw") public boolean isNsfw; public String[] options; @SerializedName("flair_id") public String flairId; @SerializedName("flair_text") public String flairText; @SerializedName("raw_rtjson") public String richTextJSON; @SerializedName("post_to_twitter") public boolean postToTwitter = false; @SerializedName("sendreplies") public boolean sendReplies; @SerializedName("show_error_list") public boolean showErrorList = true; @SerializedName("spoiler") public boolean isSpoiler; @SerializedName("sr") public String subredditName; @SerializedName("submit_type") public String submitType; public String text; public String title; @SerializedName("validate_on_submit") public boolean validateOnSubmit = true; public PollPayload(String subredditName, String title, String[] options, int duration, boolean isNsfw, boolean isSpoiler, Flair flair, boolean sendReplies, String submitType) { this.subredditName = subredditName; this.title = title; this.options = options; this.duration = duration; this.isNsfw = isNsfw; this.isSpoiler = isSpoiler; if (flair != null) { flairId = flair.getId(); flairText = flair.getText(); } this.sendReplies = sendReplies; this.submitType = submitType; } public PollPayload(String subredditName, String title, String[] options, int duration, boolean isNsfw, boolean isSpoiler, Flair flair, String richTextJSON, String text, boolean sendReplies, String submitType) { this.subredditName = subredditName; this.title = title; this.options = options; this.duration = duration; this.isNsfw = isNsfw; this.isSpoiler = isSpoiler; if (flair != null) { flairId = flair.getId(); flairText = flair.getText(); } this.richTextJSON = richTextJSON; this.text = text; this.sendReplies = sendReplies; this.submitType = submitType; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/Post.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Map; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.utils.APIUtils; /** * Created by alex on 3/1/18. */ public class Post implements Parcelable { public static final int NSFW_TYPE = -1; public static final int TEXT_TYPE = 0; public static final int IMAGE_TYPE = 1; public static final int LINK_TYPE = 2; public static final int VIDEO_TYPE = 3; public static final int GIF_TYPE = 4; public static final int NO_PREVIEW_LINK_TYPE = 5; public static final int GALLERY_TYPE = 6; private final String id; private final String fullName; private final String subredditName; private final String subredditNamePrefixed; private String subredditIconUrl; private String author; private String authorNamePrefixed; private String authorIconUrl; private final String authorFlair; private final String authorFlairHTML; private String title; private String selfText; private String selfTextPlain; private String selfTextPlainTrimmed; private String url; private String videoUrl; private String videoDownloadUrl; @Nullable private String videoFallBackDirectUrl; private String thumbnailUrl; private String redgifsId; private String streamableShortCode; private boolean isImgur; private boolean isRedgifs; private boolean isStreamable; private boolean isTumblr; private boolean loadedStreamableVideoAlready; private final String permalink; private String flair; private final long postTimeMillis; private int score; private int postType; private int voteType; private int nComments; private int upvoteRatio; private boolean hidden; private boolean spoiler; private boolean nsfw; private boolean stickied; private final boolean archived; private boolean locked; private boolean saved; private boolean sendReplies; private final boolean isCrosspost; private boolean isRead; private String crosspostParentId; private String distinguished; private final String suggestedSort; private String mp4Variant; private ArrayList previews = new ArrayList<>(); @Nullable private Map mediaMetadataMap; private ArrayList gallery = new ArrayList<>(); private boolean canModPost; private boolean approved; private long approvedAtUTC; private String approvedBy; private boolean removed; private boolean spam; //Text and video posts public Post(String id, String fullName, String subredditName, String subredditNamePrefixed, String author, String authorFlair, String authorFlairHTML, long postTimeMillis, String title, String permalink, int score, int postType, int voteType, int nComments, int upvoteRatio, String flair, boolean hidden, boolean spoiler, boolean nsfw, boolean stickied, boolean archived, boolean locked, boolean saved, boolean sendReplies, boolean isCrosspost, boolean canModPost, boolean approved, long approvedAtUTC, String approvedBy, boolean removed, boolean spam, String distinguished, String suggestedSort) { this.id = id; this.fullName = fullName; this.subredditName = subredditName; this.subredditNamePrefixed = subredditNamePrefixed; this.author = author; this.authorNamePrefixed = "u/" + author; this.authorFlair = authorFlair; this.authorFlairHTML = authorFlairHTML; this.postTimeMillis = postTimeMillis; this.title = title; this.permalink = APIUtils.API_BASE_URI + permalink; this.score = score; this.postType = postType; this.voteType = voteType; this.nComments = nComments; this.upvoteRatio = upvoteRatio; this.flair = flair; this.hidden = hidden; this.spoiler = spoiler; this.nsfw = nsfw; this.stickied = stickied; this.archived = archived; this.locked = locked; this.saved = saved; this.sendReplies = sendReplies; this.isCrosspost = isCrosspost; this.canModPost = canModPost; this.approved = approved; this.approvedAtUTC = approvedAtUTC; this.approvedBy = approvedBy; this.removed = removed; this.spam = spam; this.distinguished = distinguished; this.suggestedSort = suggestedSort; isRead = false; } public Post(String id, String fullName, String subredditName, String subredditNamePrefixed, String author, String authorFlair, String authorFlairHTML, long postTimeMillis, String title, String url, String permalink, int score, int postType, int voteType, int nComments, int upvoteRatio, String flair, boolean hidden, boolean spoiler, boolean nsfw, boolean stickied, boolean archived, boolean locked, boolean saved, boolean sendReplies, boolean isCrosspost, boolean canModPost, boolean approved, long approvedAtUTC, String approvedBy, boolean removed, boolean spam, String distinguished, String suggestedSort) { this.id = id; this.fullName = fullName; this.subredditName = subredditName; this.subredditNamePrefixed = subredditNamePrefixed; this.author = author; this.authorNamePrefixed = "u/" + author; this.authorFlair = authorFlair; this.authorFlairHTML = authorFlairHTML; this.postTimeMillis = postTimeMillis; this.title = title; this.url = url; this.permalink = APIUtils.API_BASE_URI + permalink; this.score = score; this.postType = postType; this.voteType = voteType; this.nComments = nComments; this.upvoteRatio = upvoteRatio; this.flair = flair; this.hidden = hidden; this.spoiler = spoiler; this.nsfw = nsfw; this.stickied = stickied; this.archived = archived; this.locked = locked; this.saved = saved; this.sendReplies = sendReplies; this.isCrosspost = isCrosspost; this.canModPost = canModPost; this.approved = approved; this.approvedAtUTC = approvedAtUTC; this.approvedBy = approvedBy; this.removed = removed; this.spam = spam; this.distinguished = distinguished; this.suggestedSort = suggestedSort; isRead = false; } protected Post(Parcel in) { id = in.readString(); fullName = in.readString(); subredditName = in.readString(); subredditNamePrefixed = in.readString(); subredditIconUrl = in.readString(); author = in.readString(); authorNamePrefixed = in.readString(); authorIconUrl = in.readString(); authorFlair = in.readString(); authorFlairHTML = in.readString(); title = in.readString(); selfText = in.readString(); selfTextPlain = in.readString(); selfTextPlainTrimmed = in.readString(); url = in.readString(); videoUrl = in.readString(); videoDownloadUrl = in.readString(); videoFallBackDirectUrl = in.readString(); thumbnailUrl = in.readString(); redgifsId = in.readString(); streamableShortCode = in.readString(); isImgur = in.readByte() != 0; isRedgifs = in.readByte() != 0; isStreamable = in.readByte() != 0; isTumblr = in.readByte() != 0; loadedStreamableVideoAlready = in.readByte() != 0; permalink = in.readString(); flair = in.readString(); postTimeMillis = in.readLong(); score = in.readInt(); postType = in.readInt(); voteType = in.readInt(); nComments = in.readInt(); upvoteRatio = in.readInt(); hidden = in.readByte() != 0; spoiler = in.readByte() != 0; nsfw = in.readByte() != 0; stickied = in.readByte() != 0; archived = in.readByte() != 0; locked = in.readByte() != 0; saved = in.readByte() != 0; sendReplies = in.readByte() != 0; isCrosspost = in.readByte() != 0; canModPost = in.readByte() != 0; approved = in.readByte() != 0; approvedAtUTC = in.readLong(); approvedBy = in.readString(); removed = in.readByte() != 0; spam = in.readByte() != 0; isRead = in.readByte() != 0; crosspostParentId = in.readString(); distinguished = in.readString(); suggestedSort = in.readString(); mp4Variant = in.readString(); previews = in.createTypedArrayList(Preview.CREATOR); mediaMetadataMap = (Map) in.readValue(getClass().getClassLoader()); gallery = in.createTypedArrayList(Gallery.CREATOR); } public static final Creator CREATOR = new Creator() { @Override public Post createFromParcel(Parcel in) { return new Post(in); } @Override public Post[] newArray(int size) { return new Post[size]; } }; public String getId() { return id; } public String getFullName() { return fullName; } public String getSubredditName() { return subredditName; } public String getSubredditNamePrefixed() { return subredditNamePrefixed; } public String getSubredditIconUrl() { return subredditIconUrl; } public void setSubredditIconUrl(String subredditIconUrl) { this.subredditIconUrl = subredditIconUrl; } public String getAuthor() { return author; } public boolean isAuthorDeleted() { return author != null && author.equals("[deleted]"); } public void setAuthor(String author) { this.author = author; this.authorNamePrefixed = "u/" + author; } public String getAuthorNamePrefixed() { return authorNamePrefixed; } public String getAuthorFlair() { return authorFlair; } public String getAuthorFlairHTML() { return authorFlairHTML; } public String getAuthorIconUrl() { return authorIconUrl; } public void setAuthorIconUrl(String authorIconUrl) { this.authorIconUrl = authorIconUrl; } public long getPostTimeMillis() { return postTimeMillis; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getSelfText() { return selfText; } public void setSelfText(String selfText) { this.selfText = selfText; } public String getSelfTextPlain() { return selfTextPlain; } public void setSelfTextPlain(String selfTextPlain) { this.selfTextPlain = selfTextPlain; } public String getSelfTextPlainTrimmed() { return selfTextPlainTrimmed; } public void setSelfTextPlainTrimmed(String selfTextPlainTrimmed) { this.selfTextPlainTrimmed = selfTextPlainTrimmed; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getVideoUrl() { return videoUrl; } public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } public String getVideoDownloadUrl() { return videoDownloadUrl; } public void setVideoDownloadUrl(String videoDownloadUrl) { this.videoDownloadUrl = videoDownloadUrl; } @Nullable public String getVideoFallBackDirectUrl() { return videoFallBackDirectUrl; } public void setVideoFallBackDirectUrl(@Nullable String videoFallBackDirectUrl) { this.videoFallBackDirectUrl = videoFallBackDirectUrl; } public String getThumbnailUrl() { return thumbnailUrl; } public void setThumbnailUrl(String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; } public String getRedgifsId() { return redgifsId; } public void setRedgifsId(String redgifsId) { this.redgifsId = redgifsId; } public String getStreamableShortCode() { return streamableShortCode; } public void setStreamableShortCode(String shortCode) { this.streamableShortCode = shortCode; } public void setIsImgur(boolean isImgur) { this.isImgur = isImgur; } public boolean isImgur() { return isImgur; } public boolean isRedgifs() { return isRedgifs; } public void setIsRedgifs(boolean isRedgifs) { this.isRedgifs = isRedgifs; } public boolean isStreamable() { return isStreamable; } public void setIsStreamable(boolean isStreamable) { this.isStreamable = isStreamable; } public boolean isTumblr() { return isTumblr; } public void setIsTumblr(boolean isTumblr) { this.isTumblr = isTumblr; } public boolean isNormalVideo() { return postType == Post.VIDEO_TYPE && !isImgur && !isRedgifs && !isStreamable; } public boolean isLoadedStreamableVideoAlready() { return loadedStreamableVideoAlready; } public void setLoadedStreamableVideoAlready(boolean loadedStreamableVideoAlready) { this.loadedStreamableVideoAlready = loadedStreamableVideoAlready; } public String getPermalink() { return permalink; } public String getFlair() { return flair; } public void setFlair(String flair) { this.flair = flair; } public boolean isModerator() { return distinguished != null && distinguished.equals("moderator"); } public void setIsModerator(boolean value) { distinguished = value ? "moderator" : null; } public boolean isAdmin() { return distinguished != null && distinguished.equals("admin"); } public String getSuggestedSort() { return suggestedSort; } public int getScore() { return score; } public void setScore(int score) { this.score = score; } public int getPostType() { return postType; } public void setPostType(int postType) { this.postType = postType; } public int getVoteType() { return voteType; } public void setVoteType(int voteType) { this.voteType = voteType; } public int getNComments() { return nComments; } public void setNComments(int nComments) { this.nComments = nComments; } public int getUpvoteRatio() { return upvoteRatio; } public void setUpvoteRatio(int upvoteRatio) { this.upvoteRatio = upvoteRatio; } public boolean isHidden() { return hidden; } public void setHidden(boolean hidden) { this.hidden = hidden; } public boolean isSpoiler() { return spoiler; } public void setSpoiler(boolean spoiler) { this.spoiler = spoiler; } public boolean isNSFW() { return nsfw; } public void setNSFW(boolean nsfw) { this.nsfw = nsfw; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(id); dest.writeString(fullName); dest.writeString(subredditName); dest.writeString(subredditNamePrefixed); dest.writeString(subredditIconUrl); dest.writeString(author); dest.writeString(authorNamePrefixed); dest.writeString(authorIconUrl); dest.writeString(authorFlair); dest.writeString(authorFlairHTML); dest.writeString(title); dest.writeString(selfText); dest.writeString(selfTextPlain); dest.writeString(selfTextPlainTrimmed); dest.writeString(url); dest.writeString(videoUrl); dest.writeString(videoDownloadUrl); dest.writeString(videoFallBackDirectUrl); dest.writeString(thumbnailUrl); dest.writeString(redgifsId); dest.writeString(streamableShortCode); dest.writeByte((byte) (isImgur ? 1 : 0)); dest.writeByte((byte) (isRedgifs ? 1 : 0)); dest.writeByte((byte) (isStreamable ? 1 : 0)); dest.writeByte((byte) (isTumblr ? 1 : 0)); dest.writeByte((byte) (loadedStreamableVideoAlready ? 1 : 0)); dest.writeString(permalink); dest.writeString(flair); dest.writeLong(postTimeMillis); dest.writeInt(score); dest.writeInt(postType); dest.writeInt(voteType); dest.writeInt(nComments); dest.writeInt(upvoteRatio); dest.writeByte((byte) (hidden ? 1 : 0)); dest.writeByte((byte) (spoiler ? 1 : 0)); dest.writeByte((byte) (nsfw ? 1 : 0)); dest.writeByte((byte) (stickied ? 1 : 0)); dest.writeByte((byte) (archived ? 1 : 0)); dest.writeByte((byte) (locked ? 1 : 0)); dest.writeByte((byte) (saved ? 1 : 0)); dest.writeByte((byte) (sendReplies ? 1 : 0)); dest.writeByte((byte) (isCrosspost ? 1 : 0)); dest.writeByte((byte) (canModPost ? 1 : 0)); dest.writeByte((byte) (approved ? 1 : 0)); dest.writeLong(approvedAtUTC); dest.writeString(approvedBy); dest.writeByte((byte) (removed ? 1 : 0)); dest.writeByte((byte) (spam ? 1 : 0)); dest.writeByte((byte) (isRead ? 1 : 0)); dest.writeString(crosspostParentId); dest.writeString(distinguished); dest.writeString(suggestedSort); dest.writeString(mp4Variant); dest.writeTypedList(previews); dest.writeValue(mediaMetadataMap); dest.writeTypedList(gallery); } public boolean isStickied() { return stickied; } public void setIsStickied(boolean value) { stickied = value; } public boolean isArchived() { return archived; } public boolean isLocked() { return locked; } public void setIsLocked(boolean value) { locked = value; } public boolean isSaved() { return saved; } public void setSaved(boolean saved) { this.saved = saved; } public boolean isSendReplies() { return sendReplies; } public void setSendReplies(boolean sendReplies) { this.sendReplies = sendReplies; } public boolean isCrosspost() { return isCrosspost; } public boolean isCanModPost() { return canModPost; } public boolean isApproved() { return approved; } public void setApproved(boolean approved) { this.approved = approved; } public long getApprovedAtUTC() { return approvedAtUTC; } public void setApprovedAtUTC(long approvedAtUTC) { this.approvedAtUTC = approvedAtUTC; } public String getApprovedBy() { return approvedBy; } public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; } public boolean isRemoved() { return removed; } public void setRemoved(boolean removed, boolean spam) { this.removed = removed; this.spam = spam; } public boolean isSpam() { return spam; } public void markAsRead() { isRead = true; } public boolean isRead() { return isRead; } public String getCrosspostParentId() { return crosspostParentId; } public void setCrosspostParentId(String crosspostParentId) { this.crosspostParentId = crosspostParentId; } public ArrayList getPreviews() { return previews; } public void setPreviews(ArrayList previews) { this.previews = previews; } @Nullable public Map getMediaMetadataMap() { return mediaMetadataMap; } public void setMediaMetadataMap(@Nullable Map mediaMetadataMap) { this.mediaMetadataMap = mediaMetadataMap; } public ArrayList getGallery() { return gallery; } public void setGallery(ArrayList gallery) { this.gallery = gallery; } public String getMp4Variant() { return mp4Variant; } public void setMp4Variant(String mp4Variant) { this.mp4Variant = mp4Variant; } @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof Post)) { return false; } return ((Post) obj).id.equals(id); } @Override public int hashCode() { return id.hashCode(); } public static class Gallery implements Parcelable { public static final int TYPE_IMAGE = 0; public static final int TYPE_GIF = 1; public static final int TYPE_VIDEO = 2; public String mimeType; public String url; public String fallbackUrl; private boolean hasFallback; public String fileName; public int mediaType; public String caption; public String captionUrl; public Gallery(String mimeType, String url, String fallbackUrl, String fileName, String caption, String captionUrl) { this.mimeType = mimeType; this.url = url; this.fallbackUrl = fallbackUrl; this.fileName = fileName; if (mimeType.contains("gif")) { mediaType = TYPE_GIF; } else if (mimeType.contains("jpg") || mimeType.contains("png")) { mediaType = TYPE_IMAGE; } else { mediaType = TYPE_VIDEO; } this.caption = caption; this.captionUrl = captionUrl; } protected Gallery(Parcel in) { mimeType = in.readString(); url = in.readString(); fallbackUrl = in.readString(); hasFallback = in.readByte() != 0; fileName = in.readString(); mediaType = in.readInt(); caption = in.readString(); captionUrl = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public Gallery createFromParcel(Parcel in) { return new Gallery(in); } @Override public Gallery[] newArray(int size) { return new Gallery[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(mimeType); parcel.writeString(url); parcel.writeString(fallbackUrl); parcel.writeByte((byte) (hasFallback ? 1 : 0)); parcel.writeString(fileName); parcel.writeInt(mediaType); parcel.writeString(caption); parcel.writeString(captionUrl); } public void setFallbackUrl(String fallbackUrl) { this.fallbackUrl = fallbackUrl; } public void setHasFallback(boolean hasFallback) { this.hasFallback = hasFallback; } public boolean hasFallback() { return this.hasFallback; } } public static class Preview implements Parcelable { private String previewUrl; private int previewWidth; private int previewHeight; private String previewCaption; private String previewCaptionUrl; public Preview(String previewUrl, int previewWidth, int previewHeight, String previewCaption, String previewCaptionUrl) { this.previewUrl = previewUrl; this.previewWidth = previewWidth; this.previewHeight = previewHeight; this.previewCaption = previewCaption; this.previewCaptionUrl = previewCaptionUrl; } protected Preview(Parcel in) { previewUrl = in.readString(); previewWidth = in.readInt(); previewHeight = in.readInt(); previewCaption = in.readString(); previewCaptionUrl = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public Preview createFromParcel(Parcel in) { return new Preview(in); } @Override public Preview[] newArray(int size) { return new Preview[size]; } }; public String getPreviewUrl() { return previewUrl; } public void setPreviewUrl(String previewUrl) { this.previewUrl = previewUrl; } public int getPreviewWidth() { return previewWidth; } public void setPreviewWidth(int previewWidth) { this.previewWidth = previewWidth; } public int getPreviewHeight() { return previewHeight; } public void setPreviewHeight(int previewHeight) { this.previewHeight = previewHeight; } public String getPreviewCaption() { return previewCaption; } public void setPreviewCaption(String previewCaption) { this.previewCaption = previewCaption; } public String getPreviewCaptionUrl() { return previewCaptionUrl; } public void setPreviewCaptionUrl(String previewCaptionUrl) { this.previewCaptionUrl = previewCaptionUrl; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(previewUrl); parcel.writeInt(previewWidth); parcel.writeInt(previewHeight); parcel.writeString(previewCaption); parcel.writeString(previewCaptionUrl); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/PostPagingSource.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.paging.ListenableFuturePagingSource; import androidx.paging.PagingState; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import retrofit2.HttpException; import retrofit2.Response; import retrofit2.Retrofit; public class PostPagingSource extends ListenableFuturePagingSource { public static final int TYPE_FRONT_PAGE = 0; public static final int TYPE_SUBREDDIT = 1; public static final int TYPE_USER = 2; public static final int TYPE_SEARCH = 3; public static final int TYPE_MULTI_REDDIT = 4; public static final int TYPE_ANONYMOUS_FRONT_PAGE = 5; public static final int TYPE_ANONYMOUS_MULTIREDDIT = 6; public static final String USER_WHERE_SUBMITTED = "submitted"; public static final String USER_WHERE_UPVOTED = "upvoted"; public static final String USER_WHERE_DOWNVOTED = "downvoted"; public static final String USER_WHERE_HIDDEN = "hidden"; public static final String USER_WHERE_SAVED = "saved"; private static final int HTTP_INTERNAL_SERVER_ERROR = 500; private final Executor executor; private final Retrofit retrofit; private final String accessToken; private final String accountName; private final SharedPreferences sharedPreferences; private final SharedPreferences postFeedScrolledPositionSharedPreferences; private String subredditOrUserName; private String query; private String trendingSource; private final int postType; private final SortType sortType; private final PostFilter postFilter; private final ReadPostsListInterface readPostsList; private String userWhere; private String multiRedditPath; private final LinkedHashSet postLinkedHashSet; private String previousLastItem; private List multiRedditUsernames; private boolean multiRedditUsernamesFetched = false; private String subredditOnlyName; PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postType = postType; this.sortType = sortType == null ? new SortType(SortType.Type.BEST) : sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } // PostPagingSource.TYPE_SUBREDDIT || PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE || PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String name, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.subredditOrUserName = name; if (subredditOrUserName == null) { subredditOrUserName = "popular"; } this.postType = postType; if (sortType == null) { if ("popular".equals(name) || "all".equals(name)) { this.sortType = new SortType(SortType.Type.HOT); } else { this.sortType = new SortType(SortType.Type.BEST); } } else { this.sortType = sortType; } this.postFilter = postFilter; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } // PostPagingSource.TYPE_MULTI_REDDIT PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String path, String query, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; if (path.endsWith("/")) { multiRedditPath = path.substring(0, path.length() - 1); } else { multiRedditPath = path; } this.query = query; this.postType = postType; if (sortType == null) { this.sortType = new SortType(SortType.Type.HOT); } else { this.sortType = sortType; } this.postFilter = postFilter; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String subredditOrUserName, int postType, SortType sortType, PostFilter postFilter, String where, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.subredditOrUserName = subredditOrUserName; this.postType = postType; this.sortType = sortType == null ? new SortType(SortType.Type.NEW) : sortType; this.postFilter = postFilter; userWhere = where; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String subredditOrUserName, String query, String trendingSource, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.subredditOrUserName = subredditOrUserName; this.query = query; this.trendingSource = trendingSource; this.postType = postType; this.sortType = sortType == null ? new SortType(SortType.Type.RELEVANCE) : sortType; this.postFilter = postFilter; postLinkedHashSet = new LinkedHashSet<>(); this.readPostsList = readPostsList; } @Nullable @Override public String getRefreshKey(@NonNull PagingState pagingState) { return null; } @NonNull @Override public ListenableFuture> loadFuture(@NonNull LoadParams loadParams) { RedditAPI api = retrofit.create(RedditAPI.class); switch (postType) { case TYPE_FRONT_PAGE: return loadHomePosts(loadParams, api); case TYPE_SUBREDDIT: return loadSubredditPosts(loadParams, api); case TYPE_USER: return loadUserPosts(loadParams, api); case TYPE_SEARCH: return loadSearchPosts(loadParams, api); case TYPE_MULTI_REDDIT: return loadMultiRedditPosts(loadParams, api); default: return loadAnonymousFrontPageOrMultiredditPosts(loadParams, api); } } public LoadResult transformData(Response response) { if (response.isSuccessful()) { String responseString = response.body(); LinkedHashSet newPosts = ParsePost.parsePostsSync(responseString, -1, postFilter, readPostsList); String lastItem = ParsePost.getLastItem(responseString); if (newPosts == null) { return new LoadResult.Error<>(new Exception("Error parsing posts")); } else { int currentPostsSize = postLinkedHashSet.size(); if (lastItem != null && lastItem.equals(previousLastItem)) { lastItem = null; } previousLastItem = lastItem; postLinkedHashSet.addAll(newPosts); if (currentPostsSize == postLinkedHashSet.size()) { return new LoadResult.Page<>(new ArrayList<>(), null, lastItem); } else { return new LoadResult.Page<>(new ArrayList<>(postLinkedHashSet).subList(currentPostsSize, postLinkedHashSet.size()), null, lastItem); } } } else { return new LoadResult.Error<>(new Exception("Error getting response")); } } private ListenableFuture> loadHomePosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> bestPost; String afterKey; if (loadParams.getKey() == null) { boolean savePostFeedScrolledPosition = sortType != null && sortType.getType() == SortType.Type.BEST && sharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_FRONT_PAGE_SCROLLED_POSITION, false); if (savePostFeedScrolledPosition) { String accountNameForCache = accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_ANONYMOUS : accountName; afterKey = postFeedScrolledPositionSharedPreferences.getString(accountNameForCache + SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_FRONT_PAGE_BASE, null); } else { afterKey = null; } } else { afterKey = loadParams.getKey(); } bestPost = api.getBestPostsListenableFuture(sortType.getType(), sortType.getTime(), afterKey, APIUtils.getOAuthHeader(accessToken)); ListenableFuture> pageFuture = Futures.transform(bestPost, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> fetchSubredditPosts(LoadParams loadParams, RedditAPI api, int limit) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { return api.getSubredditBestPostsListenableFuture(subredditOrUserName, sortType.getType(), sortType.getTime(), loadParams.getKey(), limit); } else { return api.getSubredditBestPostsOauthListenableFuture(subredditOrUserName, sortType.getType(), sortType.getTime(), loadParams.getKey(), limit, APIUtils.getOAuthHeader(accessToken)); } } private ListenableFuture> loadSubredditPosts(@NonNull LoadParams loadParams, RedditAPI api) { int[] limit = {APIUtils.subredditAPICallLimit(subredditOrUserName)}; ListenableFuture> subredditPost = fetchSubredditPosts(loadParams, api, limit[0]); // Retry with halved limit on HTTP 500 ListenableFuture> retryOnce = Futures.transformAsync(subredditPost, response -> { if (response.code() == HTTP_INTERNAL_SERVER_ERROR) { limit[0] /= 2; return fetchSubredditPosts(loadParams, api, limit[0]); } return Futures.immediateFuture(response); }, executor); // Retry with halved limit again on HTTP 500 ListenableFuture> retryTwice = Futures.transformAsync(retryOnce, response -> { if (response.code() == HTTP_INTERNAL_SERVER_ERROR) { limit[0] /= 2; return fetchSubredditPosts(loadParams, api, limit[0]); } return Futures.immediateFuture(response); }, executor); ListenableFuture> pageFuture = Futures.transform(retryTwice, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadUserPosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> userPosts; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { userPosts = api.getUserPostsListenableFuture(subredditOrUserName, loadParams.getKey(), sortType.getType(), sortType.getTime()); } else { userPosts = api.getUserPostsOauthListenableFuture(APIUtils.AUTHORIZATION_BASE + accessToken, subredditOrUserName, userWhere, loadParams.getKey(), USER_WHERE_SUBMITTED.equals(userWhere) ? sortType.getType() : null, USER_WHERE_SUBMITTED.equals(userWhere) ? sortType.getTime() : null); } ListenableFuture> pageFuture = Futures.transform(userPosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadSearchPosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> searchPosts; if (subredditOrUserName == null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { searchPosts = api.searchPostsListenableFuture(query, loadParams.getKey(), sortType.getType(), sortType.getTime(), trendingSource); } else { searchPosts = api.searchPostsOauthListenableFuture(query, loadParams.getKey(), sortType.getType(), sortType.getTime(), trendingSource, APIUtils.getOAuthHeader(accessToken)); } } else { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { searchPosts = api.searchPostsInSpecificSubredditListenableFuture(subredditOrUserName, query, sortType.getType(), sortType.getTime(), loadParams.getKey()); } else { searchPosts = api.searchPostsInSpecificSubredditOauthListenableFuture(subredditOrUserName, query, sortType.getType(), sortType.getTime(), loadParams.getKey(), APIUtils.getOAuthHeader(accessToken)); } } ListenableFuture> pageFuture = Futures.transform(searchPosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadMultiRedditPosts(@NonNull LoadParams loadParams, RedditAPI api) { // When searching within multi-reddit, keep original behavior (no user post merging) if (query != null && !query.isEmpty()) { ListenableFuture> multiRedditPosts; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { multiRedditPosts = api.searchMultiRedditPostsListenableFuture(multiRedditPath, query, loadParams.getKey(), sortType.getType(), sortType.getTime()); } else { multiRedditPosts = api.searchMultiRedditPostsOauthListenableFuture(multiRedditPath, query, loadParams.getKey(), sortType.getType(), sortType.getTime(), APIUtils.getOAuthHeader(accessToken)); } ListenableFuture> pageFuture = Futures.transform(multiRedditPosts, this::transformData, executor); return catchErrors(pageFuture); } // Parse composite after key String multiAfterKey = getMainAfterKey(loadParams.getKey()); Map currentUserAfterKeys = parseUserAfterKeys(loadParams.getKey()); final boolean isInitialLoad = loadParams.getKey() == null; // Determine if we have users to merge (or might have on first load) boolean hasUsers = multiRedditUsernames != null && !multiRedditUsernames.isEmpty(); boolean mightHaveUsers = !multiRedditUsernamesFetched && !accountName.equals(Account.ANONYMOUS_ACCOUNT); // Fetch multi-reddit posts (use reduced limit when merging with user posts) ListenableFuture> multiRedditPosts; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { multiRedditPosts = api.getMultiRedditPostsListenableFuture(multiRedditPath, sortType.getType(), multiAfterKey, sortType.getTime()); } else if (hasUsers || mightHaveUsers) { multiRedditPosts = api.getMultiRedditPostsOauthListenableFuture(multiRedditPath, sortType.getType(), multiAfterKey, sortType.getTime(), APIUtils.getOAuthHeader(accessToken), 75); } else { multiRedditPosts = api.getMultiRedditPostsOauthListenableFuture(multiRedditPath, sortType.getType(), multiAfterKey, sortType.getTime(), APIUtils.getOAuthHeader(accessToken)); } // On first load, fetch multi-reddit info to discover user entries, then fire user // post requests as soon as usernames are known (without waiting for multi-reddit posts) if (mightHaveUsers) { multiRedditUsernamesFetched = true; ListenableFuture> multiInfoFuture = api.getMultiRedditInfoListenableFuture( APIUtils.getOAuthHeader(accessToken), multiRedditPath); // As soon as info returns, launch user post fetches immediately ListenableFuture>> userPostsFuture = Futures.transformAsync(multiInfoFuture, infoResponse -> { parseMultiRedditInfoResponse(infoResponse); if (multiRedditUsernames == null || multiRedditUsernames.isEmpty()) { return Futures.immediateFuture(new ArrayList<>()); } List>> userFutures = launchUserPostFetches(api, currentUserAfterKeys, isInitialLoad); return Futures.successfulAsList(userFutures); }, executor); // Wait for multi-reddit posts AND user posts (both in flight simultaneously) ListenableFuture> pageFuture = Futures.whenAllSucceed(multiRedditPosts, userPostsFuture) .call(() -> { Response mainResponse = Futures.getDone(multiRedditPosts); List> userResponses = Futures.getDone(userPostsFuture); return mergeResponses(mainResponse, userResponses, getUsersToFetch(currentUserAfterKeys, isInitialLoad)); }, executor); return catchErrors(pageFuture); } // Subsequent loads: fetch multi-reddit posts and user posts all in parallel if (multiRedditUsernames != null && !multiRedditUsernames.isEmpty()) { List>> userFutures = launchUserPostFetches(api, currentUserAfterKeys, isInitialLoad); ListenableFuture>> allUserPosts = Futures.successfulAsList(userFutures); ListenableFuture> pageFuture = Futures.whenAllSucceed(multiRedditPosts, allUserPosts) .call(() -> { Response mainResponse = Futures.getDone(multiRedditPosts); List> userResponses = Futures.getDone(allUserPosts); return mergeResponses(mainResponse, userResponses, getUsersToFetch(currentUserAfterKeys, isInitialLoad)); }, executor); return catchErrors(pageFuture); } // No users, just transform multi-reddit posts ListenableFuture> pageFuture = Futures.transform(multiRedditPosts, this::transformData, executor); return catchErrors(pageFuture); } private List getUsersToFetch(Map currentUserAfterKeys, boolean isInitialLoad) { List users = new ArrayList<>(); if (multiRedditUsernames == null) return users; for (String username : multiRedditUsernames) { if (!isInitialLoad && currentUserAfterKeys != null && !currentUserAfterKeys.containsKey(username)) { continue; } users.add(username); } return users; } private List>> launchUserPostFetches( RedditAPI api, Map currentUserAfterKeys, boolean isInitialLoad) { List>> futures = new ArrayList<>(); for (String username : multiRedditUsernames) { if (!isInitialLoad && currentUserAfterKeys != null && !currentUserAfterKeys.containsKey(username)) { continue; } String userAfter = (currentUserAfterKeys != null) ? currentUserAfterKeys.get(username) : null; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { futures.add(api.getUserPostsListenableFuture(username, userAfter, sortType.getType(), sortType.getTime(), 25)); } else { futures.add(api.getUserPostsOauthListenableFuture(APIUtils.AUTHORIZATION_BASE + accessToken, username, "submitted", userAfter, sortType.getType(), sortType.getTime(), 25)); } } return futures; } private String getMainAfterKey(String compositeKey) { if (compositeKey == null || !compositeKey.startsWith("{")) return compositeKey; try { String m = new JSONObject(compositeKey).optString("m", null); return (m != null && !m.isEmpty()) ? m : null; } catch (JSONException e) { return compositeKey; } } private Map parseUserAfterKeys(String compositeKey) { if (compositeKey == null || !compositeKey.startsWith("{")) return null; try { JSONObject users = new JSONObject(compositeKey).optJSONObject("u"); if (users == null) return null; Map result = new HashMap<>(); Iterator keys = users.keys(); while (keys.hasNext()) { String key = keys.next(); String val = users.getString(key); if (!val.isEmpty()) result.put(key, val); } return result; } catch (JSONException e) { return null; } } private void parseMultiRedditInfoResponse(Response response) { if (response == null || !response.isSuccessful() || response.body() == null) return; try { JSONObject data = new JSONObject(response.body()).getJSONObject("data"); JSONArray subreddits = data.getJSONArray("subreddits"); multiRedditUsernames = new ArrayList<>(); for (int i = 0; i < subreddits.length(); i++) { String name = subreddits.getJSONObject(i).getString("name"); if (name.startsWith("u_")) { multiRedditUsernames.add(name.substring(2)); } } } catch (JSONException e) { // Failed to parse multi-reddit info } } private LoadResult mergeResponses( Response mainResponse, List> userResponses, List usersToFetch) { int currentPostsSize = postLinkedHashSet.size(); // Parse main multi-reddit response String mainLastItem = null; if (mainResponse != null && mainResponse.isSuccessful()) { String responseString = mainResponse.body(); LinkedHashSet newPosts = ParsePost.parsePostsSync(responseString, -1, postFilter, readPostsList); mainLastItem = ParsePost.getLastItem(responseString); if (newPosts != null) { postLinkedHashSet.addAll(newPosts); } } // If no user responses, return main response only if (userResponses == null || userResponses.isEmpty()) { if (mainLastItem != null && mainLastItem.equals(previousLastItem)) { mainLastItem = null; } previousLastItem = mainLastItem; int newSize = postLinkedHashSet.size(); if (newSize == currentPostsSize) { return new LoadResult.Page<>(new ArrayList<>(), null, mainLastItem); } return new LoadResult.Page<>(new ArrayList<>(postLinkedHashSet).subList(currentPostsSize, newSize), null, mainLastItem); } try { boolean hasMore = false; JSONObject compositeAfter = new JSONObject(); if (mainLastItem != null && !mainLastItem.isEmpty()) { compositeAfter.put("m", mainLastItem); hasMore = true; } // Parse user post responses JSONObject userAfters = new JSONObject(); for (int i = 0; i < usersToFetch.size(); i++) { String username = usersToFetch.get(i); Response userResponse = (i < userResponses.size()) ? userResponses.get(i) : null; if (userResponse != null && userResponse.isSuccessful()) { String responseString = userResponse.body(); LinkedHashSet userPosts = ParsePost.parsePostsSync(responseString, -1, postFilter, readPostsList); String userLastItem = ParsePost.getLastItem(responseString); if (userPosts != null) { postLinkedHashSet.addAll(userPosts); } if (userLastItem != null && !userLastItem.isEmpty()) { userAfters.put(username, userLastItem); hasMore = true; } } } if (userAfters.length() > 0) { compositeAfter.put("u", userAfters); } String nextKey = hasMore ? compositeAfter.toString() : null; return buildSortedPage(currentPostsSize, nextKey); } catch (JSONException e) { return new LoadResult.Error<>(e); } } private LoadResult buildSortedPage(int currentPostsSize, String nextKey) { int newSize = postLinkedHashSet.size(); if (newSize == currentPostsSize) { return new LoadResult.Page<>(new ArrayList<>(), null, nextKey); } List resultPosts = new ArrayList<>(postLinkedHashSet).subList(currentPostsSize, newSize); resultPosts = new ArrayList<>(resultPosts); resultPosts.sort((a, b) -> Long.compare(b.getPostTimeMillis(), a.getPostTimeMillis())); return new LoadResult.Page<>(resultPosts, null, nextKey); } private ListenableFuture> catchErrors(ListenableFuture> future) { ListenableFuture> partial = Futures.catching(future, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partial, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadAnonymousFrontPageOrMultiredditPosts(@NonNull LoadParams loadParams, RedditAPI api) { // For anonymous multireddit, extract user entries from concatenated name on first call if (postType == TYPE_ANONYMOUS_MULTIREDDIT && !multiRedditUsernamesFetched) { multiRedditUsernamesFetched = true; String[] parts = subredditOrUserName.split("\\+"); List subreddits = new ArrayList<>(); multiRedditUsernames = new ArrayList<>(); for (String part : parts) { if (part.startsWith("u_")) { multiRedditUsernames.add(part.substring(2)); } else { subreddits.add(part); } } if (!multiRedditUsernames.isEmpty() && !subreddits.isEmpty()) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < subreddits.size(); i++) { if (i > 0) sb.append("+"); sb.append(subreddits.get(i)); } subredditOnlyName = sb.toString(); } else if (multiRedditUsernames.isEmpty()) { subredditOnlyName = null; } else { // Only users, no subreddits subredditOnlyName = ""; } } // If no users to merge, use original behavior if (multiRedditUsernames == null || multiRedditUsernames.isEmpty()) { ListenableFuture> anonymousHomePosts = api.getAnonymousFrontPageOrMultiredditPostsListenableFuture( subredditOrUserName, sortType.getType(), sortType.getTime(), loadParams.getKey(), APIUtils.subredditAPICallLimit(subredditOrUserName), APIUtils.ANONYMOUS_USER_AGENT); ListenableFuture> pageFuture = Futures.transform(anonymousHomePosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } // Parse composite after key String mainAfterKey = getMainAfterKey(loadParams.getKey()); Map currentUserAfterKeys = parseUserAfterKeys(loadParams.getKey()); final boolean isInitialLoad = loadParams.getKey() == null; boolean hasSubreddits = subredditOnlyName != null && !subredditOnlyName.isEmpty(); // Launch subreddit posts fetch (reduced limit since we're merging with user posts) ListenableFuture> mainFuture; if (hasSubreddits) { mainFuture = api.getAnonymousFrontPageOrMultiredditPostsListenableFuture( subredditOnlyName, sortType.getType(), sortType.getTime(), mainAfterKey, 75, APIUtils.ANONYMOUS_USER_AGENT); } else { mainFuture = Futures.immediateFuture(null); } // Launch user post fetches in parallel List>> userFutures = launchUserPostFetches(api, currentUserAfterKeys, isInitialLoad); ListenableFuture>> allUserPosts = Futures.successfulAsList(userFutures); ListenableFuture> pageFuture = Futures.whenAllSucceed(mainFuture, allUserPosts) .call(() -> { Response mainResponse = Futures.getDone(mainFuture); List> userResponses = Futures.getDone(allUserPosts); return mergeResponses(mainResponse, userResponses, getUsersToFetch(currentUserAfterKeys, isInitialLoad)); }, executor); return catchErrors(pageFuture); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/PostViewModel.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelKt; import androidx.lifecycle.ViewModelProvider; import androidx.paging.Pager; import androidx.paging.PagingConfig; import androidx.paging.PagingData; import androidx.paging.PagingDataTransforms; import androidx.paging.PagingLiveData; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.SingleLiveEvent; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class PostViewModel extends ViewModel { private final Executor executor; private final Retrofit retrofit; private final String accessToken; private final String accountName; private final SharedPreferences sharedPreferences; private final SharedPreferences postFeedScrolledPositionSharedPreferences; private String name; private String query; private String trendingSource; private final int postType; private SortType sortType; private PostFilter postFilter; private String userWhere; private ReadPostsListInterface readPostsList; private final MutableLiveData hideReadPostsValue = new MutableLiveData<>(); private final LiveData> posts; private final LiveData> postsWithReadPostsHidden; private final MutableLiveData sortTypeLiveData; private final MutableLiveData postFilterLiveData; private final SortTypeAndPostFilterLiveData sortTypeAndPostFilterLiveData; public final SingleLiveEvent moderationEventLiveData = new SingleLiveEvent<>(); // PostPagingSource.TYPE_FRONT_PAGE public PostViewModel(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, @Nullable SharedPreferences postHistorySharedPreferences, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; sortTypeLiveData = new MutableLiveData<>(sortType); postFilterLiveData = new MutableLiveData<>(postFilter); sortTypeAndPostFilterLiveData = new SortTypeAndPostFilterLiveData(sortTypeLiveData, postFilterLiveData); Pager pager = new Pager<>(new PagingConfig(100, 4, false, 10), this::returnPagingSoruce); posts = Transformations.switchMap(sortTypeAndPostFilterLiveData, sortAndPostFilter -> { changeSortTypeAndPostFilter( sortTypeLiveData.getValue(), postFilterLiveData.getValue()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), ViewModelKt.getViewModelScope(this)); }); postsWithReadPostsHidden = PagingLiveData.cachedIn(Transformations.switchMap(hideReadPostsValue, currentlyReadPostIds -> Transformations.map( posts, postPagingData -> PagingDataTransforms.filter( postPagingData, executor, post -> !post.isRead() || !hideReadPostsValue.getValue()))), ViewModelKt.getViewModelScope(this)); hideReadPostsValue.setValue(postHistorySharedPreferences != null && postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_BASE, false)); } // PostPagingSource.TYPE_SUBREDDIT || PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE || PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT public PostViewModel(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, @Nullable SharedPreferences postHistorySharedPreferences, String subredditName, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; this.name = subredditName; sortTypeLiveData = new MutableLiveData<>(sortType); postFilterLiveData = new MutableLiveData<>(postFilter); sortTypeAndPostFilterLiveData = new SortTypeAndPostFilterLiveData(sortTypeLiveData, postFilterLiveData); Pager pager = new Pager<>(new PagingConfig(100, 4, false, 10), this::returnPagingSoruce); posts = Transformations.switchMap(sortTypeAndPostFilterLiveData, sortAndPostFilter -> { changeSortTypeAndPostFilter( sortTypeLiveData.getValue(), postFilterLiveData.getValue()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), ViewModelKt.getViewModelScope(this)); }); postsWithReadPostsHidden = PagingLiveData.cachedIn(Transformations.switchMap(hideReadPostsValue, currentlyReadPostIds -> Transformations.map( posts, postPagingData -> PagingDataTransforms.filter( postPagingData, executor, post -> !post.isRead() || !hideReadPostsValue.getValue()))), ViewModelKt.getViewModelScope(this)); hideReadPostsValue.setValue(postHistorySharedPreferences != null && postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_BASE, false) && ((postType != PostPagingSource.TYPE_SUBREDDIT || subredditName.equals("all") || subredditName.equals("popular")) || postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_SUBREDDITS_BASE, false))); } // PostPagingSource.TYPE_MULTI_REDDIT public PostViewModel(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, @Nullable SharedPreferences postHistorySharedPreferences, String multiredditPath, String query, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; this.name = multiredditPath; this.query = query; sortTypeLiveData = new MutableLiveData<>(sortType); postFilterLiveData = new MutableLiveData<>(postFilter); sortTypeAndPostFilterLiveData = new SortTypeAndPostFilterLiveData(sortTypeLiveData, postFilterLiveData); Pager pager = new Pager<>(new PagingConfig(100, 4, false, 10), this::returnPagingSoruce); posts = Transformations.switchMap(sortTypeAndPostFilterLiveData, sortAndPostFilter -> { changeSortTypeAndPostFilter( sortTypeLiveData.getValue(), postFilterLiveData.getValue()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), ViewModelKt.getViewModelScope(this)); }); postsWithReadPostsHidden = PagingLiveData.cachedIn(Transformations.switchMap(hideReadPostsValue, currentlyReadPostIds -> Transformations.map( posts, postPagingData -> PagingDataTransforms.filter( postPagingData, executor, post -> !post.isRead() || !hideReadPostsValue.getValue()))), ViewModelKt.getViewModelScope(this)); hideReadPostsValue.setValue(postHistorySharedPreferences != null && postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_BASE, false)); } // PostPagingSource.TYPE_USER public PostViewModel(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, @Nullable SharedPreferences postHistorySharedPreferences, String username, int postType, SortType sortType, PostFilter postFilter, String userWhere, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; this.name = username; this.userWhere = userWhere; sortTypeLiveData = new MutableLiveData<>(sortType); postFilterLiveData = new MutableLiveData<>(postFilter); sortTypeAndPostFilterLiveData = new SortTypeAndPostFilterLiveData(sortTypeLiveData, postFilterLiveData); Pager pager = new Pager<>(new PagingConfig(100, 4, false, 10), this::returnPagingSoruce); posts = Transformations.switchMap(sortTypeAndPostFilterLiveData, sortAndPostFilter -> { changeSortTypeAndPostFilter( sortTypeLiveData.getValue(), postFilterLiveData.getValue()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), ViewModelKt.getViewModelScope(this)); }); postsWithReadPostsHidden = PagingLiveData.cachedIn(Transformations.switchMap(hideReadPostsValue, currentlyReadPostIds -> Transformations.map( posts, postPagingData -> PagingDataTransforms.filter( postPagingData, executor, post -> !post.isRead() || !hideReadPostsValue.getValue()))), ViewModelKt.getViewModelScope(this)); hideReadPostsValue.setValue(postHistorySharedPreferences != null && postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_BASE, false) && postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_USERS_BASE, false)); } // postType == PostPagingSource.TYPE_SEARCH public PostViewModel(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, @Nullable SharedPreferences postHistorySharedPreferences, String subredditName, String query, String trendingSource, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; this.name = subredditName; this.query = query; this.trendingSource = trendingSource; sortTypeLiveData = new MutableLiveData<>(sortType); postFilterLiveData = new MutableLiveData<>(postFilter); sortTypeAndPostFilterLiveData = new SortTypeAndPostFilterLiveData(sortTypeLiveData, postFilterLiveData); Pager pager = new Pager<>(new PagingConfig(100, 4, false, 10), this::returnPagingSoruce); posts = Transformations.switchMap(sortTypeAndPostFilterLiveData, sortAndPostFilter -> { changeSortTypeAndPostFilter( sortTypeLiveData.getValue(), postFilterLiveData.getValue()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), ViewModelKt.getViewModelScope(this)); }); postsWithReadPostsHidden = PagingLiveData.cachedIn(Transformations.switchMap(hideReadPostsValue, currentlyReadPostIds -> Transformations.map( posts, postPagingData -> PagingDataTransforms.filter( postPagingData, executor, post -> !post.isRead() || !hideReadPostsValue.getValue()))), ViewModelKt.getViewModelScope(this)); hideReadPostsValue.setValue(postHistorySharedPreferences != null && postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_BASE, false) && postHistorySharedPreferences.getBoolean((accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : accountName) + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_SEARCH_BASE, false)); } public LiveData> getPosts() { return postsWithReadPostsHidden; } public void hideReadPosts() { hideReadPostsValue.setValue(true); } public PostPagingSource returnPagingSoruce() { PostPagingSource paging3PagingSource; switch (postType) { case PostPagingSource.TYPE_FRONT_PAGE: paging3PagingSource = new PostPagingSource(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, postType, sortType, postFilter, readPostsList); break; case PostPagingSource.TYPE_SUBREDDIT: case PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE: case PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: paging3PagingSource = new PostPagingSource(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, name, postType, sortType, postFilter, readPostsList); break; case PostPagingSource.TYPE_MULTI_REDDIT: paging3PagingSource = new PostPagingSource(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, name, query, postType, sortType, postFilter, readPostsList); break; case PostPagingSource.TYPE_SEARCH: paging3PagingSource = new PostPagingSource(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, name, query, trendingSource, postType, sortType, postFilter, readPostsList); break; default: //User paging3PagingSource = new PostPagingSource(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, name, postType, sortType, postFilter, userWhere, readPostsList); break; } return paging3PagingSource; } private void changeSortTypeAndPostFilter(SortType sortType, PostFilter postFilter) { this.sortType = sortType; this.postFilter = postFilter; } public void changeSortType(SortType sortType) { sortTypeLiveData.postValue(sortType); } public void changePostFilter(PostFilter postFilter) { postFilterLiveData.postValue(postFilter); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Executor executor; private final Retrofit retrofit; private String accessToken; private String accountName; private final SharedPreferences sharedPreferences; private SharedPreferences postFeedScrolledPositionSharedPreferences; private SharedPreferences postHistorySharedPreferences; private String name; private String query; private String trendingSource; private final int postType; private final SortType sortType; private final PostFilter postFilter; private String userWhere; private final ReadPostsListInterface readPostsList; // Front page public Factory(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, SharedPreferences postHistorySharedPreferences, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postHistorySharedPreferences = postHistorySharedPreferences; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; } // PostPagingSource.TYPE_SUBREDDIT public Factory(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, SharedPreferences postHistorySharedPreferences, String name, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postHistorySharedPreferences = postHistorySharedPreferences; this.name = name; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; } // PostPagingSource.TYPE_MULTI_REDDIT public Factory(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, SharedPreferences postHistorySharedPreferences, String name, String query, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postHistorySharedPreferences = postHistorySharedPreferences; this.name = name; this.query = query; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; } //User posts public Factory(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, SharedPreferences postHistorySharedPreferences, String username, int postType, SortType sortType, PostFilter postFilter, String where, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postHistorySharedPreferences = postHistorySharedPreferences; this.name = username; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; userWhere = where; this.readPostsList = readPostsList; } // PostPagingSource.TYPE_SEARCH public Factory(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, SharedPreferences postHistorySharedPreferences, String name, String query, String trendingSource, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postHistorySharedPreferences = postHistorySharedPreferences; this.name = name; this.query = query; this.trendingSource = trendingSource; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; } //Anonymous Front Page public Factory(Executor executor, Retrofit retrofit, SharedPreferences sharedPreferences, String concatenatedSubredditNames, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.sharedPreferences = sharedPreferences; this.name = concatenatedSubredditNames; this.postType = postType; this.sortType = sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; } @NonNull @Override public T create(@NonNull Class modelClass) { if (postType == PostPagingSource.TYPE_FRONT_PAGE) { return (T) new PostViewModel(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, postHistorySharedPreferences, postType, sortType, postFilter, readPostsList); } else if (postType == PostPagingSource.TYPE_SEARCH) { return (T) new PostViewModel(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, postHistorySharedPreferences, name, query, trendingSource, postType, sortType, postFilter, readPostsList); } else if (postType == PostPagingSource.TYPE_SUBREDDIT) { return (T) new PostViewModel(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, postHistorySharedPreferences, name, postType, sortType, postFilter, readPostsList); } else if (postType == PostPagingSource.TYPE_MULTI_REDDIT) { return (T) new PostViewModel(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, postHistorySharedPreferences, name, query, postType, sortType, postFilter, readPostsList); } else if (postType == PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE || postType == PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT) { return (T) new PostViewModel(executor, retrofit, null, null, sharedPreferences, null, null, name, postType, sortType, postFilter, readPostsList); } else { return (T) new PostViewModel(executor, retrofit, accessToken, accountName, sharedPreferences, postFeedScrolledPositionSharedPreferences, postHistorySharedPreferences, name, postType, sortType, postFilter, userWhere, readPostsList); } } } private static class SortTypeAndPostFilterLiveData extends MediatorLiveData> { public SortTypeAndPostFilterLiveData(LiveData sortTypeLiveData, LiveData postFilterLiveData) { addSource(sortTypeLiveData, sortType -> setValue(Pair.create(postFilterLiveData.getValue(), sortType))); addSource(postFilterLiveData, postFilter -> setValue(Pair.create(postFilter, sortTypeLiveData.getValue()))); } } public void approvePost(@NonNull Post post, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); retrofit.create(RedditAPI.class).approveThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setApproved(true); post.setApprovedBy(accountName); post.setApprovedAtUTC(System.currentTimeMillis()); post.setRemoved(false, false); moderationEventLiveData.postValue(new PostModerationEvent.Approved(post, position)); } else { moderationEventLiveData.postValue(new PostModerationEvent.ApproveFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(new PostModerationEvent.ApproveFailed(post, position)); } }); } public void removePost(@NonNull Post post, int position, boolean isSpam) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); params.put(APIUtils.SPAM_KEY, Boolean.toString(isSpam)); retrofit.create(RedditAPI.class).removeThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setApproved(false); post.setApprovedBy(null); post.setApprovedAtUTC(0); post.setRemoved(true, isSpam); moderationEventLiveData.postValue(isSpam ? new PostModerationEvent.MarkedAsSpam(post, position): new PostModerationEvent.Removed(post, position)); } else { moderationEventLiveData.postValue(isSpam ? new PostModerationEvent.MarkAsSpamFailed(post, position) : new PostModerationEvent.RemoveFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(isSpam ? new PostModerationEvent.MarkAsSpamFailed(post, position) : new PostModerationEvent.RemoveFailed(post, position)); } }); } public void toggleSticky(@NonNull Post post, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); params.put(APIUtils.STATE_KEY, Boolean.toString(!post.isStickied())); params.put(APIUtils.API_TYPE_KEY, APIUtils.API_TYPE_JSON); retrofit.create(RedditAPI.class).toggleStickyPost(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setIsStickied(!post.isStickied()); moderationEventLiveData.postValue(post.isStickied() ? new PostModerationEvent.SetStickyPost(post, position): new PostModerationEvent.UnsetStickyPost(post, position)); } else { moderationEventLiveData.postValue(post.isStickied() ? new PostModerationEvent.UnsetStickyPostFailed(post, position) : new PostModerationEvent.SetStickyPostFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(post.isStickied() ? new PostModerationEvent.UnsetStickyPostFailed(post, position) : new PostModerationEvent.SetStickyPostFailed(post, position)); } }); } public void toggleLock(@NonNull Post post, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); Call call = post.isLocked() ? retrofit.create(RedditAPI.class).unLockThing(APIUtils.getOAuthHeader(accessToken), params) : retrofit.create(RedditAPI.class).lockThing(APIUtils.getOAuthHeader(accessToken), params); call.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setIsLocked(!post.isLocked()); moderationEventLiveData.postValue(post.isLocked() ? new PostModerationEvent.Locked(post, position): new PostModerationEvent.Unlocked(post, position)); } else { moderationEventLiveData.postValue(post.isLocked() ? new PostModerationEvent.UnlockFailed(post, position) : new PostModerationEvent.LockFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(post.isLocked() ? new PostModerationEvent.UnlockFailed(post, position) : new PostModerationEvent.LockFailed(post, position)); } }); } public void toggleNSFW(@NonNull Post post, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); Call call = post.isNSFW() ? retrofit.create(RedditAPI.class).unmarkNSFW(APIUtils.getOAuthHeader(accessToken), params) : retrofit.create(RedditAPI.class).markNSFW(APIUtils.getOAuthHeader(accessToken), params); call.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setNSFW(!post.isNSFW()); moderationEventLiveData.postValue(post.isNSFW() ? new PostModerationEvent.MarkedNSFW(post, position): new PostModerationEvent.UnmarkedNSFW(post, position)); } else { moderationEventLiveData.postValue(post.isNSFW() ? new PostModerationEvent.UnmarkNSFWFailed(post, position) : new PostModerationEvent.MarkNSFWFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(post.isNSFW() ? new PostModerationEvent.UnmarkNSFWFailed(post, position) : new PostModerationEvent.MarkNSFWFailed(post, position)); } }); } public void toggleSpoiler(@NonNull Post post, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); Call call = post.isSpoiler() ? retrofit.create(RedditAPI.class).unmarkSpoiler(APIUtils.getOAuthHeader(accessToken), params) : retrofit.create(RedditAPI.class).markSpoiler(APIUtils.getOAuthHeader(accessToken), params); call.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setSpoiler(!post.isSpoiler()); moderationEventLiveData.postValue(post.isSpoiler() ? new PostModerationEvent.MarkedSpoiler(post, position): new PostModerationEvent.UnmarkedSpoiler(post, position)); } else { moderationEventLiveData.postValue(post.isSpoiler() ? new PostModerationEvent.UnmarkSpoilerFailed(post, position) : new PostModerationEvent.MarkSpoilerFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(post.isSpoiler() ? new PostModerationEvent.UnmarkSpoilerFailed(post, position) : new PostModerationEvent.MarkSpoilerFailed(post, position)); } }); } public void toggleMod(@NonNull Post post, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); params.put(APIUtils.HOW_KEY, post.isModerator() ? APIUtils.HOW_NO : APIUtils.HOW_YES); retrofit.create(RedditAPI.class).toggleDistinguishedThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setIsModerator(!post.isModerator()); moderationEventLiveData.postValue(post.isModerator() ? new PostModerationEvent.DistinguishedAsMod(post, position): new PostModerationEvent.UndistinguishedAsMod(post, position)); } else { moderationEventLiveData.postValue(post.isModerator() ? new PostModerationEvent.UndistinguishAsModFailed(post, position) : new PostModerationEvent.DistinguishAsModFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(post.isModerator() ? new PostModerationEvent.UndistinguishAsModFailed(post, position) : new PostModerationEvent.DistinguishAsModFailed(post, position)); } }); } public void toggleNotification(@NonNull Post post, int position) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, post.getFullName()); params.put(APIUtils.STATE_KEY, String.valueOf(!post.isSendReplies())); retrofit.create(RedditAPI.class).toggleRepliesNotification(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { post.setSendReplies(!post.isSendReplies()); moderationEventLiveData.postValue(post.isSendReplies() ? new PostModerationEvent.SetReceiveNotification(post, position): new PostModerationEvent.UnsetReceiveNotification(post, position)); } else { moderationEventLiveData.postValue(post.isSendReplies() ? new PostModerationEvent.UnsetReceiveNotificationFailed(post, position) : new PostModerationEvent.SetReceiveNotificationFailed(post, position)); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { moderationEventLiveData.postValue(post.isSendReplies() ? new PostModerationEvent.UnsetReceiveNotificationFailed(post, position) : new PostModerationEvent.SetReceiveNotificationFailed(post, position)); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/RedditGalleryPayload.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.os.Parcel; import android.os.Parcelable; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.subreddit.Flair; public class RedditGalleryPayload { @SerializedName("sr") public String subredditName; @SerializedName("submit_type") public String submitType; @SerializedName("api_type") public String apiType = "json"; @SerializedName("show_error_list") public boolean showErrorList = true; public String title; public String text; @SerializedName("spoiler") public boolean isSpoiler; @SerializedName("nsfw") public boolean isNSFW; public String kind = "self"; @SerializedName("original_content") public boolean originalContent = false; @SerializedName("post_to_twitter") public boolean postToTwitter = false; @SerializedName("sendreplies") public boolean sendReplies; @SerializedName("validate_on_submit") public boolean validateOnSubmit = true; @SerializedName("flair_id") public String flairId; @SerializedName("flair_text") public String flairText; public ArrayList items; public RedditGalleryPayload(String subredditName, String submitType, String title, String text, boolean isSpoiler, boolean isNSFW, boolean sendReplies, Flair flair, ArrayList items) { this.subredditName = subredditName; this.submitType = submitType; this.title = title; this.text = text; this.isSpoiler = isSpoiler; this.isNSFW = isNSFW; this.sendReplies = sendReplies; if (flair != null) { flairId = flair.getId(); flairText = flair.getText(); } this.items = items; } public static class Item implements Parcelable { public String caption; @SerializedName("outbound_url") public String outboundUrl; @SerializedName("media_id") public String mediaId; public Item(String caption, String outboundUrl, String mediaId) { this.caption = caption; this.outboundUrl = outboundUrl; this.mediaId = mediaId; } protected Item(Parcel in) { caption = in.readString(); outboundUrl = in.readString(); mediaId = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public Item createFromParcel(Parcel in) { return new Item(in); } @Override public Item[] newArray(int size) { return new Item[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(caption); parcel.writeString(outboundUrl); parcel.writeString(mediaId); } public String getCaption() { return caption; } public void setCaption(String caption) { this.caption = caption == null ? "" : caption; } public String getOutboundUrl() { return outboundUrl; } public void setOutboundUrl(String outboundUrl) { this.outboundUrl = outboundUrl; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/post/SubmitPost.java ================================================ package ml.docilealligator.infinityforreddit.post; import android.content.ContentResolver; import android.graphics.Bitmap; import android.net.Uri; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.UploadImageUtils; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; public class SubmitPost { public static void submitTextOrLinkPost(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, String subredditName, String title, String content, @Nullable String url, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications, boolean isRichTextJSON, String kind, SubmitPostListener submitPostListener) { submitPost(executor, handler, oauthRetrofit, accessToken, subredditName, title, content, url, flair, isSpoiler, isNSFW, receivePostReplyNotifications, isRichTextJSON, kind, null, submitPostListener); } public static void submitImagePost(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit uploadMediaRetrofit, ContentResolver contentResolver, String accessToken, String subredditName, String title, String content, Uri imageUri, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications, SubmitPostListener submitPostListener) { try { String imageUrlOrError = UploadImageUtils.uploadImage(oauthRetrofit, uploadMediaRetrofit, contentResolver, accessToken, imageUri); if (imageUrlOrError != null && !imageUrlOrError.startsWith("Error: ")) { submitPost(executor, handler, oauthRetrofit, accessToken, subredditName, title, content, imageUrlOrError, flair, isSpoiler, isNSFW, receivePostReplyNotifications, false, APIUtils.KIND_IMAGE, null, submitPostListener); } else { submitPostListener.submitFailed(imageUrlOrError); } } catch (IOException | JSONException | XmlPullParserException e) { e.printStackTrace(); submitPostListener.submitFailed(e.getMessage()); } } public static void submitVideoPost(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit uploadMediaRetrofit, Retrofit uploadVideoRetrofit, String accessToken, String subredditName, String title, String content, File buffer, String mimeType, Bitmap posterBitmap, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications, SubmitPostListener submitPostListener) { RedditAPI api = oauthRetrofit.create(RedditAPI.class); String fileType = mimeType.substring(mimeType.indexOf("/") + 1); Map uploadImageParams = new HashMap<>(); uploadImageParams.put(APIUtils.FILEPATH_KEY, "post_video." + fileType); uploadImageParams.put(APIUtils.MIMETYPE_KEY, mimeType); Call uploadImageCall = api.uploadImage(APIUtils.getOAuthHeader(accessToken), uploadImageParams); try { Response uploadImageResponse = uploadImageCall.execute(); if (uploadImageResponse.isSuccessful()) { Map nameValuePairsMap = UploadImageUtils.parseJSONResponseFromAWS(uploadImageResponse.body()); RequestBody fileBody = RequestBody.create(buffer, MediaType.parse("application/octet-stream")); MultipartBody.Part fileToUpload = MultipartBody.Part.createFormData("file", "post_video." + fileType, fileBody); RedditAPI uploadVideoToAWSApi; if (fileType.equals("gif")) { uploadVideoToAWSApi = uploadMediaRetrofit.create(RedditAPI.class); } else { uploadVideoToAWSApi = uploadVideoRetrofit.create(RedditAPI.class); } Call uploadMediaToAWS = uploadVideoToAWSApi.uploadMediaToAWS(nameValuePairsMap, fileToUpload); Response uploadMediaToAWSResponse = uploadMediaToAWS.execute(); if (uploadMediaToAWSResponse.isSuccessful()) { String url = UploadImageUtils.parseImageFromXMLResponseFromAWS(uploadMediaToAWSResponse.body()); if (url == null) { submitPostListener.submitFailed(null); return; } String imageUrlOrError = UploadImageUtils.uploadVideoPosterImage(oauthRetrofit, uploadMediaRetrofit, accessToken, posterBitmap); if (imageUrlOrError != null && !imageUrlOrError.startsWith("Error: ")) { if (fileType.equals("gif")) { submitPost(executor, handler, oauthRetrofit, accessToken, subredditName, title, content, url, flair, isSpoiler, isNSFW, receivePostReplyNotifications, false, APIUtils.KIND_VIDEOGIF, imageUrlOrError, submitPostListener); } else { submitPost(executor, handler, oauthRetrofit, accessToken, subredditName, title, content, url, flair, isSpoiler, isNSFW, receivePostReplyNotifications, false, APIUtils.KIND_VIDEO, imageUrlOrError, submitPostListener); } } else { submitPostListener.submitFailed(imageUrlOrError); } } else { submitPostListener.submitFailed(uploadMediaToAWSResponse.code() + " " + uploadMediaToAWSResponse.message()); } } else { submitPostListener.submitFailed(uploadImageResponse.code() + " " + uploadImageResponse.message()); } } catch (IOException | XmlPullParserException | JSONException e) { e.printStackTrace(); submitPostListener.submitFailed(e.getMessage()); } } public static void submitCrosspost(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, String subredditName, String title, String crosspostFullname, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications, String kind, SubmitPostListener submitPostListener) { submitPost(executor, handler, oauthRetrofit, accessToken, subredditName, title, crosspostFullname, null, flair, isSpoiler, isNSFW, receivePostReplyNotifications, false, kind, null, submitPostListener); } private static void submitPost(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, String subredditName, String title, @Nullable String content, @Nullable String url, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications, boolean isRichTextJSON, @NonNull String kind, @Nullable String posterUrl, SubmitPostListener submitPostListener) { RedditAPI api = oauthRetrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.API_TYPE_KEY, APIUtils.API_TYPE_JSON); params.put(APIUtils.SR_KEY, subredditName); params.put(APIUtils.TITLE_KEY, title); params.put(APIUtils.KIND_KEY, kind); switch (kind) { case APIUtils.KIND_SELF: if (isRichTextJSON) { params.put(APIUtils.RICHTEXT_JSON_KEY, content); } else { params.put(APIUtils.TEXT_KEY, content); } break; case APIUtils.KIND_LINK: case APIUtils.KIND_IMAGE: params.put(APIUtils.TEXT_KEY, content); params.put(APIUtils.URL_KEY, url); break; case APIUtils.KIND_VIDEOGIF: params.put(APIUtils.TEXT_KEY, content); params.put(APIUtils.KIND_KEY, APIUtils.KIND_IMAGE); params.put(APIUtils.URL_KEY, url); params.put(APIUtils.VIDEO_POSTER_URL_KEY, posterUrl); break; case APIUtils.KIND_VIDEO: params.put(APIUtils.TEXT_KEY, content); params.put(APIUtils.URL_KEY, url); params.put(APIUtils.VIDEO_POSTER_URL_KEY, posterUrl); break; case APIUtils.KIND_CROSSPOST: params.put(APIUtils.CROSSPOST_FULLNAME_KEY, content); break; } if (flair != null) { params.put(APIUtils.FLAIR_TEXT_KEY, flair.getText()); params.put(APIUtils.FLAIR_ID_KEY, flair.getId()); } params.put(APIUtils.SPOILER_KEY, Boolean.toString(isSpoiler)); params.put(APIUtils.NSFW_KEY, Boolean.toString(isNSFW)); params.put(APIUtils.SEND_REPLIES_KEY, Boolean.toString(receivePostReplyNotifications)); Call submitPostCall = api.submit(APIUtils.getOAuthHeader(accessToken), params); try { Response response = submitPostCall.execute(); if (response.isSuccessful()) { getSubmittedPost(executor, handler, response.body(), kind, oauthRetrofit, accessToken, submitPostListener); } else { submitPostListener.submitFailed(response.message()); } } catch (IOException | JSONException e) { e.printStackTrace(); submitPostListener.submitFailed(e.getMessage()); } } private static void getSubmittedPost(Executor executor, Handler handler, String response, String kind, Retrofit oauthRetrofit, String accessToken, SubmitPostListener submitPostListener) throws JSONException, IOException { JSONObject responseObject = new JSONObject(response).getJSONObject(JSONUtils.JSON_KEY); if (responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() != 0) { JSONArray error = responseObject.getJSONArray(JSONUtils.ERRORS_KEY) .getJSONArray(responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() - 1); if (error.length() != 0) { String errorString; if (error.length() >= 2) { errorString = error.getString(1); } else { errorString = error.getString(0); } submitPostListener.submitFailed(errorString); } else { submitPostListener.submitFailed(null); } return; } if (!kind.equals(APIUtils.KIND_IMAGE) && !kind.equals(APIUtils.KIND_VIDEO) && !kind.equals(APIUtils.KIND_VIDEOGIF)) { String postId = responseObject.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.ID_KEY); RedditAPI api = oauthRetrofit.create(RedditAPI.class); Call getPostCall = api.getPostOauth(postId, APIUtils.getOAuthHeader(accessToken)); Response getPostCallResponse = getPostCall.execute(); if (getPostCallResponse.isSuccessful()) { ParsePost.parsePost(executor, handler, getPostCallResponse.body(), new ParsePost.ParsePostListener() { @Override public void onParsePostSuccess(Post post) { submitPostListener.submitSuccessful(post); } @Override public void onParsePostFail() { submitPostListener.submitFailed(null); } }); } else { submitPostListener.submitFailed(getPostCallResponse.message()); } } else { submitPostListener.submitSuccessful(null); } } public interface SubmitPostListener { void submitSuccessful(Post post); void submitFailed(@Nullable String errorMessage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/DeletePostFilter.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeletePostFilter { public static void deletePostFilter(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, PostFilter postFilter) { executor.execute(() -> redditDataRoomDatabase.postFilterDao().deletePostFilter(postFilter)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/DeletePostFilterUsage.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class DeletePostFilterUsage { public static void deletePostFilterUsage(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, PostFilterUsage postFilterUsage) { executor.execute(() -> redditDataRoomDatabase.postFilterUsageDao().deletePostFilterUsage(postFilterUsage)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/PostFilter.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import ml.docilealligator.infinityforreddit.post.Post; @Entity(tableName = "post_filter") public class PostFilter implements Parcelable { @PrimaryKey @NonNull @ColumnInfo(name = "name") public String name = "New Filter"; @ColumnInfo(name = "max_vote") public int maxVote = -1; @ColumnInfo(name = "min_vote") public int minVote = -1; @ColumnInfo(name = "max_comments") public int maxComments = -1; @ColumnInfo(name = "min_comments") public int minComments = -1; @ColumnInfo(name = "max_awards") public int maxAwards = -1; @ColumnInfo(name = "min_awards") public int minAwards = -1; @Ignore public boolean allowNSFW; @ColumnInfo(name = "only_nsfw") public boolean onlyNSFW; @ColumnInfo(name = "only_spoiler") public boolean onlySpoiler; @ColumnInfo(name = "post_title_excludes_regex") public String postTitleExcludesRegex; @ColumnInfo(name = "post_title_contains_regex") public String postTitleContainsRegex; @ColumnInfo(name = "post_title_excludes_strings") public String postTitleExcludesStrings; @ColumnInfo(name = "post_title_contains_strings") public String postTitleContainsStrings; @ColumnInfo(name = "exclude_subreddits") public String excludeSubreddits; @ColumnInfo(name = "contain_subreddits") public String containSubreddits; @ColumnInfo(name = "exclude_users") public String excludeUsers; @ColumnInfo(name = "contain_users") public String containUsers; @ColumnInfo(name = "contain_flairs") public String containFlairs; @ColumnInfo(name = "exclude_flairs") public String excludeFlairs; @ColumnInfo(name = "exclude_domains") public String excludeDomains; @ColumnInfo(name = "contain_domains") public String containDomains; @ColumnInfo(name = "contain_text_type") public boolean containTextType = true; @ColumnInfo(name = "contain_link_type") public boolean containLinkType = true; @ColumnInfo(name = "contain_image_type") public boolean containImageType = true; @ColumnInfo(name = "contain_gif_type") public boolean containGifType = true; @ColumnInfo(name = "contain_video_type") public boolean containVideoType = true; @ColumnInfo(name = "contain_gallery_type") public boolean containGalleryType = true; public PostFilter() { } protected PostFilter(Parcel in) { name = in.readString(); maxVote = in.readInt(); minVote = in.readInt(); maxComments = in.readInt(); minComments = in.readInt(); maxAwards = in.readInt(); minAwards = in.readInt(); allowNSFW = in.readByte() != 0; onlyNSFW = in.readByte() != 0; onlySpoiler = in.readByte() != 0; postTitleExcludesRegex = in.readString(); postTitleContainsRegex = in.readString(); postTitleExcludesStrings = in.readString(); postTitleContainsStrings = in.readString(); excludeSubreddits = in.readString(); containSubreddits = in.readString(); excludeUsers = in.readString(); containUsers = in.readString(); containFlairs = in.readString(); excludeFlairs = in.readString(); excludeDomains = in.readString(); containDomains = in.readString(); containTextType = in.readByte() != 0; containLinkType = in.readByte() != 0; containImageType = in.readByte() != 0; containGifType = in.readByte() != 0; containVideoType = in.readByte() != 0; containGalleryType = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public PostFilter createFromParcel(Parcel in) { return new PostFilter(in); } @Override public PostFilter[] newArray(int size) { return new PostFilter[size]; } }; public static boolean isPostAllowed(Post post, PostFilter postFilter) { if (postFilter == null || post == null) { return true; } if (post.isNSFW() && !postFilter.allowNSFW) { return false; } if(post.isStickied()){ return true; } if (postFilter.maxVote > 0 && post.getVoteType() + post.getScore() > postFilter.maxVote) { return false; } if (postFilter.minVote > 0 && post.getVoteType() + post.getScore() < postFilter.minVote) { return false; } if (postFilter.maxComments > 0 && post.getNComments() > postFilter.maxComments) { return false; } if (postFilter.minComments > 0 && post.getNComments() < postFilter.minComments) { return false; } if (postFilter.onlyNSFW && !post.isNSFW()) { if (postFilter.onlySpoiler) { return post.isSpoiler(); } return false; } if (postFilter.onlySpoiler && !post.isSpoiler()) { if (postFilter.onlyNSFW) { return post.isNSFW(); } return false; } if (!postFilter.containTextType && post.getPostType() == Post.TEXT_TYPE) { return false; } if (!postFilter.containLinkType && (post.getPostType() == Post.LINK_TYPE || post.getPostType() == Post.NO_PREVIEW_LINK_TYPE)) { return false; } if (!postFilter.containImageType && post.getPostType() == Post.IMAGE_TYPE) { return false; } if (!postFilter.containGifType && post.getPostType() == Post.GIF_TYPE) { return false; } if (!postFilter.containVideoType && post.getPostType() == Post.VIDEO_TYPE) { return false; } if (!postFilter.containGalleryType && post.getPostType() == Post.GALLERY_TYPE) { return false; } if (postFilter.postTitleExcludesRegex != null && !postFilter.postTitleExcludesRegex.equals("")) { try { Pattern pattern = Pattern.compile(postFilter.postTitleExcludesRegex); Matcher matcher = pattern.matcher(post.getTitle()); if (matcher.find()) { return false; } } catch (PatternSyntaxException ignore) {} } if (postFilter.postTitleContainsRegex != null && !postFilter.postTitleContainsRegex.equals("")) { try { Pattern pattern = Pattern.compile(postFilter.postTitleContainsRegex); Matcher matcher = pattern.matcher(post.getTitle()); if (!matcher.find()) { return false; } } catch (PatternSyntaxException e) { e.printStackTrace(); } } if (postFilter.postTitleExcludesStrings != null && !postFilter.postTitleExcludesStrings.equals("")) { String[] titles = postFilter.postTitleExcludesStrings.split(",", 0); for (String t : titles) { if (!t.trim().equals("") && post.getTitle().toLowerCase().contains(t.toLowerCase().trim())) { return false; } } } if (postFilter.postTitleContainsStrings != null && !postFilter.postTitleContainsStrings.equals("")) { String[] titles = postFilter.postTitleContainsStrings.split(",", 0); boolean hasRequiredString = false; for (String t : titles) { if (post.getTitle().toLowerCase().contains(t.toLowerCase().trim())) { hasRequiredString = true; break; } } if (!hasRequiredString) { return false; } } if (postFilter.excludeSubreddits != null && !postFilter.excludeSubreddits.equals("")) { String[] subreddits = postFilter.excludeSubreddits.split(",", 0); for (String s : subreddits) { if (!s.trim().equals("") && post.getSubredditName().equalsIgnoreCase(s.trim())) { return false; } } } if (postFilter.containSubreddits != null && !postFilter.containSubreddits.equals("")) { String[] subreddits = postFilter.containSubreddits.split(",", 0); boolean hasRequiredSubreddit = false; String subreddit = post.getSubredditName(); for (String s : subreddits) { if (!s.trim().equals("") && subreddit.equalsIgnoreCase(s.trim())) { hasRequiredSubreddit = true; break; } } if (!hasRequiredSubreddit) { return false; } } if (postFilter.excludeUsers != null && !postFilter.excludeUsers.equals("")) { String[] users = postFilter.excludeUsers.split(",", 0); for (String u : users) { if (!u.trim().equals("") && post.getAuthor().equalsIgnoreCase(u.trim())) { return false; } } } if (postFilter.containUsers != null && !postFilter.containUsers.equals("")) { String[] users = postFilter.containUsers.split(",", 0); boolean hasRequiredUser = false; String user = post.getAuthor(); for (String s : users) { if (!s.trim().equals("") && user.equalsIgnoreCase(s.trim())) { hasRequiredUser = true; break; } } if (!hasRequiredUser) { return false; } } if (postFilter.excludeFlairs != null && !postFilter.excludeFlairs.equals("")) { String[] flairs = postFilter.excludeFlairs.split(",", 0); for (String f : flairs) { if (!f.trim().equals("") && post.getFlair().trim().equalsIgnoreCase(f.trim())) { return false; } } } if (post.getUrl() != null && postFilter.excludeDomains != null && !postFilter.excludeDomains.equals("")) { String[] domains = postFilter.excludeDomains.split(",", 0); String url = post.getUrl().toLowerCase(); for (String f : domains) { if (!f.trim().equals("") && url.contains(f.trim().toLowerCase())) { return false; } } } if (post.getUrl() != null && postFilter.containDomains != null && !postFilter.containDomains.equals("")) { String[] domains = postFilter.containDomains.split(",", 0); String url = post.getUrl().toLowerCase(); boolean hasRequiredDomain = false; for (String f : domains) { if (url.contains(f.trim().toLowerCase())) { hasRequiredDomain = true; break; } } if (!hasRequiredDomain) { return false; } } if (postFilter.containFlairs != null && !postFilter.containFlairs.equals("")) { String[] flairs = postFilter.containFlairs.split(",", 0); if (flairs.length > 0) { boolean match = false; for (int i = 0; i < flairs.length; i++) { String flair = flairs[i].trim(); if (flair.equals("") && i == flairs.length - 1) { return false; } if (!flair.equals("") && post.getFlair().trim().equalsIgnoreCase(flair)) { match = true; break; } } return match; } } return true; } public static PostFilter mergePostFilter(List postFilterList) { if (postFilterList.size() == 1) { return postFilterList.get(0); } PostFilter postFilter = new PostFilter(); StringBuilder stringBuilder; postFilter.name = "Merged"; for (PostFilter p : postFilterList) { postFilter.maxVote = Math.min(p.maxVote, postFilter.maxVote); postFilter.minVote = Math.max(p.minVote, postFilter.minVote); postFilter.maxComments = Math.min(p.maxComments, postFilter.maxComments); postFilter.minComments = Math.max(p.minComments, postFilter.minComments); postFilter.maxAwards = Math.min(p.maxAwards, postFilter.maxAwards); postFilter.minAwards = Math.max(p.minAwards, postFilter.minAwards); postFilter.onlyNSFW = p.onlyNSFW ? p.onlyNSFW : postFilter.onlyNSFW; postFilter.onlySpoiler = p.onlySpoiler ? p.onlySpoiler : postFilter.onlySpoiler; if (p.postTitleExcludesRegex != null && !p.postTitleExcludesRegex.equals("")) { postFilter.postTitleExcludesRegex = p.postTitleExcludesRegex; } if (p.postTitleContainsRegex != null && !p.postTitleContainsRegex.equals("")) { postFilter.postTitleContainsRegex = p.postTitleContainsRegex; } if (p.postTitleExcludesStrings != null && !p.postTitleExcludesStrings.equals("")) { stringBuilder = new StringBuilder(postFilter.postTitleExcludesStrings == null ? "" : postFilter.postTitleExcludesStrings); stringBuilder.append(",").append(p.postTitleExcludesStrings); postFilter.postTitleExcludesStrings = stringBuilder.toString(); } if (p.postTitleContainsStrings != null && !p.postTitleContainsStrings.equals("")) { stringBuilder = new StringBuilder(postFilter.postTitleContainsStrings == null ? "" : postFilter.postTitleContainsStrings); stringBuilder.append(",").append(p.postTitleContainsStrings); postFilter.postTitleContainsStrings = stringBuilder.toString(); } if (p.excludeSubreddits != null && !p.excludeSubreddits.equals("")) { stringBuilder = new StringBuilder(postFilter.excludeSubreddits == null ? "" : postFilter.excludeSubreddits); stringBuilder.append(",").append(p.excludeSubreddits); postFilter.excludeSubreddits = stringBuilder.toString(); } if (p.containSubreddits != null && !p.containSubreddits.equals("")) { stringBuilder = new StringBuilder(postFilter.containSubreddits == null ? "" : postFilter.containSubreddits); stringBuilder.append(",").append(p.containSubreddits); postFilter.containSubreddits = stringBuilder.toString(); } if (p.excludeUsers != null && !p.excludeUsers.equals("")) { stringBuilder = new StringBuilder(postFilter.excludeUsers == null ? "" : postFilter.excludeUsers); stringBuilder.append(",").append(p.excludeUsers); postFilter.excludeUsers = stringBuilder.toString(); } if (p.containUsers != null && !p.containUsers.equals("")) { stringBuilder = new StringBuilder(postFilter.containUsers == null ? "" : postFilter.containUsers); stringBuilder.append(",").append(p.containUsers); postFilter.containUsers = stringBuilder.toString(); } if (p.containFlairs != null && !p.containFlairs.equals("")) { stringBuilder = new StringBuilder(postFilter.containFlairs == null ? "" : postFilter.containFlairs); stringBuilder.append(",").append(p.containFlairs); postFilter.containFlairs = stringBuilder.toString(); } if (p.excludeFlairs != null && !p.excludeFlairs.equals("")) { stringBuilder = new StringBuilder(postFilter.excludeFlairs == null ? "" : postFilter.excludeFlairs); stringBuilder.append(",").append(p.excludeFlairs); postFilter.excludeFlairs = stringBuilder.toString(); } if (p.excludeDomains != null && !p.excludeDomains.equals("")) { stringBuilder = new StringBuilder(postFilter.excludeDomains == null ? "" : postFilter.excludeDomains); stringBuilder.append(",").append(p.excludeDomains); postFilter.excludeDomains = stringBuilder.toString(); } if (p.containDomains != null && !p.containDomains.equals("")) { stringBuilder = new StringBuilder(postFilter.containDomains == null ? "" : postFilter.containDomains); stringBuilder.append(",").append(p.containDomains); postFilter.containDomains = stringBuilder.toString(); } postFilter.containTextType = p.containTextType && postFilter.containTextType; postFilter.containLinkType = p.containLinkType && postFilter.containLinkType; postFilter.containImageType = p.containImageType && postFilter.containImageType; postFilter.containGifType = p.containGifType && postFilter.containGifType; postFilter.containVideoType = p.containVideoType && postFilter.containVideoType; postFilter.containGalleryType = p.containGalleryType && postFilter.containGalleryType; } return postFilter; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(name); parcel.writeInt(maxVote); parcel.writeInt(minVote); parcel.writeInt(maxComments); parcel.writeInt(minComments); parcel.writeInt(maxAwards); parcel.writeInt(minAwards); parcel.writeByte((byte) (allowNSFW ? 1 : 0)); parcel.writeByte((byte) (onlyNSFW ? 1 : 0)); parcel.writeByte((byte) (onlySpoiler ? 1 : 0)); parcel.writeString(postTitleExcludesRegex); parcel.writeString(postTitleContainsRegex); parcel.writeString(postTitleExcludesStrings); parcel.writeString(postTitleContainsStrings); parcel.writeString(excludeSubreddits); parcel.writeString(containSubreddits); parcel.writeString(excludeUsers); parcel.writeString(containUsers); parcel.writeString(containFlairs); parcel.writeString(excludeFlairs); parcel.writeString(excludeDomains); parcel.writeString(containDomains); parcel.writeByte((byte) (containTextType ? 1 : 0)); parcel.writeByte((byte) (containLinkType ? 1 : 0)); parcel.writeByte((byte) (containImageType ? 1 : 0)); parcel.writeByte((byte) (containGifType ? 1 : 0)); parcel.writeByte((byte) (containVideoType ? 1 : 0)); parcel.writeByte((byte) (containGalleryType ? 1 : 0)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/PostFilterDao.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; 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.Transaction; import java.util.List; @Dao public interface PostFilterDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(PostFilter postFilter); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List postFilters); @Query("DELETE FROM post_filter") void deleteAllPostFilters(); @Delete void deletePostFilter(PostFilter postFilter); @Query("DELETE FROM post_filter WHERE name = :name") void deletePostFilter(String name); @Query("SELECT * FROM post_filter WHERE name = :name LIMIT 1") PostFilter getPostFilter(String name); @Query("SELECT * FROM post_filter ORDER BY name") LiveData> getAllPostFiltersLiveData(); @Query("SELECT * FROM post_filter") List getAllPostFilters(); @Query("SELECT * FROM post_filter WHERE post_filter.name IN " + "(SELECT post_filter_usage.name FROM post_filter_usage WHERE (usage = :usage AND name_of_usage = :nameOfUsage COLLATE NOCASE) " + "OR (usage =:usage AND name_of_usage = '--'))") List getValidPostFilters(int usage, String nameOfUsage); @Transaction @Query("SELECT * FROM post_filter ORDER BY name") LiveData> getAllPostFilterWithUsageLiveData(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/PostFilterUsage.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; @Entity(tableName = "post_filter_usage", primaryKeys = {"name", "usage", "name_of_usage"}, foreignKeys = @ForeignKey(entity = PostFilter.class, parentColumns = "name", childColumns = "name", onDelete = ForeignKey.CASCADE)) public class PostFilterUsage implements Parcelable { public static final int HOME_TYPE = 1; public static final int SUBREDDIT_TYPE = 2; public static final int USER_TYPE = 3; public static final int MULTIREDDIT_TYPE = 4; public static final int SEARCH_TYPE = 5; public static final int HISTORY_TYPE = 6; public static final int UPVOTED_TYPE = 7; public static final int DOWNVOTED_TYPE = 8; public static final int HIDDEN_TYPE = 9; public static final int SAVED_TYPE = 10; public static final String HISTORY_TYPE_USAGE_READ_POSTS = "-read-posts"; public static final String NO_USAGE = "--"; @NonNull @ColumnInfo(name = "name") public String name; @ColumnInfo(name = "usage") public int usage; @NonNull @ColumnInfo(name = "name_of_usage") public String nameOfUsage; public PostFilterUsage(@NonNull String name, int usage, String nameOfUsage) { this.name = name; this.usage = usage; if (nameOfUsage == null || nameOfUsage.equals("")) { this.nameOfUsage = NO_USAGE; } else { this.nameOfUsage = nameOfUsage; } } protected PostFilterUsage(Parcel in) { name = in.readString(); usage = in.readInt(); nameOfUsage = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public PostFilterUsage createFromParcel(Parcel in) { return new PostFilterUsage(in); } @Override public PostFilterUsage[] newArray(int size) { return new PostFilterUsage[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(name); parcel.writeInt(usage); parcel.writeString(nameOfUsage); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/PostFilterUsageDao.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; 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 java.util.List; @Dao public interface PostFilterUsageDao { @Query("SELECT * FROM post_filter_usage WHERE name = :name") LiveData> getAllPostFilterUsageLiveData(String name); @Query("SELECT * FROM post_filter_usage WHERE name = :name") List getAllPostFilterUsage(String name); @Query("SELECT * FROM post_filter_usage") List getAllPostFilterUsageForBackup(); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(PostFilterUsage postFilterUsage); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List postFilterUsageList); @Delete void deletePostFilterUsage(PostFilterUsage postFilterUsage); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/PostFilterUsageViewModel.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class PostFilterUsageViewModel extends ViewModel { private final LiveData> mPostFilterUsageListLiveData; public PostFilterUsageViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String name) { mPostFilterUsageListLiveData = redditDataRoomDatabase.postFilterUsageDao().getAllPostFilterUsageLiveData(name); } public LiveData> getPostFilterUsageListLiveData() { return mPostFilterUsageListLiveData; } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mName; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String name) { mRedditDataRoomDatabase = redditDataRoomDatabase; mName = name; } @Override public T create(Class modelClass) { //noinspection unchecked return (T) new PostFilterUsageViewModel(mRedditDataRoomDatabase, mName); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/PostFilterWithUsage.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import androidx.room.Embedded; import androidx.room.Relation; import java.util.List; public class PostFilterWithUsage { @Embedded public PostFilter postFilter; @Relation( parentColumn = "name", entityColumn = "name" ) public List postFilterUsages; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/PostFilterWithUsageViewModel.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class PostFilterWithUsageViewModel extends ViewModel { private final LiveData> mPostFilterWithUsageListLiveData; public PostFilterWithUsageViewModel(RedditDataRoomDatabase redditDataRoomDatabase) { mPostFilterWithUsageListLiveData = redditDataRoomDatabase.postFilterDao().getAllPostFilterWithUsageLiveData(); } public LiveData> getPostFilterWithUsageListLiveData() { return mPostFilterWithUsageListLiveData; } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; public Factory(RedditDataRoomDatabase redditDataRoomDatabase) { mRedditDataRoomDatabase = redditDataRoomDatabase; } @Override public T create(Class modelClass) { //noinspection unchecked return (T) new PostFilterWithUsageViewModel(mRedditDataRoomDatabase); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/SavePostFilter.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import android.os.Handler; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SavePostFilter { public interface SavePostFilterListener { void success(); void duplicate(); } public static void savePostFilter(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, PostFilter postFilter, String originalName, SavePostFilterListener savePostFilterListener) { executor.execute(() -> { if (!originalName.equals(postFilter.name) && redditDataRoomDatabase.postFilterDao().getPostFilter(postFilter.name) != null) { handler.post(savePostFilterListener::duplicate); } else { List postFilterUsages = redditDataRoomDatabase.postFilterUsageDao().getAllPostFilterUsage(originalName); if (!originalName.equals(postFilter.name)) { redditDataRoomDatabase.postFilterDao().deletePostFilter(originalName); } redditDataRoomDatabase.postFilterDao().insert(postFilter); for (PostFilterUsage postFilterUsage : postFilterUsages) { postFilterUsage.name = postFilter.name; redditDataRoomDatabase.postFilterUsageDao().insert(postFilterUsage); } handler.post(savePostFilterListener::success); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/postfilter/SavePostFilterUsage.java ================================================ package ml.docilealligator.infinityforreddit.postfilter; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SavePostFilterUsage { public static void savePostFilterUsage(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, PostFilterUsage postFilterUsage) { executor.execute(() -> redditDataRoomDatabase.postFilterUsageDao().insert(postFilterUsage)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/readpost/InsertReadPost.java ================================================ package ml.docilealligator.infinityforreddit.readpost; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class InsertReadPost { public static void insertReadPost(RedditDataRoomDatabase redditDataRoomDatabase, Executor executor, String username, String postId, int readPostsLimit) { executor.execute(() -> { ReadPostDao readPostDao = redditDataRoomDatabase.readPostDao(); int limit = Math.max(readPostsLimit, 100); boolean isReadPostLimit = readPostsLimit != -1; while (readPostDao.getReadPostsCount(username) > limit && isReadPostLimit) { readPostDao.deleteOldestReadPosts(username); } if (username != null && !username.isEmpty()) { readPostDao.insert(new ReadPost(username, postId)); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/readpost/NullReadPostsList.java ================================================ package ml.docilealligator.infinityforreddit.readpost; import java.util.Collections; import java.util.List; import java.util.Set; public class NullReadPostsList implements ReadPostsListInterface { public static NullReadPostsList getInstance() { return InstanceHolder.instance; } @Override public Set getReadPostsIdsByIds(List ids) { return Collections.emptySet(); } private static class InstanceHolder { private static final NullReadPostsList instance = new NullReadPostsList(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/readpost/ReadPost.java ================================================ package ml.docilealligator.infinityforreddit.readpost; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import ml.docilealligator.infinityforreddit.account.Account; @Entity(tableName = "read_posts", primaryKeys = {"username", "id"}, foreignKeys = @ForeignKey(entity = Account.class, parentColumns = "username", childColumns = "username", onDelete = ForeignKey.CASCADE)) public class ReadPost implements Parcelable { @NonNull @ColumnInfo(name = "username") private String username; @NonNull @ColumnInfo(name = "id") private String id; @ColumnInfo(name = "time") private long time; public ReadPost(@NonNull String username, @NonNull String id) { this.username = username; this.id = id; this.time = System.currentTimeMillis(); } protected ReadPost(Parcel in) { username = in.readString(); id = in.readString(); time = in.readLong(); } public static final Creator CREATOR = new Creator() { @Override public ReadPost createFromParcel(Parcel in) { return new ReadPost(in); } @Override public ReadPost[] newArray(int size) { return new ReadPost[size]; } }; @NonNull public String getUsername() { return username; } public void setUsername(@NonNull String username) { this.username = username; } @NonNull public String getId() { return id; } public void setId(@NonNull String id) { this.id = id; } public long getTime() { return time; } public void setTime(long time) { this.time = time; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(username); parcel.writeString(id); parcel.writeLong(time); } @Override public boolean equals(@Nullable Object obj) { if (obj instanceof ReadPost) { return ((ReadPost) obj).id.equals(id); } return false; } @Override public int hashCode() { return id.hashCode(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/readpost/ReadPostDao.java ================================================ package ml.docilealligator.infinityforreddit.readpost; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import com.google.common.util.concurrent.ListenableFuture; import java.util.List; @Dao public interface ReadPostDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(ReadPost readPost); @Query("SELECT * FROM read_posts WHERE username = :username AND (:before IS NULL OR time < :before) ORDER BY time DESC LIMIT 25") ListenableFuture> getAllReadPostsListenableFuture(String username, Long before); @Query("SELECT * FROM read_posts WHERE username = :username AND (:before IS NULL OR time < :before) ORDER BY time DESC LIMIT 25") List getAllReadPosts(String username, Long before); @Query("SELECT * FROM read_posts WHERE id = :id LIMIT 1") ReadPost getReadPost(String id); @Query("SELECT COUNT(id) FROM read_posts WHERE username = :username") int getReadPostsCount(String username); @Query("DELETE FROM read_posts WHERE rowid IN (SELECT rowid FROM read_posts WHERE username = :username ORDER BY time ASC LIMIT 100)") void deleteOldestReadPosts(String username); @Query("DELETE FROM read_posts") void deleteAllReadPosts(); @Query("SELECT id FROM read_posts WHERE id IN (:ids) AND username = :username") List getReadPostsIdsByIds(List ids, String username); default int getMaxReadPostEntrySize() { // in bytes return 20 + // max username size 10 + // id size 8; // time size } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/readpost/ReadPostsList.java ================================================ package ml.docilealligator.infinityforreddit.readpost; import androidx.annotation.WorkerThread; import java.util.HashSet; import java.util.List; public class ReadPostsList implements ReadPostsListInterface { private final ReadPostDao readPostDao; private final String accountName; private final boolean readPostsDisabled; public ReadPostsList(ReadPostDao readPostDao, String accountName, boolean readPostsDisabled) { this.accountName = accountName; this.readPostDao = readPostDao; this.readPostsDisabled = readPostsDisabled; } @WorkerThread @Override public HashSet getReadPostsIdsByIds(List ids) { return readPostsDisabled ? new HashSet<>() : new HashSet<>(this.readPostDao.getReadPostsIdsByIds(ids, accountName)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/readpost/ReadPostsListInterface.java ================================================ package ml.docilealligator.infinityforreddit.readpost; import java.util.List; import java.util.Set; public interface ReadPostsListInterface { Set getReadPostsIdsByIds(List ids); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/readpost/ReadPostsUtils.java ================================================ package ml.docilealligator.infinityforreddit.readpost; import android.content.SharedPreferences; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class ReadPostsUtils { public static int GetReadPostsLimit(String accountName, SharedPreferences mPostHistorySharedPreferences) { if (mPostHistorySharedPreferences.getBoolean(accountName + SharedPreferencesUtils.READ_POSTS_LIMIT_ENABLED, true)) { return mPostHistorySharedPreferences.getInt(accountName + SharedPreferencesUtils.READ_POSTS_LIMIT, 500); } else { return -1; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/recentsearchquery/InsertRecentSearchQuery.java ================================================ package ml.docilealligator.infinityforreddit.recentsearchquery; import android.os.Handler; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; public class InsertRecentSearchQuery { public interface InsertRecentSearchQueryListener { void success(); } public static void insertRecentSearchQueryListener(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String username, String recentSearchQuery, String searchInSubredditOrUserName, MultiReddit searchInMultiReddit, int searchInThingType, InsertRecentSearchQueryListener insertRecentSearchQueryListener) { executor.execute(() -> { RecentSearchQueryDao recentSearchQueryDao = redditDataRoomDatabase.recentSearchQueryDao(); if (searchInMultiReddit == null) { recentSearchQueryDao.insert(new RecentSearchQuery(username, recentSearchQuery, searchInSubredditOrUserName, null, null, searchInThingType)); } else { recentSearchQueryDao.insert(new RecentSearchQuery(username, recentSearchQuery, searchInSubredditOrUserName, searchInMultiReddit.getPath(), searchInMultiReddit.getDisplayName(), searchInThingType)); } handler.post(insertRecentSearchQueryListener::success); }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/recentsearchquery/RecentSearchQuery.java ================================================ package ml.docilealligator.infinityforreddit.recentsearchquery; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.account.Account; @Entity(tableName = "recent_search_queries", primaryKeys = {"username", "search_query"}, foreignKeys = @ForeignKey(entity = Account.class, parentColumns = "username", childColumns = "username", onDelete = ForeignKey.CASCADE)) public class RecentSearchQuery { @NonNull @ColumnInfo(name = "username") private String username; @NonNull @ColumnInfo(name = "search_query") private String searchQuery; @Nullable @ColumnInfo(name = "search_in_subreddit_or_user_name") private String searchInSubredditOrUserName; @Nullable @ColumnInfo(name = "search_in_multireddit_path") private String multiRedditPath; @Nullable @ColumnInfo(name = "search_in_multireddit_display_name") private String multiRedditDisplayName; @SelectThingReturnKey.THING_TYPE @ColumnInfo(name = "search_in_thing_type") private int searchInThingType; @ColumnInfo(name = "time") private long time; public RecentSearchQuery(@NonNull String username, @NonNull String searchQuery, @Nullable String searchInSubredditOrUserName, @Nullable String multiRedditPath, @Nullable String multiRedditDisplayName, @SelectThingReturnKey.THING_TYPE int searchInThingType) { this.username = username; this.searchQuery = searchQuery; this.searchInSubredditOrUserName = searchInSubredditOrUserName; this.searchInThingType = searchInThingType; this.multiRedditPath = multiRedditPath; this.multiRedditDisplayName = multiRedditDisplayName; this.time = System.currentTimeMillis(); } @NonNull public String getUsername() { return username; } public void setUsername(@NonNull String username) { this.username = username; } @NonNull public String getSearchQuery() { return searchQuery; } public void setSearchQuery(@NonNull String searchQuery) { this.searchQuery = searchQuery; } @Nullable public String getSearchInSubredditOrUserName() { return searchInSubredditOrUserName; } public void setSearchInSubredditOrUserName(@Nullable String searchInSubredditOrUserName) { this.searchInSubredditOrUserName = searchInSubredditOrUserName; } @Nullable public String getMultiRedditPath() { return multiRedditPath; } public void setMultiRedditPath(@Nullable String multiRedditPath) { this.multiRedditPath = multiRedditPath; } @Nullable public String getMultiRedditDisplayName() { return multiRedditDisplayName; } public void setMultiRedditDisplayName(@Nullable String multiRedditDisplayName) { this.multiRedditDisplayName = multiRedditDisplayName; } @SelectThingReturnKey.THING_TYPE public int getSearchInThingType() { return searchInThingType; } public void setSearchInThingType(@SelectThingReturnKey.THING_TYPE int searchInThingType) { this.searchInThingType = searchInThingType; } public long getTime() { return time; } public void setTime(long time) { this.time = time; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/recentsearchquery/RecentSearchQueryDao.java ================================================ package ml.docilealligator.infinityforreddit.recentsearchquery; 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 java.util.List; @Dao public interface RecentSearchQueryDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(RecentSearchQuery recentSearchQuery); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List recentSearchQueries); @Query("SELECT * FROM recent_search_queries WHERE username = :username ORDER BY time DESC") LiveData> getAllRecentSearchQueriesLiveData(String username); @Query("SELECT * FROM recent_search_queries WHERE username = :username ORDER BY time DESC LIMIT :limit") LiveData> getRecentSearchQueriesLiveData(String username, int limit); @Query("SELECT * FROM recent_search_queries WHERE username = :username ORDER BY time DESC") List getAllRecentSearchQueries(String username); @Query("DELETE FROM recent_search_queries WHERE username = :username") void deleteAllRecentSearchQueries(String username); @Delete void deleteRecentSearchQueries(RecentSearchQuery recentSearchQuery); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/recentsearchquery/RecentSearchQueryRepository.java ================================================ package ml.docilealligator.infinityforreddit.recentsearchquery; import androidx.lifecycle.LiveData; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class RecentSearchQueryRepository { private final LiveData> mAllRecentSearchQueries; private final LiveData> mLimitedRecentSearchQueries; RecentSearchQueryRepository(RedditDataRoomDatabase redditDataRoomDatabase, String username, int limit) { mAllRecentSearchQueries = redditDataRoomDatabase.recentSearchQueryDao().getAllRecentSearchQueriesLiveData(username); mLimitedRecentSearchQueries = redditDataRoomDatabase.recentSearchQueryDao().getRecentSearchQueriesLiveData(username, limit); } LiveData> getAllRecentSearchQueries() { return mAllRecentSearchQueries; } LiveData> getLimitedRecentSearchQueries() { return mLimitedRecentSearchQueries; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/recentsearchquery/RecentSearchQueryViewModel.java ================================================ package ml.docilealligator.infinityforreddit.recentsearchquery; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class RecentSearchQueryViewModel extends ViewModel { public static final int DEFAULT_DISPLAY_LIMIT = 5; private final LiveData> mAllRecentSearchQueries; private final LiveData> mLimitedRecentSearchQueries; public RecentSearchQueryViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String username, int limit) { RecentSearchQueryRepository repository = new RecentSearchQueryRepository(redditDataRoomDatabase, username, limit); mAllRecentSearchQueries = repository.getAllRecentSearchQueries(); mLimitedRecentSearchQueries = repository.getLimitedRecentSearchQueries(); } public LiveData> getAllRecentSearchQueries() { return mAllRecentSearchQueries; } public LiveData> getLimitedRecentSearchQueries() { return mLimitedRecentSearchQueries; } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mUsername; private final int mLimit; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String username) { this(redditDataRoomDatabase, username, DEFAULT_DISPLAY_LIMIT); } public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String username, int limit) { mRedditDataRoomDatabase = redditDataRoomDatabase; mUsername = username; mLimit = limit; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new RecentSearchQueryViewModel(mRedditDataRoomDatabase, mUsername, mLimit); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/repositories/CommentActivityRepository.kt ================================================ package ml.docilealligator.infinityforreddit.repositories import androidx.lifecycle.LiveData import ml.docilealligator.infinityforreddit.comment.CommentDraft import ml.docilealligator.infinityforreddit.comment.CommentDraftDao import ml.docilealligator.infinityforreddit.comment.DraftType class CommentActivityRepository( private val commentDraftDao: CommentDraftDao ) { fun getCommentDraft(fullname: String): LiveData { return commentDraftDao.getCommentDraftLiveData(fullname, DraftType.REPLY) } suspend fun saveCommentDraft(fullname: String, content: String) { commentDraftDao.insert(CommentDraft(fullname, content, System.currentTimeMillis(), DraftType.REPLY)) } suspend fun deleteCommentDraft(fullname: String) { commentDraftDao.delete(CommentDraft(fullname, "", 0, DraftType.REPLY)) } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/repositories/CopyMultiRedditActivityRepository.kt ================================================ package ml.docilealligator.infinityforreddit.repositories import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ml.docilealligator.infinityforreddit.APIError import ml.docilealligator.infinityforreddit.APIResult import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase import ml.docilealligator.infinityforreddit.account.Account import ml.docilealligator.infinityforreddit.apis.RedditAPIKt import ml.docilealligator.infinityforreddit.multireddit.AnonymousMultiredditSubreddit import ml.docilealligator.infinityforreddit.multireddit.ExpandedSubredditInMultiReddit import ml.docilealligator.infinityforreddit.multireddit.FetchMultiRedditInfo import ml.docilealligator.infinityforreddit.multireddit.MultiReddit import ml.docilealligator.infinityforreddit.utils.APIUtils import ml.docilealligator.infinityforreddit.utils.JSONUtils import org.json.JSONException import org.json.JSONObject import retrofit2.HttpException import retrofit2.Retrofit import java.io.IOException interface CopyMultiRedditActivityRepository { suspend fun fetchMultiRedditInfo(multipath: String): MultiReddit? suspend fun copyMultiReddit(multipath: String, name: String, description: String, subreddits: List): APIResult //suspend fun copyMultiRedditAnonymous(multipath: String, name: String, description: String, subreddits: List): APIResult } class CopyMultiRedditActivityRepositoryImpl( val oauthRetrofit: Retrofit, val redditDataRoomDatabase: RedditDataRoomDatabase, val accessToken: String ): CopyMultiRedditActivityRepository { override suspend fun fetchMultiRedditInfo(multipath: String): MultiReddit? { try { val response = oauthRetrofit.create(RedditAPIKt::class.java) .getMultiRedditInfo(APIUtils.getOAuthHeader(accessToken), multipath) return withContext(Dispatchers.Default) { FetchMultiRedditInfo.parseMultiRedditInfo(response) } } catch (e: IOException) { e.printStackTrace() return null } catch (e: HttpException) { e.printStackTrace() return null } } override suspend fun copyMultiReddit(multipath: String, name: String, description: String, subreddits: List): APIResult { if (accessToken.isEmpty()) { return copyMultiRedditAnonymous(name, description, subreddits) } try { val params = mapOf( APIUtils.FROM_KEY to multipath, APIUtils.DISPLAY_NAME_KEY to name, APIUtils.DESCRIPTION_MD_KEY to description ) val response = oauthRetrofit.create(RedditAPIKt::class.java) .copyMultiReddit(APIUtils.getOAuthHeader(accessToken), params) return withContext(Dispatchers.Default) { APIResult.Success(FetchMultiRedditInfo.parseMultiRedditInfo(response)) } } catch (e: IOException) { e.printStackTrace() return APIResult.Error(APIError.Message(e.localizedMessage ?: "Network error")) } catch (e: HttpException) { e.printStackTrace() try { val errorMessage = JSONObject(e.response()?.errorBody()?.string() ?: "").getString(JSONUtils.EXPLANATION_KEY) return APIResult.Error(APIError.Message(errorMessage)) } catch(ignore: JSONException) { return APIResult.Error(APIError.MessageRes(R.string.copy_multi_reddit_failed)) } } } suspend fun copyMultiRedditAnonymous(name: String, description: String, subreddits: List): APIResult { if (!redditDataRoomDatabase.accountDaoKt().isAnonymousAccountInserted()) { redditDataRoomDatabase.accountDaoKt().insert(Account.getAnonymousAccount()) } if (redditDataRoomDatabase.multiRedditDaoKt().getMultiReddit("/user/-/m/$name", Account.ANONYMOUS_ACCOUNT) != null) { return APIResult.Error(APIError.MessageRes(R.string.duplicate_multi_reddit)) } else { val newMultiReddit = MultiReddit( "/user/-/m/$name", name, name, description, null, null, "private", Account.ANONYMOUS_ACCOUNT, 0, System.currentTimeMillis(), true, false, false ) redditDataRoomDatabase.multiRedditDaoKt().insert(newMultiReddit) val anonymousMultiRedditSubreddits: MutableList = mutableListOf() for (s in subreddits) { anonymousMultiRedditSubreddits.add(AnonymousMultiredditSubreddit("/user/-/m/$name", s.name, s.iconUrl)) } redditDataRoomDatabase.anonymousMultiredditSubredditDaoKt().insertAll(anonymousMultiRedditSubreddits) return APIResult.Success(newMultiReddit) } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/repositories/EditCommentActivityRepository.kt ================================================ package ml.docilealligator.infinityforreddit.repositories import androidx.lifecycle.LiveData import ml.docilealligator.infinityforreddit.comment.CommentDraft import ml.docilealligator.infinityforreddit.comment.CommentDraftDao import ml.docilealligator.infinityforreddit.comment.DraftType class EditCommentActivityRepository( private val commentDraftDao: CommentDraftDao ) { fun getCommentDraft(fullname: String): LiveData { return commentDraftDao.getCommentDraftLiveData(fullname, DraftType.EDIT) } suspend fun saveCommentDraft(fullname: String, content: String) { commentDraftDao.insert(CommentDraft(fullname, content, System.currentTimeMillis(), DraftType.EDIT)) } suspend fun deleteCommentDraft(fullname: String) { commentDraftDao.delete(CommentDraft(fullname, "", 0, DraftType.EDIT)) } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/services/DownloadMediaService.java ================================================ package ml.docilealligator.infinityforreddit.services; import android.app.Notification; import android.app.PendingIntent; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobService; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.media.MediaScannerConnection; import android.net.NetworkRequest; import android.net.Uri; import android.os.Build; import android.os.PersistableBundle; import android.provider.MediaStore; import android.util.Log; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.documentfile.provider.DocumentFile; import org.apache.commons.io.FilenameUtils; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import ml.docilealligator.infinityforreddit.DownloadProgressResponseBody; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.VideoLinkFetcher; import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity; import ml.docilealligator.infinityforreddit.apis.DownloadFile; import ml.docilealligator.infinityforreddit.apis.StreamableAPI; import ml.docilealligator.infinityforreddit.broadcastreceivers.DownloadedMediaDeleteActionBroadcastReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.post.ImgurMedia; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.NotificationUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; import retrofit2.Response; import retrofit2.Retrofit; public class DownloadMediaService extends JobService { public static final String EXTRA_URL = "EU"; public static final String EXTRA_FILE_NAME = "EFN"; public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_TITLE = "ET"; public static final String EXTRA_MEDIA_TYPE = "EIG"; public static final String EXTRA_IS_NSFW = "EIN"; public static final String EXTRA_REDGIFS_ID = "EGI"; public static final String EXTRA_STREAMABLE_SHORT_CODE = "ESSC"; public static final String EXTRA_IS_ALL_GALLERY_MEDIA = "EIAGM"; public static final int EXTRA_MEDIA_TYPE_IMAGE = 0; public static final int EXTRA_MEDIA_TYPE_GIF = 1; public static final int EXTRA_MEDIA_TYPE_VIDEO = 2; public static final String EXTRA_POST_ID = "EPI"; public static final String EXTRA_COMMENT_ID = "ECI"; public static final String EXTRA_ALL_GALLERY_IMAGE_URLS = "EAGIU"; public static final String EXTRA_ALL_GALLERY_IMAGE_MEDIA_TYPES = "EAGIMT"; public static final String EXTRA_ALL_GALLERY_IMAGE_FILE_NAMES = "EAGIFN"; private static final int NO_ERROR = -1; private static final int ERROR_CANNOT_GET_DESTINATION_DIRECTORY = 0; private static final int ERROR_FILE_CANNOT_DOWNLOAD = 1; private static final int ERROR_FILE_CANNOT_SAVE = 2; private static final int ERROR_FILE_CANNOT_FETCH_REDGIFS_VIDEO_LINK = 3; private static final int ERROR_CANNOT_FETCH_STREAMABLE_VIDEO_LINK = 4; private static final int ERROR_INVALID_ARGUMENT = 5; private static int JOB_ID = 20000; @Inject @Named("download_media") Retrofit retrofit; @Inject @Named("redgifs") Retrofit mRedgifsRetrofit; @Inject Provider mStreamableApiProvider; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private NotificationManagerCompat notificationManager; private static String sanitizeFilename(String inputName) { if (inputName == null || inputName.isEmpty()) { return "reddit_media"; // Default name if title is missing } // Remove characters that are invalid in filenames on most systems String sanitized = inputName.replaceAll("[\\\\/:*?\"<>|]", "_"); // Replace multiple spaces/underscores with a single underscore sanitized = sanitized.replaceAll("[\\s_]+", "_"); // Trim leading/trailing underscores sanitized = sanitized.replaceAll("^_+|_+$", ""); // Limit length to avoid issues with max path length int maxLength = 100; // Adjust max length as needed if (sanitized.length() > maxLength) { sanitized = sanitized.substring(0, maxLength); // Ensure we don't cut in the middle of a multi-byte character if needed, // but for simplicity, basic substring is often sufficient. // Re-trim in case the cut resulted in a trailing underscore sanitized = sanitized.replaceAll("_+$", ""); } // Handle case where sanitization results in an empty string if (sanitized.isEmpty()) { return "reddit_media_" + System.currentTimeMillis(); } return sanitized; } // Helper function to get file extension (overload for Post) private static String getExtension(String url, int mediaType, String defaultFileName) { return getExtensionInternal(url, mediaType, defaultFileName); } // Helper function to get file extension (overload for ImgurMedia) private static String getExtension(ImgurMedia imgurMedia) { // ImgurMedia already has a reasonable filename with extension String fileName = imgurMedia.getFileName(); String extension = FilenameUtils.getExtension(fileName); if (extension != null && !extension.isEmpty()) { // Limit extension length return "." + extension.toLowerCase().substring(0, Math.min(extension.length(), 5)); } // Fallback based on type if filename lacks extension return getExtensionInternal(imgurMedia.getLink(), imgurMedia.getType() == ImgurMedia.TYPE_VIDEO ? EXTRA_MEDIA_TYPE_VIDEO : EXTRA_MEDIA_TYPE_IMAGE, null); } // Internal helper for extension logic private static String getExtensionInternal(String url, int mediaType, String defaultFileName) { String extension = FilenameUtils.getExtension(url); if (extension != null && !extension.isEmpty()) { // Basic validation for common image/video extensions if (extension.matches("(?i)(jpg|jpeg|png|gif|mp4|webm|mov|avi)")) { // Limit extension length to prevent abuse return "." + extension.toLowerCase().substring(0, Math.min(extension.length(), 5)); } } // Fallback based on media type or default filename switch (mediaType) { case EXTRA_MEDIA_TYPE_IMAGE: return ".jpg"; case EXTRA_MEDIA_TYPE_GIF: return ".gif"; case EXTRA_MEDIA_TYPE_VIDEO: return ".mp4"; default: // Try extracting from defaultFileName if provided if (defaultFileName != null && defaultFileName.contains(".")) { String defaultExt = FilenameUtils.getExtension(defaultFileName); if (defaultExt != null && !defaultExt.isEmpty()) { return "." + defaultExt.toLowerCase().substring(0, Math.min(defaultExt.length(), 5)); } } return ".unknown"; // Default if no extension found } } public DownloadMediaService() { } /** * * @param context * @param contentEstimatedBytes * @param post * @param galleryIndex if post is not a gallery post, then galleryIndex should be 0 * @return JobInfo for DownloadMediaService */ public static JobInfo constructJobInfo(Context context, long contentEstimatedBytes, Post post, int galleryIndex) { PersistableBundle extras = new PersistableBundle(); String sanitizedTitle = sanitizeFilename(post.getTitle()); if (post.getId() != null && !post.getId().isEmpty()) { sanitizedTitle = sanitizedTitle + "_" + post.getId(); } String url = ""; String extension = ""; int currentMediaType = -1; if (post.getPostType() == Post.IMAGE_TYPE) { url = post.getUrl(); currentMediaType = EXTRA_MEDIA_TYPE_IMAGE; extras.putString(EXTRA_URL, url); extras.putInt(EXTRA_MEDIA_TYPE, currentMediaType); extras.putString(EXTRA_SUBREDDIT_NAME, post.getSubredditName()); extras.putInt(EXTRA_IS_NSFW, post.isNSFW() ? 1 : 0); } else if (post.getPostType() == Post.GIF_TYPE) { url = post.getVideoUrl(); // GIFs often served as videos (mp4) currentMediaType = EXTRA_MEDIA_TYPE_GIF; // Keep original type for logic, but extension might be mp4 extras.putString(EXTRA_URL, url); extras.putInt(EXTRA_MEDIA_TYPE, currentMediaType); extras.putString(EXTRA_SUBREDDIT_NAME, post.getSubredditName()); extras.putInt(EXTRA_IS_NSFW, post.isNSFW() ? 1 : 0); } else if (post.getPostType() == Post.VIDEO_TYPE) { currentMediaType = EXTRA_MEDIA_TYPE_VIDEO; if (post.isStreamable()) { if (post.isLoadedStreamableVideoAlready()) { extras.putString(EXTRA_URL, post.getVideoUrl()); } else { extras.putString(EXTRA_STREAMABLE_SHORT_CODE, post.getStreamableShortCode()); } extras.putString(EXTRA_FILE_NAME, "Streamable-" + post.getStreamableShortCode() + ".mp4"); } else if (post.isRedgifs()) { extras.putString(EXTRA_URL, post.getVideoUrl()); extras.putString(EXTRA_REDGIFS_ID, post.getRedgifsId()); String redgifsId = post.getRedgifsId(); if (redgifsId != null && redgifsId.contains("-")) { redgifsId = redgifsId.substring(0, redgifsId.indexOf('-')); } } else if (post.isImgur()) { url = post.getVideoUrl(); extras.putString(EXTRA_URL, url); } else { // Standard Reddit video url = post.getVideoUrl(); extras.putString(EXTRA_URL, url); } extras.putInt(EXTRA_MEDIA_TYPE, currentMediaType); extras.putString(EXTRA_SUBREDDIT_NAME, post.getSubredditName()); extras.putInt(EXTRA_IS_NSFW, post.isNSFW() ? 1 : 0); } else if (post.getPostType() == Post.GALLERY_TYPE) { Post.Gallery media = post.getGallery().get(galleryIndex); Log.d("GalleryDownload", "DownloadMediaService.constructJobInfo(Gallery): media.mediaType=" + media.mediaType + ", post.isNSFW()=" + post.isNSFW()); if (media.mediaType == Post.Gallery.TYPE_VIDEO) { url = media.url; currentMediaType = EXTRA_MEDIA_TYPE_VIDEO; extras.putString(EXTRA_URL, url); extras.putInt(EXTRA_MEDIA_TYPE, currentMediaType); } else { url = media.hasFallback() ? media.fallbackUrl : media.url; // Retrieve original instead of the one additionally compressed by reddit currentMediaType = media.mediaType == Post.Gallery.TYPE_GIF ? EXTRA_MEDIA_TYPE_GIF : EXTRA_MEDIA_TYPE_IMAGE; extras.putString(EXTRA_URL, url); extras.putInt(EXTRA_MEDIA_TYPE, currentMediaType); } extras.putString(EXTRA_SUBREDDIT_NAME, post.getSubredditName()); extras.putInt(EXTRA_IS_NSFW, post.isNSFW() ? 1 : 0); } // Determine extension based on URL and media type extension = getExtension(url, currentMediaType, null); // Pass null for defaultFileName initially // Construct filename: title + (optional index for gallery) + extension String finalFileName = sanitizedTitle + (post.getPostType() == Post.GALLERY_TYPE && galleryIndex >= 0 ? "_" + (galleryIndex + 1) : "") + // Use 1-based index for galleries extension; // Set the final filename in extras extras.putString(EXTRA_FILE_NAME, finalFileName); // Re-fetch extension if it was based on a potentially incorrect default, now using the final name if (url == null || url.isEmpty()) { // Especially for Redgifs/Streamable where URL might be fetched later extension = getExtension(url, currentMediaType, finalFileName); finalFileName = sanitizedTitle + (post.getPostType() == Post.GALLERY_TYPE && galleryIndex >= 0 ? "_" + (galleryIndex + 1) : "") + extension; extras.putString(EXTRA_FILE_NAME, finalFileName); // Update again if extension changed } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } public static JobInfo constructGalleryDownloadAllMediaJobInfo(Context context, long contentEstimatedBytes, Post post) { PersistableBundle extras = new PersistableBundle(); if (post.getPostType() == Post.GALLERY_TYPE) { extras.putString(EXTRA_SUBREDDIT_NAME, post.getSubredditName()); extras.putInt(EXTRA_IS_NSFW, post.isNSFW() ? 1 : 0); ArrayList gallery = post.getGallery(); StringBuilder concatUrlsBuilder = new StringBuilder(); StringBuilder concatMediaTypesBuilder = new StringBuilder(); StringBuilder concatFileNamesBuilder = new StringBuilder(); for (int i = 0; i < gallery.size(); i++) { Post.Gallery media = gallery.get(i); String url = ""; int currentMediaType = -1; String sanitizedTitle = sanitizeFilename(post.getTitle()); // Sanitize title once if (post.getId() != null && !post.getId().isEmpty()) { sanitizedTitle = sanitizedTitle + "_" + post.getId(); } if (media.mediaType == Post.Gallery.TYPE_VIDEO) { url = media.url; currentMediaType = EXTRA_MEDIA_TYPE_VIDEO; concatUrlsBuilder.append(url).append(" "); concatMediaTypesBuilder.append(currentMediaType).append(" "); } else { url = media.hasFallback() ? media.fallbackUrl : media.url; // Retrieve original currentMediaType = media.mediaType == Post.Gallery.TYPE_GIF ? EXTRA_MEDIA_TYPE_GIF : EXTRA_MEDIA_TYPE_IMAGE; concatUrlsBuilder.append(url).append(" "); concatMediaTypesBuilder.append(currentMediaType).append(" "); } // Construct filename for this gallery item String extension = getExtension(url, currentMediaType, media.fileName); // Use original media.fileName as fallback hint String finalFileName = sanitizedTitle + "_" + (i + 1) + extension; // Use 1-based index concatFileNamesBuilder.append(finalFileName).append(" "); } if (concatUrlsBuilder.length() > 0) { concatUrlsBuilder.deleteCharAt(concatUrlsBuilder.length() - 1); } if (concatMediaTypesBuilder.length() > 0) { concatMediaTypesBuilder.deleteCharAt(concatMediaTypesBuilder.length() - 1); } if (concatFileNamesBuilder.length() > 0) { concatFileNamesBuilder.deleteCharAt(concatFileNamesBuilder.length() - 1); } extras.putString(EXTRA_ALL_GALLERY_IMAGE_URLS, concatUrlsBuilder.toString()); extras.putString(EXTRA_ALL_GALLERY_IMAGE_MEDIA_TYPES, concatMediaTypesBuilder.toString()); extras.putString(EXTRA_ALL_GALLERY_IMAGE_FILE_NAMES, concatFileNamesBuilder.toString()); extras.putInt(EXTRA_IS_ALL_GALLERY_MEDIA, 1); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } public static JobInfo constructJobInfo(Context context, long contentEstimatedBytes, ImgurMedia imgurMedia, String subredditName, boolean isNsfw, String title) { PersistableBundle extras = new PersistableBundle(); extras.putString(EXTRA_URL, imgurMedia.getLink()); if (title == null || title.trim().isEmpty()) { title = imgurMedia.getId(); // Fallback to ID if title is missing } String sanitizedTitle = sanitizeFilename(title); // Use static sanitize helper String extension = getExtension(imgurMedia); // Use static ImgurMedia extension helper String finalFileName = sanitizedTitle + extension; extras.putString(EXTRA_FILE_NAME, finalFileName); // Set the constructed filename if (imgurMedia.getType() == ImgurMedia.TYPE_VIDEO) { extras.putInt(EXTRA_MEDIA_TYPE, EXTRA_MEDIA_TYPE_VIDEO); } else { extras.putInt(EXTRA_MEDIA_TYPE, EXTRA_MEDIA_TYPE_IMAGE); } // Pass the received subreddit, NSFW status, and title to the extras extras.putString(EXTRA_SUBREDDIT_NAME, subredditName); extras.putBoolean(EXTRA_IS_NSFW, isNsfw); extras.putString(EXTRA_TITLE, title); // Add title as well for consistency if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } public static JobInfo constructImgurAlbumDownloadAllMediaJobInfo(Context context, long contentEstimatedBytes, List imgurMedia, String subredditName, boolean isNsfw, String title) { PersistableBundle extras = new PersistableBundle(); Log.d("ImgurDownload", "Creating job for Imgur album with " + imgurMedia.size() + " items, isNsfw=" + isNsfw); StringBuilder concatUrlsBuilder = new StringBuilder(); StringBuilder concatMediaTypesBuilder = new StringBuilder(); StringBuilder concatFileNamesBuilder = new StringBuilder(); for (int i = 0; i < imgurMedia.size(); i++) { ImgurMedia media = imgurMedia.get(i); String url = media.getLink(); int currentMediaType; if (media.getType() == ImgurMedia.TYPE_VIDEO) { currentMediaType = EXTRA_MEDIA_TYPE_VIDEO; concatUrlsBuilder.append(url).append(" "); concatMediaTypesBuilder.append(currentMediaType).append(" "); Log.d("ImgurDownload", "Item " + i + ": Video - " + url); } else { currentMediaType = EXTRA_MEDIA_TYPE_IMAGE; concatUrlsBuilder.append(url).append(" "); concatMediaTypesBuilder.append(currentMediaType).append(" "); Log.d("ImgurDownload", "Item " + i + ": Image - " + url); } if (title == null || title.trim().isEmpty()) { title = media.getId(); // Fallback to ID } String sanitizedTitle = sanitizeFilename(title); String extension = getExtension(media); String finalFileName = sanitizedTitle + "_" + (i + 1) + extension; // Add 1-based index concatFileNamesBuilder.append(finalFileName).append(" "); } if (concatUrlsBuilder.length() > 0) { concatUrlsBuilder.deleteCharAt(concatUrlsBuilder.length() - 1); } if (concatMediaTypesBuilder.length() > 0) { concatMediaTypesBuilder.deleteCharAt(concatMediaTypesBuilder.length() - 1); } if (concatFileNamesBuilder.length() > 0) { concatFileNamesBuilder.deleteCharAt(concatFileNamesBuilder.length() - 1); } extras.putString(EXTRA_ALL_GALLERY_IMAGE_URLS, concatUrlsBuilder.toString()); extras.putString(EXTRA_ALL_GALLERY_IMAGE_MEDIA_TYPES, concatMediaTypesBuilder.toString()); extras.putString(EXTRA_ALL_GALLERY_IMAGE_FILE_NAMES, concatFileNamesBuilder.toString()); extras.putString(EXTRA_SUBREDDIT_NAME, subredditName); extras.putBoolean(EXTRA_IS_NSFW, isNsfw); extras.putString(EXTRA_TITLE, title); extras.putInt(EXTRA_MEDIA_TYPE, EXTRA_MEDIA_TYPE_IMAGE); Log.d("ImgurDownload", "Bundle created with media types: " + concatMediaTypesBuilder.toString()); Log.d("ImgurDownload", "Overall media type set to: " + EXTRA_MEDIA_TYPE_IMAGE + " (IMAGE)"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } public static JobInfo constructJobInfo(Context context, long contentEstimatedBytes, PersistableBundle extras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadMediaService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } @Override public void onCreate() { ((Infinity) getApplication()).getAppComponent().inject(this); notificationManager = NotificationManagerCompat.from(this); } @Override public boolean onStartJob(JobParameters params) { PersistableBundle extras = params.getExtras(); int mediaType = extras.getInt(EXTRA_MEDIA_TYPE, EXTRA_MEDIA_TYPE_IMAGE); Log.d("ImgurDownload", "onStartJob - overall mediaType: " + mediaType); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getNotificationChannelId(mediaType)); NotificationChannelCompat serviceChannel = new NotificationChannelCompat.Builder( getNotificationChannelId(mediaType), NotificationManagerCompat.IMPORTANCE_LOW) .setName(getNotificationChannel(mediaType)) .build(); notificationManager.createNotificationChannel(serviceChannel); int randomNotificationIdOffset = new Random().nextInt(10000); String notificationTitle = extras.containsKey(EXTRA_FILE_NAME) ? extras.getString(EXTRA_FILE_NAME) : (extras.getInt(EXTRA_IS_ALL_GALLERY_MEDIA, 0) == 1 ? getString(R.string.download_all_gallery_media_notification_title) : getString(R.string.download_all_imgur_album_media_notification_title)); switch (extras.getInt(EXTRA_MEDIA_TYPE, EXTRA_MEDIA_TYPE_IMAGE)) { case EXTRA_MEDIA_TYPE_GIF: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.DOWNLOAD_GIF_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle), JobService.JOB_END_NOTIFICATION_POLICY_DETACH); } else { notificationManager.notify(NotificationUtils.DOWNLOAD_GIF_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle)); } break; case EXTRA_MEDIA_TYPE_VIDEO: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle), JobService.JOB_END_NOTIFICATION_POLICY_DETACH); } else { notificationManager.notify(NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle)); } break; default: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.DOWNLOAD_IMAGE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle), JobService.JOB_END_NOTIFICATION_POLICY_DETACH); } else { notificationManager.notify(NotificationUtils.DOWNLOAD_IMAGE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle)); } } mExecutor.execute(() -> { Log.d("GalleryDownload", "DownloadMediaService.onStartJob(): Job started. Extras: " + extras.toString()); String subredditName = extras.getString(EXTRA_SUBREDDIT_NAME); // Remove the direct getBoolean call: // boolean isNsfw = extras.getBoolean(EXTRA_IS_NSFW, false); // Explicitly get the object and check its type: Object nsfwValue = extras.get(EXTRA_IS_NSFW); boolean isNsfw = false; // Default value if (nsfwValue instanceof Boolean) { isNsfw = (Boolean) nsfwValue; } else if (nsfwValue instanceof Integer) { // Correctly handle the Integer case based on the value (1 for true, 0 for false) isNsfw = ((Integer) nsfwValue) != 0; // Optional: Log a warning if you still want to know this happened, but it's handled now. Log.d("ImgurDownload", "EXTRA_IS_NSFW was Integer: " + nsfwValue); } else if (nsfwValue != null) { // Optional: Handle unexpected types if necessary Log.d("ImgurDownload", "Unexpected type for EXTRA_IS_NSFW: " + nsfwValue.getClass().getName()); } Log.d("ImgurDownload", "Processing download with isNsfw=" + isNsfw); if (extras.containsKey(EXTRA_ALL_GALLERY_IMAGE_URLS)) { // Download all images in a gallery post String concatUrls = extras.getString(EXTRA_ALL_GALLERY_IMAGE_URLS); String concatMediaTypes = extras.getString(EXTRA_ALL_GALLERY_IMAGE_MEDIA_TYPES); String concatFileNames = extras.getString(EXTRA_ALL_GALLERY_IMAGE_FILE_NAMES); Log.d("ImgurDownload", "Processing album/gallery with media types: " + concatMediaTypes); String[] urls = concatUrls.split(" "); String[] mediaTypes = concatMediaTypes.split(" "); String[] fileNames = concatFileNames.split(" "); Log.d("ImgurDownload", "Split into " + urls.length + " items to download"); boolean allImagesDownloadedSuccessfully = true; for (int i = 0; i < urls.length; i++) { String mimeType = Integer.parseInt(mediaTypes[i]) == EXTRA_MEDIA_TYPE_VIDEO ? "video/*" : "image/*"; int finalI = i; int individualMediaType = Integer.parseInt(mediaTypes[i]); Log.d("ImgurDownload", "Processing item " + i + ": mediaType=" + individualMediaType + " (" + (individualMediaType == EXTRA_MEDIA_TYPE_VIDEO ? "VIDEO" : individualMediaType == EXTRA_MEDIA_TYPE_GIF ? "GIF" : "IMAGE") + ")"); // Create a new PersistableBundle for each individual download to ensure correct mediaType PersistableBundle itemExtras = new PersistableBundle(extras); // Set the media type for this specific item itemExtras.putInt(EXTRA_MEDIA_TYPE, individualMediaType); allImagesDownloadedSuccessfully &= downloadMedia(params, urls[i], itemExtras, builder, individualMediaType, randomNotificationIdOffset, fileNames[i], mimeType, subredditName, isNsfw, true, new DownloadProgressResponseBody.ProgressListener() { long time = 0; @Override public void update(long bytesRead, long contentLength, boolean done) { if (!done) { if (contentLength != -1) { long currentTime = System.currentTimeMillis(); if (currentTime - time > 1000) { time = currentTime; int currentMediaProgress = (int) (((float) bytesRead / contentLength + (float) finalI / urls.length) * 100); updateNotification(builder, individualMediaType, 0, currentMediaProgress, randomNotificationIdOffset, null, null); } } } } }); } updateNotification(builder, mediaType, allImagesDownloadedSuccessfully ? R.string.downloading_media_finished : R.string.download_gallery_failed_some_images, -1, randomNotificationIdOffset, null, null); jobFinished(params, false); } else { String fileUrl = extras.getString(EXTRA_URL); String fileName = extras.getString(EXTRA_FILE_NAME); String mimeType = mediaType == EXTRA_MEDIA_TYPE_VIDEO ? "video/*" : "image/*"; Log.d("ImgurDownload", "Processing single download: mediaType=" + mediaType + " (" + (mediaType == EXTRA_MEDIA_TYPE_VIDEO ? "VIDEO" : mediaType == EXTRA_MEDIA_TYPE_GIF ? "GIF" : "IMAGE") + ")"); downloadMedia(params, fileUrl, extras, builder, mediaType, randomNotificationIdOffset, fileName, mimeType, subredditName, isNsfw, false, new DownloadProgressResponseBody.ProgressListener() { long time = 0; @Override public void update(long bytesRead, long contentLength, boolean done) { if (!done) { if (contentLength != -1) { long currentTime = System.currentTimeMillis(); if (currentTime - time > 1000) { time = currentTime; updateNotification(builder, mediaType, 0, (int) ((100 * bytesRead) / contentLength), randomNotificationIdOffset, null, null); } } } } }); } }); return true; } @Override public boolean onStopJob(JobParameters params) { return false; } /** * * @param params * @param fileUrl * @param intent * @param builder * @param mediaType * @param randomNotificationIdOffset * @param fileName * @param mimeType * @param subredditName * @param isNsfw * @param multipleDownloads * @param progressListener * @return true if download succeeded or false otherwise. */ private boolean downloadMedia(JobParameters params, String fileUrl, PersistableBundle intent, NotificationCompat.Builder builder, int mediaType, int randomNotificationIdOffset, String fileName, String mimeType, String subredditName, boolean isNsfw, boolean multipleDownloads, DownloadProgressResponseBody.ProgressListener progressListener) { Log.d("GalleryDownload", "DownloadMediaService.downloadMedia(): Starting download. " + "mediaType=" + mediaType + ", isNsfw=" + isNsfw + ", fileName=" + fileName + ", fileUrl=" + (fileUrl == null ? "NULL (will fetch)" : fileUrl) + ", multipleDownloads=" + multipleDownloads); Log.d("ImgurDownload", "downloadMedia - mediaType=" + mediaType + " (" + (mediaType == EXTRA_MEDIA_TYPE_VIDEO ? "VIDEO" : mediaType == EXTRA_MEDIA_TYPE_GIF ? "GIF" : "IMAGE") + ")" + ", fileName=" + fileName + ", isNsfw=" + isNsfw); if (fileUrl == null) { // Only Redgifs and Streamble video can go inside this if clause. String redgifsId = intent.getString(EXTRA_REDGIFS_ID, null); String streamableShortCode = intent.getString(EXTRA_STREAMABLE_SHORT_CODE, null); if (redgifsId == null && streamableShortCode == null) { downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_INVALID_ARGUMENT, multipleDownloads); return false; } fileUrl = VideoLinkFetcher.fetchVideoLinkSync(mRedgifsRetrofit, mStreamableApiProvider, mCurrentAccountSharedPreferences, redgifsId == null ? ViewVideoActivity.VIDEO_TYPE_STREAMABLE : ViewVideoActivity.VIDEO_TYPE_REDGIFS, redgifsId, streamableShortCode); if (fileUrl == null) { downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, redgifsId == null ? ERROR_CANNOT_FETCH_STREAMABLE_VIDEO_LINK : ERROR_FILE_CANNOT_FETCH_REDGIFS_VIDEO_LINK, multipleDownloads); return false; } } OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(chain -> { okhttp3.Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() .body(new DownloadProgressResponseBody(originalResponse.body(), progressListener)) .build(); }) .addInterceptor(chain -> chain.proceed( chain.request() .newBuilder() .header("User-Agent", APIUtils.USER_AGENT) .build() )) .build(); retrofit = retrofit.newBuilder().client(client).build(); boolean separateDownloadFolder = mSharedPreferences.getBoolean(SharedPreferencesUtils.SEPARATE_FOLDER_FOR_EACH_SUBREDDIT, false); Response response; String destinationFileUriString = null; boolean isDefaultDestination = true; try { response = retrofit.create(DownloadFile.class).downloadFile(fileUrl).execute(); if (response.isSuccessful() && response.body() != null) { String destinationFileDirectory = getDownloadLocation(mediaType, isNsfw); Log.d("ImgurDownload", "Got download location: " + destinationFileDirectory + " for mediaType=" + mediaType + ", isNsfw=" + isNsfw); if (destinationFileDirectory == null || destinationFileDirectory.isEmpty()) { Log.e("ImgurDownload", "Download location is empty! mediaType=" + mediaType + ", isNsfw=" + isNsfw); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, multipleDownloads); return false; } isDefaultDestination = false; DocumentFile picFile; DocumentFile dir; if (separateDownloadFolder && subredditName != null && !subredditName.equals("")) { dir = DocumentFile.fromTreeUri(DownloadMediaService.this, Uri.parse(destinationFileDirectory)); if (dir == null) { Log.e("ImgurDownload", "Could not get tree URI from destination directory"); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, multipleDownloads); return false; } dir = dir.findFile(subredditName); if (dir == null) { dir = DocumentFile.fromTreeUri(DownloadMediaService.this, Uri.parse(destinationFileDirectory)).createDirectory(subredditName); if (dir == null) { Log.e("ImgurDownload", "Could not create subreddit directory"); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, multipleDownloads); return false; } } } else { dir = DocumentFile.fromTreeUri(DownloadMediaService.this, Uri.parse(destinationFileDirectory)); if (dir == null) { Log.e("ImgurDownload", "Could not get tree URI from destination directory (no subreddit)"); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, multipleDownloads); return false; } } int dotIndex = fileName.lastIndexOf('.'); final String baseName = (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); final String extension = (dotIndex == -1) ? "" : fileName.substring(dotIndex); DocumentFile[] files = dir.listFiles(); HashSet existingFileNames = new HashSet<>(); if (files != null) { for (DocumentFile file : files) { if (file.getName() != null) { existingFileNames.add(file.getName().toLowerCase()); } } } if (existingFileNames.contains(fileName.toLowerCase())) { int num = 1; String newFileName; do { newFileName = baseName + " (" + num + ")" + extension; num++; } while (existingFileNames.contains(newFileName.toLowerCase())); fileName = newFileName; } picFile = dir.createFile(mimeType, fileName); if (picFile == null) { Log.e("ImgurDownload", "Could not create file: " + fileName); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, multipleDownloads); return false; } destinationFileUriString = picFile.getUri().toString(); Log.d("ImgurDownload", "File created successfully at: " + destinationFileUriString); } else { Log.e("ImgurDownload", "Download response not successful: " + response.code()); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_FILE_CANNOT_DOWNLOAD, multipleDownloads); return false; } } catch (IOException e) { e.printStackTrace(); Log.e("ImgurDownload", "IOException during download: " + e.getMessage()); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_FILE_CANNOT_DOWNLOAD, multipleDownloads); return false; } try { Uri destinationFileUri = writeResponseBodyToDisk(response.body(), isDefaultDestination, destinationFileUriString, fileName, mediaType); Log.d("ImgurDownload", "File written successfully"); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, destinationFileUri, NO_ERROR, multipleDownloads); return true; } catch (IOException e) { e.printStackTrace(); Log.e("ImgurDownload", "IOException writing to disk: " + e.getMessage()); downloadFinished(params, builder, mediaType, randomNotificationIdOffset, mimeType, null, ERROR_FILE_CANNOT_SAVE, multipleDownloads); return false; } } private Notification createNotification(NotificationCompat.Builder builder, String fileName) { builder.setContentTitle(fileName).setContentText(getString(R.string.downloading)).setProgress(100, 0, false); return builder.setSmallIcon(R.drawable.ic_notification) .setColor(mCustomThemeWrapper.getColorPrimaryLightTheme()) .build(); } private void updateNotification(NotificationCompat.Builder builder, int mediaType, int contentStringResId, int progress, int randomNotificationIdOffset, Uri mediaUri, String mimeType) { if (notificationManager != null) { if (progress < 0) { builder.setProgress(0, 0, false); } else { builder.setProgress(100, progress, false); } if (contentStringResId != 0) { builder.setContentText(getString(contentStringResId)); builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(contentStringResId))); } if (mediaUri != null) { int pendingIntentFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_CANCEL_CURRENT; Intent intent = new Intent(); intent.setAction(android.content.Intent.ACTION_VIEW); intent.setDataAndType(mediaUri, mimeType); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); PendingIntent pendingIntent = PendingIntent.getActivity(DownloadMediaService.this, 0, intent, pendingIntentFlags); builder.setContentIntent(pendingIntent); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, mediaUri); shareIntent.setType(mimeType); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent intentAction = Intent.createChooser(shareIntent, getString(R.string.share)); PendingIntent shareActionPendingIntent = PendingIntent.getActivity(this, 1, intentAction, pendingIntentFlags); builder.addAction(new NotificationCompat.Action(R.drawable.ic_notification, getString(R.string.share), shareActionPendingIntent)); Intent deleteIntent = new Intent(this, DownloadedMediaDeleteActionBroadcastReceiver.class); deleteIntent.setData(mediaUri); deleteIntent.putExtra(DownloadedMediaDeleteActionBroadcastReceiver.EXTRA_NOTIFICATION_ID, getNotificationId(mediaType, randomNotificationIdOffset)); PendingIntent deleteActionPendingIntent = PendingIntent.getBroadcast(this, 2, deleteIntent, pendingIntentFlags); builder.addAction(new NotificationCompat.Action(R.drawable.ic_notification, getString(R.string.delete), deleteActionPendingIntent)); } notificationManager.notify(getNotificationId(mediaType, randomNotificationIdOffset), builder.build()); } } private String getNotificationChannelId(int mediaType) { switch (mediaType) { case EXTRA_MEDIA_TYPE_GIF: return NotificationUtils.CHANNEL_ID_DOWNLOAD_GIF; case EXTRA_MEDIA_TYPE_VIDEO: return NotificationUtils.CHANNEL_ID_DOWNLOAD_VIDEO; default: return NotificationUtils.CHANNEL_ID_DOWNLOAD_IMAGE; } } private String getNotificationChannel(int mediaType) { switch (mediaType) { case EXTRA_MEDIA_TYPE_GIF: return NotificationUtils.CHANNEL_DOWNLOAD_GIF; case EXTRA_MEDIA_TYPE_VIDEO: return NotificationUtils.CHANNEL_DOWNLOAD_VIDEO; default: return NotificationUtils.CHANNEL_DOWNLOAD_IMAGE; } } private int getNotificationId(int mediaType, int randomNotificationIdOffset) { switch (mediaType) { case EXTRA_MEDIA_TYPE_GIF: return NotificationUtils.DOWNLOAD_GIF_NOTIFICATION_ID + randomNotificationIdOffset; case EXTRA_MEDIA_TYPE_VIDEO: return NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID + randomNotificationIdOffset; default: return NotificationUtils.DOWNLOAD_IMAGE_NOTIFICATION_ID + randomNotificationIdOffset; } } private String getDownloadLocation(int mediaType, boolean isNsfw) { String defaultSharedPrefsFile = "ml.docilealligator.infinityforreddit_preferences"; // Additional diagnostics String nsfwLoc = mSharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); String imgLoc = mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); String gifLoc = mSharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); String vidLoc = mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); Log.d("GalleryDownload", "DownloadMediaService.getDownloadLocation(): Checking locations for mediaType=" + mediaType + ", isNsfw=" + isNsfw); Log.d("ImgurDownload", "DownloadMediaService getDownloadLocation - mediaType=" + mediaType + ", isNsfw=" + isNsfw + ", prefs contain - IMAGE: " + (imgLoc.isEmpty() ? "EMPTY" : "SET") + ", GIF: " + (gifLoc.isEmpty() ? "EMPTY" : "SET") + ", VIDEO: " + (vidLoc.isEmpty() ? "EMPTY" : "SET") + ", NSFW: " + (nsfwLoc.isEmpty() ? "EMPTY" : "SET")); // Try alternate SharedPreferences file if image location is empty if (imgLoc.isEmpty()) { imgLoc = getApplicationContext().getSharedPreferences(defaultSharedPrefsFile, MODE_PRIVATE) .getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); Log.d("ImgurDownload", "Tried alternate SharedPreferences, IMAGE: " + (imgLoc.isEmpty() ? "EMPTY" : "SET")); } if (isNsfw && mSharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { // If NSFW location is set, return it. Otherwise, return empty string to indicate not set. if (!nsfwLoc.isEmpty()) { Log.d("ImgurDownload", "Using NSFW location: " + nsfwLoc); return nsfwLoc; } else { Log.d("GalleryDownload", "NSFW location requested but not set, returning empty."); return ""; // Explicitly return empty if NSFW location is not set } } // If not using separate NSFW folder, proceed with type-specific locations String finalLocation; switch (mediaType) { case EXTRA_MEDIA_TYPE_GIF: finalLocation = gifLoc.isEmpty() ? imgLoc : gifLoc; // Fallback to image location if GIF is not set break; case EXTRA_MEDIA_TYPE_VIDEO: finalLocation = vidLoc.isEmpty() ? imgLoc : vidLoc; // Fallback to image location if VIDEO is not set break; default: // EXTRA_MEDIA_TYPE_IMAGE finalLocation = imgLoc; break; } Log.d("GalleryDownload", "DownloadMediaService.getDownloadLocation(): Returning final location: " + (finalLocation == null || finalLocation.isEmpty() ? "EMPTY" : finalLocation)); return finalLocation; } private Uri writeResponseBodyToDisk(ResponseBody body, boolean isDefaultDestination, String destinationFileUriString, String destinationFileName, int mediaType) throws IOException { ContentResolver contentResolver = getContentResolver(); if (isDefaultDestination) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { InputStream inputStream = body.byteStream(); OutputStream outputStream = new FileOutputStream(destinationFileUriString); byte[] fileReader = new byte[4096]; long fileSize = body.contentLength(); long fileSizeDownloaded = 0; while (true) { int read = inputStream.read(fileReader); if (read == -1) { break; } outputStream.write(fileReader, 0, read); fileSizeDownloaded += read; } outputStream.flush(); } else { ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, destinationFileName); String mimeType; switch (mediaType) { case EXTRA_MEDIA_TYPE_VIDEO: mimeType = "video/mpeg"; break; case EXTRA_MEDIA_TYPE_GIF: mimeType = "image/gif"; break; default: mimeType = "image/jpeg"; } contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, destinationFileUriString); contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1); final Uri contentUri = mediaType == EXTRA_MEDIA_TYPE_VIDEO ? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) : MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); Uri uri = contentResolver.insert(contentUri, contentValues); if (uri == null) { throw new IOException("Failed to create new MediaStore record."); } OutputStream stream = contentResolver.openOutputStream(uri); if (stream == null) { throw new IOException("Failed to get output stream."); } InputStream in = body.byteStream(); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { stream.write(buf, 0, len); } contentValues.clear(); contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0); contentResolver.update(uri, contentValues, null, null); destinationFileUriString = uri.toString(); } } else { try (OutputStream stream = contentResolver.openOutputStream(Uri.parse(destinationFileUriString))) { if (stream == null) { throw new IOException("Failed to get output stream."); } InputStream in = body.byteStream(); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { stream.write(buf, 0, len); } } } return Uri.parse(destinationFileUriString); } private void downloadFinished(JobParameters parameters, NotificationCompat.Builder builder, int mediaType, int randomNotificationIdOffset, String mimeType, Uri destinationFileUri, int errorCode, boolean multipleDownloads) { if (errorCode != NO_ERROR) { if (!multipleDownloads) { switch (errorCode) { case ERROR_CANNOT_GET_DESTINATION_DIRECTORY: updateNotification(builder, mediaType, R.string.downloading_image_or_gif_failed_cannot_get_destination_directory, -1, randomNotificationIdOffset, null, null); break; case ERROR_FILE_CANNOT_DOWNLOAD: updateNotification(builder, mediaType, R.string.downloading_media_failed_cannot_download_media, -1, randomNotificationIdOffset, null, null); break; case ERROR_FILE_CANNOT_SAVE: updateNotification(builder, mediaType, R.string.downloading_media_failed_cannot_save_to_destination_directory, -1, randomNotificationIdOffset, null, null); break; case ERROR_FILE_CANNOT_FETCH_REDGIFS_VIDEO_LINK: updateNotification(builder, mediaType, R.string.download_media_failed_cannot_fetch_redgifs_url, -1, randomNotificationIdOffset, null, null); break; case ERROR_CANNOT_FETCH_STREAMABLE_VIDEO_LINK: updateNotification(builder, mediaType, R.string.download_media_failed_cannot_fetch_streamable_url, -1, randomNotificationIdOffset, null, null); break; case ERROR_INVALID_ARGUMENT: updateNotification(builder, mediaType, R.string.download_media_failed_invalid_argument, -1, randomNotificationIdOffset, null, null); break; } } } else { MediaScannerConnection.scanFile( DownloadMediaService.this, new String[]{destinationFileUri.toString()}, null, (path, uri) -> { if (!multipleDownloads) { updateNotification(builder, mediaType, R.string.downloading_media_finished, -1, randomNotificationIdOffset, destinationFileUri, mimeType); } } ); } if (!multipleDownloads) { jobFinished(parameters, false); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/services/DownloadRedditVideoService.java ================================================ package ml.docilealligator.infinityforreddit.services; import android.app.Notification; import android.app.PendingIntent; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobService; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.media.MediaCodec; import android.media.MediaExtractor; import android.media.MediaFormat; import android.media.MediaMuxer; import android.media.MediaScannerConnection; import android.net.NetworkRequest; import android.net.Uri; import android.os.Build; import android.os.PersistableBundle; import android.provider.MediaStore; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.documentfile.provider.DocumentFile; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Random; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.DownloadProgressResponseBody; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.apis.DownloadFile; import ml.docilealligator.infinityforreddit.broadcastreceivers.DownloadedMediaDeleteActionBroadcastReceiver; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.utils.NotificationUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; import retrofit2.Response; import retrofit2.Retrofit; public class DownloadRedditVideoService extends JobService { public static final String EXTRA_VIDEO_URL = "EVU"; public static final String EXTRA_SUBREDDIT = "ES"; public static final String EXTRA_POST_ID = "EPI"; public static final String EXTRA_FILE_NAME = "EFN"; public static final String EXTRA_IS_NSFW = "EIN"; private static final int NO_ERROR = -1; private static final int ERROR_CANNOT_GET_CACHE_DIRECTORY = 0; private static final int ERROR_VIDEO_FILE_CANNOT_DOWNLOAD = 1; private static final int ERROR_VIDEO_FILE_CANNOT_SAVE = 2; private static final int ERROR_AUDIO_FILE_CANNOT_SAVE = 3; private static final int ERROR_MUX_FAILED = 4; private static final int ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE = 5; private static final int ERROR_CANNOT_GET_DESTINATION_DIRECTORY = 6; private static int JOB_ID = 30000; @Inject @Named("download_media") Retrofit retrofit; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor executor; private NotificationManagerCompat notificationManager; private final String[] possibleAudioUrlSuffices = new String[]{"/CMAF_AUDIO_128.mp4", "/CMAF_AUDIO_64.mp4", "/DASH_AUDIO_128.mp4", "/DASH_audio.mp4", "/DASH_audio", "/audio.mp4", "/audio"}; public DownloadRedditVideoService() { } public static JobInfo constructJobInfo(Context context, long contentEstimatedBytes, PersistableBundle extras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadRedditVideoService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, DownloadRedditVideoService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } @Override public void onCreate() { ((Infinity) getApplication()).getAppComponent().inject(this); notificationManager = NotificationManagerCompat.from(this); } @Override public boolean onStartJob(JobParameters params) { NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadRedditVideoService.this, NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO); PersistableBundle intent = params.getExtras(); String subredditName = intent.getString(EXTRA_SUBREDDIT); String postId = intent.getString(EXTRA_POST_ID); String finalFileName = intent.getString(EXTRA_FILE_NAME); // Use the passed filename for notifications, fallback if missing String notificationTitle = (finalFileName != null && !finalFileName.isEmpty()) ? finalFileName : "reddit_video.mp4"; // Extract base name for cache files if needed, fallback to postId String cacheBaseName = (finalFileName != null && finalFileName.contains(".")) ? finalFileName.substring(0, finalFileName.lastIndexOf('.')) : (subredditName + "-" + postId); NotificationChannelCompat serviceChannel = new NotificationChannelCompat.Builder( NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO, NotificationManagerCompat.IMPORTANCE_LOW) .setName(NotificationUtils.CHANNEL_DOWNLOAD_REDDIT_VIDEO) .build(); notificationManager.createNotificationChannel(serviceChannel); int randomNotificationIdOffset = new Random().nextInt(10000); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle), // Use notificationTitle JobService.JOB_END_NOTIFICATION_POLICY_DETACH); } else { notificationManager.notify(NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(builder, notificationTitle)); // Use notificationTitle } String videoUrl = intent.getString(EXTRA_VIDEO_URL); String audioUrlPrefix = Build.VERSION.SDK_INT > Build.VERSION_CODES.N ? videoUrl.substring(0, videoUrl.lastIndexOf('/')) : null; boolean isNsfw = intent.getInt(EXTRA_IS_NSFW, 0) == 1; executor.execute(() -> { final DownloadProgressResponseBody.ProgressListener progressListener = new DownloadProgressResponseBody.ProgressListener() { long time = 0; @Override public void update(long bytesRead, long contentLength, boolean done) { if (!done) { if (contentLength != -1) { long currentTime = System.currentTimeMillis(); if (currentTime - time > 1000) { time = currentTime; updateNotification(builder, 0, (int) ((100 * bytesRead) / contentLength), randomNotificationIdOffset, null); } } } } }; OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(chain -> { okhttp3.Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() .body(new DownloadProgressResponseBody(originalResponse.body(), progressListener)) .build(); }) .build(); retrofit = retrofit.newBuilder().client(client).build(); DownloadFile downloadFileRetrofit = retrofit.create(DownloadFile.class); boolean separateDownloadFolder = sharedPreferences.getBoolean(SharedPreferencesUtils.SEPARATE_FOLDER_FOR_EACH_SUBREDDIT, false); File externalCacheDirectory = Utils.getCacheDir(this); if (externalCacheDirectory != null) { // Use the filename passed via extras String destinationFileName = (finalFileName != null && !finalFileName.isEmpty()) ? finalFileName : (cacheBaseName + ".mp4"); // Use cacheBaseName for temporary files to avoid conflicts if sanitization differs slightly String tempFileBaseName = cacheBaseName; try { Response videoResponse = downloadFileRetrofit.downloadFile(videoUrl).execute(); if (videoResponse.isSuccessful() && videoResponse.body() != null) { String externalCacheDirectoryPath = externalCacheDirectory.getAbsolutePath() + "/"; String destinationFileDirectory; if (isNsfw && sharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER, false)) { destinationFileDirectory = sharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); } else { destinationFileDirectory = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); } String destinationFileUriString; // Backup validation in case empty directory somehow gets through if (destinationFileDirectory == null || destinationFileDirectory.isEmpty()) { downloadFinished(params, builder, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, randomNotificationIdOffset); return; } boolean isDefaultDestination; isDefaultDestination = false; DocumentFile picFile; DocumentFile dir; try { if (separateDownloadFolder) { dir = DocumentFile.fromTreeUri(DownloadRedditVideoService.this, Uri.parse(destinationFileDirectory)); if (dir == null) { downloadFinished(params, builder, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, randomNotificationIdOffset); return; } dir = dir.findFile(subredditName); if (dir == null) { dir = DocumentFile.fromTreeUri(DownloadRedditVideoService.this, Uri.parse(destinationFileDirectory)).createDirectory(subredditName); if (dir == null) { downloadFinished(params, builder, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, randomNotificationIdOffset); return; } } } else { dir = DocumentFile.fromTreeUri(DownloadRedditVideoService.this, Uri.parse(destinationFileDirectory)); if (dir == null) { downloadFinished(params, builder, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, randomNotificationIdOffset); return; } } } catch (IllegalArgumentException e) { // Handle invalid URI format as backup e.printStackTrace(); downloadFinished(params, builder, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, randomNotificationIdOffset); return; } DocumentFile checkForDuplicates = dir.findFile(destinationFileName); int num = 1; String baseNameForCheck = destinationFileName.substring(0, destinationFileName.lastIndexOf('.')); String extension = destinationFileName.substring(destinationFileName.lastIndexOf('.')); while (checkForDuplicates != null) { // Handle duplicates based on the final intended filename structure destinationFileName = baseNameForCheck + " (" + num + ")" + extension; checkForDuplicates = dir.findFile(destinationFileName); num++; } // Update tempFileBaseName if duplicates forced a change in the final name, // though cache files might not strictly need this if based on postId. // Let's keep cache names simple based on initial cacheBaseName for now. picFile = dir.createFile("video/mp4", destinationFileName); if (picFile == null) { downloadFinished(params, builder, null, ERROR_CANNOT_GET_DESTINATION_DIRECTORY, randomNotificationIdOffset); return; } destinationFileUriString = picFile.getUri().toString(); updateNotification(builder, R.string.downloading_reddit_video_audio_track, 0, randomNotificationIdOffset, null); // Use tempFileBaseName for cache file path String videoFilePath = externalCacheDirectoryPath + tempFileBaseName + "-cache.mp4"; String savedVideoFilePath = writeResponseBodyToDisk(videoResponse.body(), videoFilePath); if (savedVideoFilePath == null) { downloadFinished(params, builder, null, ERROR_VIDEO_FILE_CANNOT_SAVE, randomNotificationIdOffset); return; } if (audioUrlPrefix != null) { ResponseBody audioResponse = getAudioResponse(downloadFileRetrofit, audioUrlPrefix, 0); // Use tempFileBaseName for cache file path String outputFilePath = externalCacheDirectoryPath + tempFileBaseName + ".mp4"; if (audioResponse != null) { // Use tempFileBaseName for cache file path String audioFilePath = externalCacheDirectoryPath + tempFileBaseName + "-cache.mp3"; String savedAudioFilePath = writeResponseBodyToDisk(audioResponse, audioFilePath); if (savedAudioFilePath == null) { downloadFinished(params, builder, null, ERROR_AUDIO_FILE_CANNOT_SAVE, randomNotificationIdOffset); return; } updateNotification(builder, R.string.downloading_reddit_video_muxing, -1, randomNotificationIdOffset, null); if (!muxVideoAndAudio(videoFilePath, audioFilePath, outputFilePath)) { downloadFinished(params, builder, null, ERROR_MUX_FAILED, randomNotificationIdOffset); return; } updateNotification(builder, R.string.downloading_reddit_video_save_file_to_public_dir, -1, randomNotificationIdOffset, null); try { Uri destinationFileUri = copyToDestination(outputFilePath, destinationFileUriString, destinationFileName, isDefaultDestination); new File(videoFilePath).delete(); new File(audioFilePath).delete(); new File(outputFilePath).delete(); downloadFinished(params, builder, destinationFileUri, NO_ERROR, randomNotificationIdOffset); } catch (IOException e) { e.printStackTrace(); downloadFinished(params, builder, null, ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE, randomNotificationIdOffset); } } else { updateNotification(builder, R.string.downloading_reddit_video_muxing, -1, randomNotificationIdOffset, null); if (!muxVideoAndAudio(videoFilePath, null, outputFilePath)) { downloadFinished(params, builder, null, ERROR_MUX_FAILED, randomNotificationIdOffset); return; } updateNotification(builder, R.string.downloading_reddit_video_save_file_to_public_dir, -1, randomNotificationIdOffset, null); try { Uri destinationFileUri = copyToDestination(outputFilePath, destinationFileUriString, destinationFileName, isDefaultDestination); new File(videoFilePath).delete(); new File(outputFilePath).delete(); downloadFinished(params, builder, destinationFileUri, NO_ERROR, randomNotificationIdOffset); } catch (IOException e) { e.printStackTrace(); downloadFinished(params, builder, null, ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE, randomNotificationIdOffset); } } } else { // do not remux video on <= Android N, just save video updateNotification(builder, R.string.downloading_reddit_video_save_file_to_public_dir, -1, randomNotificationIdOffset, null); try { Uri destinationFileUri = copyToDestination(videoFilePath, destinationFileUriString, destinationFileName, isDefaultDestination); new File(videoFilePath).delete(); downloadFinished(params, builder, destinationFileUri, NO_ERROR, randomNotificationIdOffset); } catch (IOException e) { e.printStackTrace(); downloadFinished(params, builder, null, ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE, randomNotificationIdOffset); } } } else { downloadFinished(params, builder, null, ERROR_VIDEO_FILE_CANNOT_DOWNLOAD, randomNotificationIdOffset); } } catch (IOException e) { e.printStackTrace(); downloadFinished(params, builder, null, ERROR_VIDEO_FILE_CANNOT_DOWNLOAD, randomNotificationIdOffset); } } else { downloadFinished(params, builder, null, ERROR_CANNOT_GET_CACHE_DIRECTORY, randomNotificationIdOffset); } }); return true; } @Override public boolean onStopJob(JobParameters params) { return false; } @Nullable private ResponseBody getAudioResponse(DownloadFile downloadFileRetrofit, @NonNull String audioUrlPrefix, int audioSuffixIndex) throws IOException { if (audioSuffixIndex >= possibleAudioUrlSuffices.length) { return null; } String audioUrl = audioUrlPrefix + possibleAudioUrlSuffices[audioSuffixIndex]; Response audioResponse = downloadFileRetrofit.downloadFile(audioUrl).execute(); ResponseBody responseBody = audioResponse.body(); if (audioResponse.isSuccessful() && responseBody != null) { return responseBody; } return getAudioResponse(downloadFileRetrofit, audioUrlPrefix, audioSuffixIndex + 1); } private String writeResponseBodyToDisk(ResponseBody body, String filePath) { try { File file = new File(filePath); InputStream inputStream = null; OutputStream outputStream = null; try { byte[] fileReader = new byte[4096]; long fileSize = body.contentLength(); long fileSizeDownloaded = 0; inputStream = body.byteStream(); outputStream = new FileOutputStream(file); while (true) { int read = inputStream.read(fileReader); if (read == -1) { break; } outputStream.write(fileReader, 0, read); fileSizeDownloaded += read; } outputStream.flush(); return file.getPath(); } catch (IOException e) { return null; } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } catch (IOException e) { return null; } } private boolean muxVideoAndAudio(String videoFilePath, String audioFilePath, String outputFilePath) { try { File file = new File(outputFilePath); file.createNewFile(); MediaExtractor videoExtractor = new MediaExtractor(); videoExtractor.setDataSource(videoFilePath); MediaMuxer muxer = new MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); videoExtractor.selectTrack(0); MediaFormat videoFormat = videoExtractor.getTrackFormat(0); int videoTrack = muxer.addTrack(videoFormat); boolean sawEOS = false; int offset = 100; int sampleSize = 4096 * 1024; ByteBuffer videoBuf = ByteBuffer.allocate(sampleSize); ByteBuffer audioBuf = ByteBuffer.allocate(sampleSize); MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo(); videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); // audio not present for all videos MediaExtractor audioExtractor = new MediaExtractor(); MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo(); int audioTrack = -1; if (audioFilePath != null) { audioExtractor.setDataSource(audioFilePath); audioExtractor.selectTrack(0); MediaFormat audioFormat = audioExtractor.getTrackFormat(0); audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); audioTrack = muxer.addTrack(audioFormat); } muxer.start(); while (!sawEOS) { videoBufferInfo.offset = offset; videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset); if (videoBufferInfo.size < 0) { sawEOS = true; videoBufferInfo.size = 0; } else { videoBufferInfo.presentationTimeUs = videoExtractor.getSampleTime(); videoBufferInfo.flags = videoExtractor.getSampleFlags(); muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo); videoExtractor.advance(); } } if (audioFilePath != null) { boolean sawEOS2 = false; while (!sawEOS2) { audioBufferInfo.offset = offset; audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset); if (audioBufferInfo.size < 0) { sawEOS2 = true; audioBufferInfo.size = 0; } else { audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime(); audioBufferInfo.flags = audioExtractor.getSampleFlags(); muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo); audioExtractor.advance(); } } } muxer.stop(); muxer.release(); } catch (IllegalArgumentException | IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); return false; } return true; } private Uri copyToDestination(String srcPath, String destinationFileUriString, String destinationFileName, boolean isDefaultDestination) throws IOException { ContentResolver contentResolver = getContentResolver(); if (isDefaultDestination) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { InputStream in = new FileInputStream(srcPath); OutputStream out = new FileOutputStream(destinationFileUriString); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } new File(srcPath).delete(); } else { ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, destinationFileName); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4"); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, destinationFileUriString); contentValues.put(MediaStore.Video.Media.IS_PENDING, 1); OutputStream stream = null; Uri uri = null; try { final Uri contentUri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); uri = contentResolver.insert(contentUri, contentValues); if (uri == null) { throw new IOException("Failed to create new MediaStore record."); } stream = contentResolver.openOutputStream(uri); if (stream == null) { throw new IOException("Failed to get output stream."); } InputStream in = new FileInputStream(srcPath); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { stream.write(buf, 0, len); } contentValues.clear(); contentValues.put(MediaStore.Video.Media.IS_PENDING, 0); contentResolver.update(uri, contentValues, null, null); return uri; } catch (IOException e) { if (uri != null) { // Don't leave an orphan entry in the MediaStore contentResolver.delete(uri, null, null); } throw e; } finally { if (stream != null) { stream.close(); } } } } else { OutputStream stream = contentResolver.openOutputStream(Uri.parse(destinationFileUriString)); if (stream == null) { throw new IOException("Failed to get output stream."); } InputStream in = new FileInputStream(srcPath); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { stream.write(buf, 0, len); } } return Uri.parse(destinationFileUriString); } private void downloadFinished(JobParameters parameters, NotificationCompat.Builder builder, Uri destinationFileUri, int errorCode, int randomNotificationIdOffset) { if (errorCode != NO_ERROR) { switch (errorCode) { case ERROR_CANNOT_GET_CACHE_DIRECTORY: updateNotification(builder, R.string.downloading_reddit_video_failed_cannot_get_cache_directory, -1, randomNotificationIdOffset, null); break; case ERROR_VIDEO_FILE_CANNOT_DOWNLOAD: updateNotification(builder, R.string.downloading_reddit_video_failed_cannot_download_video, -1, randomNotificationIdOffset, null); break; case ERROR_VIDEO_FILE_CANNOT_SAVE: updateNotification(builder, R.string.downloading_reddit_video_failed_cannot_save_video, -1, randomNotificationIdOffset, null); break; case ERROR_AUDIO_FILE_CANNOT_SAVE: updateNotification(builder, R.string.downloading_reddit_video_failed_cannot_save_audio, -1, randomNotificationIdOffset, null); break; case ERROR_MUX_FAILED: updateNotification(builder, R.string.downloading_reddit_video_failed_cannot_mux, -1, randomNotificationIdOffset, null); break; case ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE: updateNotification(builder, R.string.downloading_reddit_video_failed_cannot_save_mux_video, -1, randomNotificationIdOffset, null); break; case ERROR_CANNOT_GET_DESTINATION_DIRECTORY: updateNotification(builder, R.string.downloading_media_failed_cannot_save_to_destination_directory, -1, randomNotificationIdOffset, null); break; } } else { MediaScannerConnection.scanFile( this, new String[]{destinationFileUri.toString()}, null, (path, uri) -> { updateNotification(builder, R.string.downloading_reddit_video_finished, -1, randomNotificationIdOffset, destinationFileUri); } ); } jobFinished(parameters, false); } private Notification createNotification(NotificationCompat.Builder builder, String fileName) { builder.setContentTitle(fileName).setContentText(getString(R.string.downloading_reddit_video)).setProgress(100, 0, false); return builder.setSmallIcon(R.drawable.ic_notification).setColor(customThemeWrapper.getColorPrimaryLightTheme()).build(); } private void updateNotification(NotificationCompat.Builder builder, int contentStringResId, int progress, int randomNotificationIdOffset, Uri mediaUri) { if (notificationManager != null) { if (progress < 0) { builder.setProgress(0, 0, false); } else { builder.setProgress(100, progress, false); } if (contentStringResId != 0) { builder.setContentText(getString(contentStringResId)); builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(contentStringResId))); } if (mediaUri != null) { int pendingIntentFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_CANCEL_CURRENT; Intent intent = new Intent(); intent.setAction(android.content.Intent.ACTION_VIEW); intent.setDataAndType(mediaUri, "video/mp4"); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags); builder.setContentIntent(pendingIntent); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, mediaUri); shareIntent.setType("video/mp4"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent intentAction = Intent.createChooser(shareIntent, getString(R.string.share)); PendingIntent shareActionPendingIntent = PendingIntent.getActivity(this, 1, intentAction, pendingIntentFlags); builder.addAction(new NotificationCompat.Action(R.drawable.ic_notification, getString(R.string.share), shareActionPendingIntent)); Intent deleteIntent = new Intent(this, DownloadedMediaDeleteActionBroadcastReceiver.class); deleteIntent.setData(mediaUri); deleteIntent.putExtra(DownloadedMediaDeleteActionBroadcastReceiver.EXTRA_NOTIFICATION_ID, NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID + randomNotificationIdOffset); PendingIntent deleteActionPendingIntent = PendingIntent.getBroadcast(this, 2, deleteIntent, pendingIntentFlags); builder.addAction(new NotificationCompat.Action(R.drawable.ic_notification, getString(R.string.delete), deleteActionPendingIntent)); } else { builder.setContentIntent(null); builder.clearActions(); } notificationManager.notify(NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID + randomNotificationIdOffset, builder.build()); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/services/EditProfileService.java ================================================ package ml.docilealligator.infinityforreddit.services; import android.app.Notification; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.NetworkRequest; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.PersistableBundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import com.bumptech.glide.Glide; import org.greenrobot.eventbus.EventBus; import java.io.FileNotFoundException; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import jp.wasabeef.glide.transformations.CropTransformation; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.events.SubmitChangeAvatarEvent; import ml.docilealligator.infinityforreddit.events.SubmitChangeBannerEvent; import ml.docilealligator.infinityforreddit.events.SubmitSaveProfileEvent; import ml.docilealligator.infinityforreddit.utils.EditProfileUtils; import ml.docilealligator.infinityforreddit.utils.NotificationUtils; import retrofit2.Retrofit; public class EditProfileService extends JobService { public static final String EXTRA_ACCESS_TOKEN = "EAT"; public static final String EXTRA_ACCOUNT_NAME = "EAN"; public static final String EXTRA_DISPLAY_NAME = "EDN"; public static final String EXTRA_ABOUT_YOU = "EAY"; public static final String EXTRA_POST_TYPE = "EPT"; public static final String EXTRA_MEDIA_URI = "EU"; public static final int EXTRA_POST_TYPE_UNKNOWN = 0x500; public static final int EXTRA_POST_TYPE_CHANGE_BANNER = 0x501; public static final int EXTRA_POST_TYPE_CHANGE_AVATAR = 0x502; public static final int EXTRA_POST_TYPE_SAVE_EDIT_PROFILE = 0x503; private static final int MAX_BANNER_WIDTH = 1280; private static final int MIN_BANNER_WIDTH = 640; private static final int AVATAR_SIZE = 256; private static int JOB_ID = 10000; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Handler handler; public static JobInfo constructJobInfo(Context context, long contentEstimatedBytes, PersistableBundle extras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, EditProfileService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, EditProfileService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } @Override public void onCreate() { super.onCreate(); ((Infinity) getApplication()).getAppComponent().inject(this); handler = new Handler(); } @Override public boolean onStartJob(JobParameters params) { NotificationChannelCompat serviceChannel = new NotificationChannelCompat.Builder( NotificationUtils.CHANNEL_SUBMIT_POST, NotificationManagerCompat.IMPORTANCE_LOW) .setName(NotificationUtils.CHANNEL_SUBMIT_POST) .build(); NotificationManagerCompat manager = NotificationManagerCompat.from(this); manager.createNotificationChannel(serviceChannel); int randomNotificationIdOffset = new Random().nextInt(10000); PersistableBundle bundle = params.getExtras(); String accessToken = bundle.getString(EXTRA_ACCESS_TOKEN); String accountName = bundle.getString(EXTRA_ACCOUNT_NAME); final int postType = bundle.getInt(EXTRA_POST_TYPE, EXTRA_POST_TYPE_UNKNOWN); switch (postType) { case EXTRA_POST_TYPE_CHANGE_BANNER: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.EDIT_PROFILE_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.submit_change_banner), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.EDIT_PROFILE_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.submit_change_banner)); } break; case EXTRA_POST_TYPE_CHANGE_AVATAR: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.EDIT_PROFILE_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.submit_change_avatar), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.EDIT_PROFILE_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.submit_change_avatar)); } break; case EXTRA_POST_TYPE_SAVE_EDIT_PROFILE: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.EDIT_PROFILE_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.submit_save_profile), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.EDIT_PROFILE_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.submit_save_profile)); } break; default: case EXTRA_POST_TYPE_UNKNOWN: return false; } mExecutor.execute(() -> { switch (postType) { case EXTRA_POST_TYPE_CHANGE_BANNER: submitChangeBannerSync(params, accessToken, Uri.parse(bundle.getString(EXTRA_MEDIA_URI)), accountName); break; case EXTRA_POST_TYPE_CHANGE_AVATAR: submitChangeAvatarSync(params, accessToken, Uri.parse(bundle.getString(EXTRA_MEDIA_URI)), accountName); break; case EXTRA_POST_TYPE_SAVE_EDIT_PROFILE: submitSaveEditProfileSync( params, accessToken, accountName, bundle.getString(EXTRA_DISPLAY_NAME), bundle.getString(EXTRA_ABOUT_YOU) ); break; } }); return true; } @Override public boolean onStopJob(JobParameters params) { return false; } @WorkerThread private void submitChangeBannerSync(JobParameters parameters, String accessToken, Uri mediaUri, String accountName) { try { final int width = getWidthBanner(mediaUri); final int height = Math.round(width * 3 / 10f); // ratio 10:3 CropTransformation bannerCrop = new CropTransformation(width, height, CropTransformation.CropType.CENTER); Bitmap resource = Glide.with(this).asBitmap().skipMemoryCache(true) .load(mediaUri).transform(bannerCrop).submit().get(); String potentialError = EditProfileUtils.uploadBannerSync(mOauthRetrofit, accessToken, accountName, resource); if (potentialError == null) { //Successful handler.post(() -> EventBus.getDefault().post(new SubmitChangeBannerEvent(true, ""))); jobFinished(parameters, false); } else { handler.post(() -> EventBus.getDefault().post(new SubmitChangeBannerEvent(false, potentialError))); jobFinished(parameters, false); } } catch (InterruptedException | ExecutionException | FileNotFoundException e) { e.printStackTrace(); handler.post(() -> EventBus.getDefault().post(new SubmitChangeBannerEvent(false, e.getLocalizedMessage()))); jobFinished(parameters, false); } } @WorkerThread private void submitChangeAvatarSync(JobParameters parameters, String accessToken, Uri mediaUri, String accountName) { try { final CropTransformation avatarCrop = new CropTransformation(AVATAR_SIZE, AVATAR_SIZE, CropTransformation.CropType.CENTER); final Bitmap resource = Glide.with(this).asBitmap().skipMemoryCache(true) .load(mediaUri).transform(avatarCrop).submit().get(); String potentialError = EditProfileUtils.uploadAvatarSync(mOauthRetrofit, accessToken, accountName, resource); if (potentialError == null) { //Successful handler.post(() -> EventBus.getDefault().post(new SubmitChangeAvatarEvent(true, ""))); jobFinished(parameters, false); } else { handler.post(() -> EventBus.getDefault().post(new SubmitChangeAvatarEvent(false, potentialError))); jobFinished(parameters, false); } } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); handler.post(() -> EventBus.getDefault().post(new SubmitChangeAvatarEvent(false, e.getLocalizedMessage()))); jobFinished(parameters, false); } } @WorkerThread private void submitSaveEditProfileSync(JobParameters parameters, @Nullable String accessToken, @NonNull String accountName, String displayName, String publicDesc ) { String potentialError = EditProfileUtils.updateProfileSync(mOauthRetrofit, accessToken, accountName, displayName, publicDesc); if (potentialError == null) { //Successful handler.post(() -> EventBus.getDefault().post(new SubmitSaveProfileEvent(true, ""))); jobFinished(parameters, false); } else { handler.post(() -> EventBus.getDefault().post(new SubmitSaveProfileEvent(false, potentialError))); jobFinished(parameters, false); } } private Notification createNotification(int stringResId) { return new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_SUBMIT_POST) .setContentTitle(getString(stringResId)) .setContentText(getString(R.string.please_wait)) .setSmallIcon(R.drawable.ic_notification) .setColor(mCustomThemeWrapper.getColorPrimaryLightTheme()) .build(); } private int getWidthBanner(Uri mediaUri) throws FileNotFoundException { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(getContentResolver().openInputStream(mediaUri), null, options); return Math.max(Math.min(options.outWidth, MAX_BANNER_WIDTH), MIN_BANNER_WIDTH); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/services/SubmitPostService.java ================================================ package ml.docilealligator.infinityforreddit.services; import android.app.Notification; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobService; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.NetworkRequest; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.PersistableBundle; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import com.bumptech.glide.Glide; import org.greenrobot.eventbus.EventBus; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import ml.docilealligator.infinityforreddit.utils.APIUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.network.AnyAccountAccessTokenAuthenticator; import ml.docilealligator.infinityforreddit.subreddit.Flair; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.events.SubmitCrosspostEvent; import ml.docilealligator.infinityforreddit.events.SubmitGalleryPostEvent; import ml.docilealligator.infinityforreddit.events.SubmitImagePostEvent; import ml.docilealligator.infinityforreddit.events.SubmitPollPostEvent; import ml.docilealligator.infinityforreddit.events.SubmitTextOrLinkPostEvent; import ml.docilealligator.infinityforreddit.events.SubmitVideoOrGifPostEvent; import ml.docilealligator.infinityforreddit.markdown.RichTextJSONConverter; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.post.SubmitPost; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.NotificationUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import retrofit2.Response; import retrofit2.Retrofit; public class SubmitPostService extends JobService { public static final String EXTRA_ACCOUNT = "EA"; public static final String EXTRA_SUBREDDIT_NAME = "ESN"; public static final String EXTRA_TITLE = "ET"; public static final String EXTRA_CONTENT = "EC"; public static final String EXTRA_IS_RICHTEXT_JSON = "EIRJ"; public static final String EXTRA_UPLOADED_IMAGES = "EUI"; public static final String EXTRA_URL = "EU"; public static final String EXTRA_REDDIT_GALLERY_PAYLOAD = "ERGP"; public static final String EXTRA_POLL_PAYLOAD = "EPP"; public static final String EXTRA_KIND = "EK"; public static final String EXTRA_FLAIR = "EF"; public static final String EXTRA_IS_SPOILER = "EIS"; public static final String EXTRA_IS_NSFW = "EIN"; public static final String EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS = "ERPRN"; public static final String EXTRA_POST_TYPE = "EPT"; public static final String EXTRA_MEDIA_URI = "EMU"; public static final int EXTRA_POST_TEXT_OR_LINK = 0; public static final int EXTRA_POST_TYPE_IMAGE = 1; public static final int EXTRA_POST_TYPE_VIDEO = 2; public static final int EXTRA_POST_TYPE_GALLERY = 3; public static final int EXTRA_POST_TYPE_POLL = 4; public static final int EXTRA_POST_TYPE_CROSSPOST = 5; private static int JOB_ID = 1000; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject @Named("oauth") Retrofit mOauthRetrofit; @Inject @Named("upload_media") Retrofit mUploadMediaRetrofit; @Inject @Named("upload_video") Retrofit mUploadVideoRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; @Inject Executor mExecutor; private Handler handler; public SubmitPostService() { } public static JobInfo constructJobInfo(Context context, long contentEstimatedBytes, PersistableBundle extras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, SubmitPostService.class)) .setUserInitiated(true) .setRequiredNetwork(new NetworkRequest.Builder().clearCapabilities().build()) .setEstimatedNetworkBytes(0, contentEstimatedBytes + 500) .setExtras(extras) .build(); } else { return new JobInfo.Builder(JOB_ID++, new ComponentName(context, SubmitPostService.class)) .setOverrideDeadline(0) .setExtras(extras) .build(); } } @Override public boolean onStartJob(JobParameters params) { NotificationChannelCompat serviceChannel = new NotificationChannelCompat.Builder( NotificationUtils.CHANNEL_SUBMIT_POST, NotificationManagerCompat.IMPORTANCE_LOW) .setName(NotificationUtils.CHANNEL_SUBMIT_POST) .build(); NotificationManagerCompat manager = NotificationManagerCompat.from(this); manager.createNotificationChannel(serviceChannel); int randomNotificationIdOffset = new Random().nextInt(10000); PersistableBundle bundle = params.getExtras(); int postType = bundle.getInt(EXTRA_POST_TYPE, EXTRA_POST_TEXT_OR_LINK); if (postType == EXTRA_POST_TEXT_OR_LINK) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting)); } } else if (postType == EXTRA_POST_TYPE_CROSSPOST) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting)); } } else if (postType == EXTRA_POST_TYPE_IMAGE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_image), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_image)); } } else if (postType == EXTRA_POST_TYPE_VIDEO) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_video), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_video)); } } else if (postType == EXTRA_POST_TYPE_GALLERY) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_gallery), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_gallery)); } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setNotification(params, NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_poll), JobService.JOB_END_NOTIFICATION_POLICY_REMOVE); } else { manager.notify(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset, createNotification(R.string.posting_poll)); } } mExecutor.execute(() -> { Account account = Account.fromJson(bundle.getString(EXTRA_ACCOUNT)); String subredditName = bundle.getString(EXTRA_SUBREDDIT_NAME); String title = bundle.getString(EXTRA_TITLE); Flair flair = Flair.fromJson(bundle.getString(EXTRA_FLAIR)); boolean isSpoiler = bundle.getInt(EXTRA_IS_SPOILER, 0) == 1; boolean isNSFW = bundle.getInt(EXTRA_IS_NSFW, 0) == 1; boolean receivePostReplyNotifications = bundle.getInt(EXTRA_RECEIVE_POST_REPLY_NOTIFICATIONS, 1) == 1; Retrofit newAuthenticatorOauthRetrofit = mOauthRetrofit.newBuilder() .client(new OkHttpClient.Builder() .authenticator(new AnyAccountAccessTokenAuthenticator(APIUtils.getClientId(getApplicationContext()), mRetrofit, mRedditDataRoomDatabase, account, mCurrentAccountSharedPreferences)) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build()) .build(); if (postType == EXTRA_POST_TEXT_OR_LINK) { String content = bundle.getString(EXTRA_CONTENT, ""); boolean isRichTextJSON = bundle.getInt(EXTRA_IS_RICHTEXT_JSON, 0) == 1; String kind = bundle.getString(EXTRA_KIND); if (isRichTextJSON) { List uploadedImages = UploadedImage.fromListJson(bundle.getString(EXTRA_UPLOADED_IMAGES)); try { content = new RichTextJSONConverter().constructRichTextJSON(SubmitPostService.this, content, uploadedImages); } catch (JSONException e) { handler.post(() -> EventBus.getDefault().post(new SubmitTextOrLinkPostEvent(false, null, getString(R.string.convert_to_richtext_json_failed)))); return; } } submitTextOrLinkPost(params, manager, randomNotificationIdOffset, newAuthenticatorOauthRetrofit, account, subredditName, title, content, bundle.getString(EXTRA_URL), flair, isSpoiler, isNSFW, receivePostReplyNotifications, isRichTextJSON, kind); } else if (postType == EXTRA_POST_TYPE_CROSSPOST) { submitCrosspost(params, manager, randomNotificationIdOffset, mExecutor, handler, newAuthenticatorOauthRetrofit, account, subredditName, title, bundle.getString(EXTRA_CONTENT), flair, isSpoiler, isNSFW, receivePostReplyNotifications); } else if (postType == EXTRA_POST_TYPE_IMAGE) { Uri mediaUri = Uri.parse(bundle.getString(EXTRA_MEDIA_URI)); submitImagePost(params, manager, randomNotificationIdOffset, newAuthenticatorOauthRetrofit, getContentResolver(), account, mediaUri, subredditName, title, bundle.getString(EXTRA_CONTENT), flair, isSpoiler, isNSFW, receivePostReplyNotifications); } else if (postType == EXTRA_POST_TYPE_VIDEO) { Uri mediaUri = Uri.parse(bundle.getString(EXTRA_MEDIA_URI)); submitVideoPost(params, manager, randomNotificationIdOffset, newAuthenticatorOauthRetrofit, account, mediaUri, subredditName, title, bundle.getString(EXTRA_CONTENT), flair, isSpoiler, isNSFW, receivePostReplyNotifications); } else if (postType == EXTRA_POST_TYPE_GALLERY) { submitGalleryPost(params, manager, randomNotificationIdOffset, newAuthenticatorOauthRetrofit, account, bundle.getString(EXTRA_REDDIT_GALLERY_PAYLOAD)); } else { submitPollPost(params, manager, randomNotificationIdOffset, newAuthenticatorOauthRetrofit, account, bundle.getString(EXTRA_POLL_PAYLOAD)); } }); return true; } @Override public boolean onStopJob(JobParameters params) { return false; } @Override public void onCreate() { ((Infinity) getApplication()).getAppComponent().inject(this); handler = new Handler(); } private Notification createNotification(int stringResId) { return new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_SUBMIT_POST) .setContentTitle(getString(stringResId)) .setContentText(getString(R.string.please_wait)) .setSmallIcon(R.drawable.ic_notification) .setColor(mCustomThemeWrapper.getColorPrimaryLightTheme()) .build(); } @WorkerThread private void submitTextOrLinkPost(JobParameters parameters, NotificationManagerCompat manager, int randomNotificationIdOffset, Retrofit newAuthenticatorOauthRetrofit, Account selectedAccount, String subredditName, String title, String content, @Nullable String url, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications, boolean isRichtextJSON, String kind) { SubmitPost.submitTextOrLinkPost(mExecutor, handler, newAuthenticatorOauthRetrofit, selectedAccount.getAccessToken(), subredditName, title, content, url, flair, isSpoiler, isNSFW, receivePostReplyNotifications, isRichtextJSON, kind, new SubmitPost.SubmitPostListener() { @Override public void submitSuccessful(Post post) { handler.post(() -> EventBus.getDefault().post(new SubmitTextOrLinkPostEvent(true, post, null))); stopJob(parameters, manager, randomNotificationIdOffset); } @Override public void submitFailed(@Nullable String errorMessage) { handler.post(() -> EventBus.getDefault().post(new SubmitTextOrLinkPostEvent(false, null, errorMessage))); stopJob(parameters, manager, randomNotificationIdOffset); } }); } @WorkerThread private void submitCrosspost(JobParameters parameters, NotificationManagerCompat manager, int randomNotificationIdOffset, Executor executor, Handler handler, Retrofit newAuthenticatorOauthRetrofit, Account selectedAccount, String subredditName, String title, String content, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications) { SubmitPost.submitCrosspost(executor, handler, newAuthenticatorOauthRetrofit, selectedAccount.getAccessToken(), subredditName, title, content, flair, isSpoiler, isNSFW, receivePostReplyNotifications, APIUtils.KIND_CROSSPOST, new SubmitPost.SubmitPostListener() { @Override public void submitSuccessful(Post post) { handler.post(() -> EventBus.getDefault().post(new SubmitCrosspostEvent(true, post, null))); stopJob(parameters, manager, randomNotificationIdOffset); } @Override public void submitFailed(@Nullable String errorMessage) { handler.post(() -> EventBus.getDefault().post(new SubmitCrosspostEvent(false, null, errorMessage))); stopJob(parameters, manager, randomNotificationIdOffset); } }); } @WorkerThread private void submitImagePost(JobParameters parameters, NotificationManagerCompat manager, int randomNotificationIdOffset, Retrofit newAuthenticatorOauthRetrofit, ContentResolver contentResolver, Account selectedAccount, Uri mediaUri, String subredditName, String title, String content, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications) { SubmitPost.submitImagePost(mExecutor, handler, newAuthenticatorOauthRetrofit, mUploadMediaRetrofit, contentResolver, selectedAccount.getAccessToken(), subredditName, title, content, mediaUri, flair, isSpoiler, isNSFW, receivePostReplyNotifications, new SubmitPost.SubmitPostListener() { @Override public void submitSuccessful(Post post) { handler.post(() -> { EventBus.getDefault().post(new SubmitImagePostEvent(true, null)); Toast.makeText(SubmitPostService.this, R.string.image_is_processing, Toast.LENGTH_SHORT).show(); }); stopJob(parameters, manager, randomNotificationIdOffset); } @Override public void submitFailed(@Nullable String errorMessage) { handler.post(() -> EventBus.getDefault().post(new SubmitImagePostEvent(false, errorMessage))); stopJob(parameters, manager, randomNotificationIdOffset); } }); } @WorkerThread private void submitVideoPost(JobParameters parameters, NotificationManagerCompat manager, int randomNotificationIdOffset, Retrofit newAuthenticatorOauthRetrofit, Account selectedAccount, Uri mediaUri, String subredditName, String title, String content, Flair flair, boolean isSpoiler, boolean isNSFW, boolean receivePostReplyNotifications) { try { InputStream in = getContentResolver().openInputStream(mediaUri); String type = getContentResolver().getType(mediaUri); File cacheDir = Utils.getCacheDir(this); if (cacheDir == null) { handler.post(() -> EventBus.getDefault().post(new SubmitVideoOrGifPostEvent(false, false, getString(R.string.submit_video_or_gif_post_failed_cannot_get_cache_directory)))); return; } String cacheFilePath; if (type != null && type.contains("gif")) { cacheFilePath = cacheDir + "/" + mediaUri.getLastPathSegment() + ".gif"; } else { cacheFilePath = cacheDir + "/" + mediaUri.getLastPathSegment() + ".mp4"; } copyFileToCache(in, cacheFilePath); Bitmap resource = Glide.with(this).asBitmap().load(mediaUri).submit().get(); if (type != null) { SubmitPost.submitVideoPost(mExecutor, handler, newAuthenticatorOauthRetrofit, mUploadMediaRetrofit, mUploadVideoRetrofit, selectedAccount.getAccessToken(), subredditName, title, content, new File(cacheFilePath), type, resource, flair, isSpoiler, isNSFW, receivePostReplyNotifications, new SubmitPost.SubmitPostListener() { @Override public void submitSuccessful(Post post) { handler.post(() -> { EventBus.getDefault().post(new SubmitVideoOrGifPostEvent(true, false, null)); if (type.contains("gif")) { Toast.makeText(SubmitPostService.this, R.string.gif_is_processing, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(SubmitPostService.this, R.string.video_is_processing, Toast.LENGTH_SHORT).show(); } }); stopJob(parameters, manager, randomNotificationIdOffset); } @Override public void submitFailed(@Nullable String errorMessage) { handler.post(() -> EventBus.getDefault().post(new SubmitVideoOrGifPostEvent(false, false, errorMessage))); stopJob(parameters, manager, randomNotificationIdOffset); } }); } else { handler.post(() -> EventBus.getDefault().post(new SubmitVideoOrGifPostEvent(false, true, null))); stopJob(parameters, manager, randomNotificationIdOffset); } } catch (IOException | InterruptedException | ExecutionException e) { e.printStackTrace(); handler.post(() -> EventBus.getDefault().post(new SubmitVideoOrGifPostEvent(false, true, null))); stopJob(parameters, manager, randomNotificationIdOffset); } } @WorkerThread private void submitGalleryPost(JobParameters parameters, NotificationManagerCompat manager, int randomNotificationIdOffset, Retrofit newAuthenticatorOauthRetrofit, Account selectedAccount, String payload) { try { Response response = newAuthenticatorOauthRetrofit.create(RedditAPI.class).submitGalleryPost(APIUtils.getOAuthHeader(selectedAccount.getAccessToken()), payload).execute(); if (response.isSuccessful()) { JSONObject responseObject = new JSONObject(response.body()).getJSONObject(JSONUtils.JSON_KEY); if (responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() != 0) { JSONArray error = responseObject.getJSONArray(JSONUtils.ERRORS_KEY).getJSONArray(responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() - 1); if (error.length() != 0) { String errorMessage; if (error.length() >= 2) { errorMessage = error.getString(1); } else { errorMessage = error.getString(0); } handler.post(() -> EventBus.getDefault().post(new SubmitGalleryPostEvent(false, null, errorMessage))); } else { handler.post(() -> EventBus.getDefault().post(new SubmitGalleryPostEvent(false, null, null))); } } else { String postUrl = responseObject.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.URL_KEY); handler.post(() -> { EventBus.getDefault().post(new SubmitGalleryPostEvent(true, postUrl, null)); }); } } else { handler.post(() -> EventBus.getDefault().post(new SubmitGalleryPostEvent(false, null, response.message()))); } } catch (IOException | JSONException e) { e.printStackTrace(); handler.post(() -> EventBus.getDefault().post(new SubmitGalleryPostEvent(false, null, e.getMessage()))); } finally { stopJob(parameters, manager, randomNotificationIdOffset); } } @WorkerThread private void submitPollPost(JobParameters parameters, NotificationManagerCompat manager, int randomNotificationIdOffset, Retrofit newAuthenticatorOauthRetrofit, Account selectedAccount, String payload) { try { Response response = newAuthenticatorOauthRetrofit.create(RedditAPI.class).submitPollPost(APIUtils.getOAuthHeader(selectedAccount.getAccessToken()), payload).execute(); if (response.isSuccessful()) { JSONObject responseObject = new JSONObject(response.body()).getJSONObject(JSONUtils.JSON_KEY); if (responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() != 0) { JSONArray error = responseObject.getJSONArray(JSONUtils.ERRORS_KEY).getJSONArray(responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() - 1); if (error.length() != 0) { String errorMessage; if (error.length() >= 2) { errorMessage = error.getString(1); } else { errorMessage = error.getString(0); } handler.post(() -> EventBus.getDefault().post(new SubmitPollPostEvent(false, null, errorMessage))); } else { handler.post(() -> EventBus.getDefault().post(new SubmitPollPostEvent(false, null, null))); } } else { String postUrl = responseObject.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.URL_KEY); handler.post(() -> { EventBus.getDefault().post(new SubmitPollPostEvent(true, postUrl, null)); }); } } else { handler.post(() -> EventBus.getDefault().post(new SubmitPollPostEvent(false, null, response.message()))); } } catch (IOException | JSONException e) { e.printStackTrace(); handler.post(() -> EventBus.getDefault().post(new SubmitPollPostEvent(false, null, e.getMessage()))); } finally { stopJob(parameters, manager, randomNotificationIdOffset); } } private static void copyFileToCache(InputStream fileInputStream, String destinationFilePath) throws IOException { OutputStream out = new FileOutputStream(destinationFilePath); byte[] buf = new byte[2048]; int len; while ((len = fileInputStream.read(buf)) > 0) { out.write(buf, 0, len); } } private void stopJob(JobParameters parameters, NotificationManagerCompat notificationManager, int randomNotificationIdOffset) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { notificationManager.cancel(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset); } jobFinished(parameters, false); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/APIKeysPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.preference.EditTextPreference; import androidx.preference.Preference; import androidx.preference.PreferenceManager; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.QRCodeScannerActivity; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.AppRestartHelper; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class APIKeysPreferenceFragment extends CustomFontPreferenceFragmentCompat { private static final String TAG = "APIKeysPrefFragment"; private static final int CLIENT_ID_LENGTH = 22; private static final int GIPHY_API_KEY_LENGTH = 32; @Inject @Named("default") SharedPreferences mSharedPreferences; private ActivityResultLauncher qrCodeScannerLauncher; private EditTextPreference clientIdPref; private EditText currentClientIdEditText; public APIKeysPreferenceFragment() {} @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Initialize the launcher for QR code scanning qrCodeScannerLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) { String qrCodeResult = result.getData().getStringExtra(QRCodeScannerActivity.EXTRA_QR_CODE_RESULT); if (qrCodeResult != null && qrCodeResult.length() == CLIENT_ID_LENGTH) { // Just set the text in the dialog EditText if it's available if (currentClientIdEditText != null) { currentClientIdEditText.setText(qrCodeResult); Toast.makeText(requireContext(), R.string.qr_code_scanned_press_ok, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(requireContext(), R.string.qr_code_scanned_dialog_closed, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(requireContext(), "Invalid QR code. Client ID must be " + CLIENT_ID_LENGTH + " characters.", Toast.LENGTH_LONG).show(); } } }); } @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { PreferenceManager preferenceManager = getPreferenceManager(); // Use default shared preferences file for client ID preferenceManager.setSharedPreferencesName(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE); setPreferencesFromResource(R.xml.api_keys_preferences, rootKey); ((Infinity) requireActivity().getApplication()).getAppComponent().inject(this); setupClientIdPreference(); setupGiphyApiKeyPreference(); setupUserAgentPreference(); setupRedirectUriPreference(); } private void setupClientIdPreference() { clientIdPref = findPreference(SharedPreferencesUtils.CLIENT_ID_PREF_KEY); if (clientIdPref != null) { // Set input type to visible password to prevent suggestions, but allow any string clientIdPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); editText.setSingleLine(true); // Store a reference to the current EditText currentClientIdEditText = editText; // Get current and default values String currentValue = clientIdPref.getText(); String defaultValue = editText.getContext().getString(R.string.default_client_id); // Clear the text field only if the current value is the default value if (currentValue == null || currentValue.isEmpty() || currentValue.equals(defaultValue)) { editText.setText(""); } // Otherwise, the EditText will automatically show the non-default current value // Setup validation for the specific length setupLengthValidation(editText, CLIENT_ID_LENGTH); // Add QR code scanning button to dialog View rootView = editText.getRootView(); if (rootView != null) { // Find the parent container for the EditText View parent = (View) editText.getParent(); if (parent instanceof LinearLayout) { LinearLayout layout = (LinearLayout) parent; // Create a horizontal layout LinearLayout horizontalLayout = new LinearLayout(getContext()); horizontalLayout.setOrientation(LinearLayout.HORIZONTAL); // Setup layout params LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f); LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); // Remove the EditText from its current parent layout.removeView(editText); // Create a scan button ImageButton scanButton = new ImageButton(getContext()); scanButton.setImageResource(android.R.drawable.ic_menu_camera); scanButton.setContentDescription(getString(R.string.content_description_scan_qr_code)); // Set onClick listener for the scan button scanButton.setOnClickListener(v -> { Intent intent = new Intent(getActivity(), QRCodeScannerActivity.class); // Launch QR code scanner as a result activity qrCodeScannerLauncher.launch(intent); }); // Add EditText and button to horizontal layout horizontalLayout.addView(editText, editTextParams); horizontalLayout.addView(scanButton, buttonParams); // Add the horizontal layout to the original parent layout.addView(horizontalLayout, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); } } }); // Set a summary provider that hides the default value but shows custom ones clientIdPref.setSummaryProvider((Preference.SummaryProvider) preference -> { String currentValue = preference.getText(); String defaultValue = preference.getContext().getString(R.string.default_client_id); if (currentValue == null || currentValue.isEmpty() || currentValue.equals(defaultValue)) { // Show generic message if value is null, empty, or the default return preference.getContext().getString(R.string.tap_to_set_client_id); } else { // Show the actual custom client ID return currentValue; } }); clientIdPref.setOnPreferenceChangeListener(((preference, newValue) -> { // Reset the current EditText reference since the dialog will be dismissed currentClientIdEditText = null; String value = (String) newValue; // Final validation check (redundant due to button state, but safe) if (value == null || value.length() != CLIENT_ID_LENGTH) { return false; // Should not happen if button logic is correct } // Manually save the preference value *before* restarting // Get the specific SharedPreferences instance used by the PreferenceManager SharedPreferences prefs = preference.getContext().getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString(SharedPreferencesUtils.CLIENT_ID_PREF_KEY, value); boolean success = editor.commit(); // Use commit() for synchronous saving if (success) { Log.i(TAG, "Client ID manually saved successfully."); // Update the summary provider manually since we return false preference.setSummaryProvider(clientIdPref.getSummaryProvider()); // Re-set to trigger update AppRestartHelper.triggerAppRestart(requireContext()); // Use the helper } else { Log.e(TAG, "Failed to save Client ID manually."); Toast.makeText(getContext(), "Error saving Client ID.", Toast.LENGTH_SHORT).show(); // Don't restart if save failed } // Return false because we handled the saving manually (or attempted to) return false; })); // Add dialog click listener to reset the current EditText reference when dialog is canceled clientIdPref.setOnPreferenceClickListener(preference -> { // This will be called before the dialog appears // We'll set the EditText reference in setOnBindEditTextListener return false; // Return false to allow normal processing }); } else { Log.e(TAG, "Could not find Client ID preference: " + SharedPreferencesUtils.CLIENT_ID_PREF_KEY); } } private void setupGiphyApiKeyPreference() { EditTextPreference giphyApiKeyPref = findPreference(SharedPreferencesUtils.GIPHY_API_KEY_PREF_KEY); if (giphyApiKeyPref != null) { // Set input type to visible password to prevent suggestions giphyApiKeyPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); editText.setSingleLine(true); // No need to clear the text field like for client ID, as there's no "default" shown // Setup validation for the specific length setupLengthValidation(editText, GIPHY_API_KEY_LENGTH); }); // Set a summary provider that hides the default value but shows custom ones giphyApiKeyPref.setSummaryProvider((Preference.SummaryProvider) preference -> { String currentValue = preference.getText(); // Use the default Giphy key from resources as the "default" for comparison, even though we don't display it. String defaultValue = preference.getContext().getString(R.string.default_giphy_api_key); // Need to add this string resource if (currentValue == null || currentValue.isEmpty() || currentValue.equals(defaultValue)) { // Show generic message if value is null, empty, or the (unseen) default return preference.getContext().getString(R.string.tap_to_set_giphy_api_key); } else { // Show the actual custom Giphy API Key return currentValue; } }); giphyApiKeyPref.setOnPreferenceChangeListener(((preference, newValue) -> { String value = (String) newValue; // Final validation check (redundant due to button state, but safe) if (value == null || value.length() != GIPHY_API_KEY_LENGTH) { // Also allow empty string to revert to default if (value != null && value.isEmpty()) { // Handled below } else { Toast.makeText(getContext(), R.string.giphy_api_key_length_error, Toast.LENGTH_SHORT).show(); return false; // Prevent saving if invalid length (and not empty) } } // Allow empty string to revert to default if (value == null) { value = ""; // Treat null as empty } // Get the specific SharedPreferences instance used by the PreferenceManager SharedPreferences prefs = preference.getContext().getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString(SharedPreferencesUtils.GIPHY_API_KEY_PREF_KEY, value); boolean success = editor.commit(); // Use commit() for synchronous saving if (success) { Log.i(TAG, "Giphy API Key saved successfully."); // Re-set the SummaryProvider to trigger a summary update preference.setSummaryProvider(giphyApiKeyPref.getSummaryProvider()); Toast.makeText(getContext(), "Giphy API Key saved.", Toast.LENGTH_SHORT).show(); } else { Log.e(TAG, "Failed to save Giphy API Key."); Toast.makeText(getContext(), "Error saving Giphy API Key.", Toast.LENGTH_SHORT).show(); } // Return false because we handle the saving and summary update manually return true; })); } else { Log.e(TAG, "Could not find Giphy API Key preference: " + SharedPreferencesUtils.GIPHY_API_KEY_PREF_KEY); } } private void setupUserAgentPreference() { EditTextPreference userAgentPref = findPreference(SharedPreferencesUtils.USER_AGENT_PREF_KEY); if (userAgentPref != null) { userAgentPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); editText.setSingleLine(true); String currentValue = userAgentPref.getText(); if (currentValue == null || currentValue.isEmpty() || currentValue.equals(APIUtils.DEFAULT_USER_AGENT)) { editText.setText(""); } }); userAgentPref.setSummaryProvider((Preference.SummaryProvider) preference -> { String currentValue = preference.getText(); if (currentValue == null || currentValue.isEmpty() || currentValue.equals(APIUtils.DEFAULT_USER_AGENT)) { return preference.getContext().getString(R.string.tap_to_set_user_agent); } else { return currentValue; } }); userAgentPref.setOnPreferenceChangeListener(((preference, newValue) -> { String value = (String) newValue; if (value == null) { value = ""; } SharedPreferences prefs = preference.getContext().getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString(SharedPreferencesUtils.USER_AGENT_PREF_KEY, value); boolean success = editor.commit(); if (success) { Log.i(TAG, "User Agent saved successfully."); preference.setSummaryProvider(userAgentPref.getSummaryProvider()); AppRestartHelper.triggerAppRestart(requireContext()); } else { Log.e(TAG, "Failed to save User Agent."); Toast.makeText(getContext(), "Error saving User Agent.", Toast.LENGTH_SHORT).show(); } return false; })); } else { Log.e(TAG, "Could not find User Agent preference: " + SharedPreferencesUtils.USER_AGENT_PREF_KEY); } } private void setupRedirectUriPreference() { EditTextPreference redirectUriPref = findPreference(SharedPreferencesUtils.REDIRECT_URI_PREF_KEY); if (redirectUriPref != null) { redirectUriPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); editText.setSingleLine(true); String currentValue = redirectUriPref.getText(); String defaultValue = editText.getContext().getString(R.string.default_redirect_uri); if (currentValue == null || currentValue.isEmpty() || currentValue.equals(defaultValue)) { editText.setText(""); } }); redirectUriPref.setSummaryProvider((Preference.SummaryProvider) preference -> { String currentValue = preference.getText(); String defaultValue = preference.getContext().getString(R.string.default_redirect_uri); if (currentValue == null || currentValue.isEmpty() || currentValue.equals(defaultValue)) { return preference.getContext().getString(R.string.tap_to_set_redirect_uri); } else { return currentValue; } }); redirectUriPref.setOnPreferenceChangeListener(((preference, newValue) -> { String value = (String) newValue; if (value == null) { value = ""; } SharedPreferences prefs = preference.getContext().getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString(SharedPreferencesUtils.REDIRECT_URI_PREF_KEY, value); boolean success = editor.commit(); if (success) { Log.i(TAG, "Redirect URI saved successfully."); preference.setSummaryProvider(redirectUriPref.getSummaryProvider()); AppRestartHelper.triggerAppRestart(requireContext()); } else { Log.e(TAG, "Failed to save Redirect URI."); Toast.makeText(getContext(), "Error saving Redirect URI.", Toast.LENGTH_SHORT).show(); } return false; })); } else { Log.e(TAG, "Could not find Redirect URI preference: " + SharedPreferencesUtils.REDIRECT_URI_PREF_KEY); } } // Reusable helper method for setting up length validation on an EditTextPreference dialog private void setupLengthValidation(android.widget.EditText editText, final int requiredLength) { // Add TextWatcher to enable/disable OK button based on length editText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { View rootView = editText.getRootView(); if (rootView != null) { Button positiveButton = rootView.findViewById(android.R.id.button1); if (positiveButton != null) { boolean isEnabled = s.length() == requiredLength || (requiredLength == GIPHY_API_KEY_LENGTH && s.length() == 0); // Allow empty for Giphy positiveButton.setEnabled(isEnabled); positiveButton.setAlpha(isEnabled ? 1.0f : 0.5f); // Adjust alpha for visual feedback } else { Log.w(TAG, "Could not find positive button (android.R.id.button1) in dialog root view (afterTextChanged)."); } } else { Log.w(TAG, "Could not get root view from EditText to find positive button (afterTextChanged)."); } } }); // Ensure the button state is correct initially based on the current value editText.post(() -> { View rootView = editText.getRootView(); if (rootView != null) { Button positiveButton = rootView.findViewById(android.R.id.button1); if (positiveButton != null) { boolean isEnabled = editText.getText().length() == requiredLength || (requiredLength == GIPHY_API_KEY_LENGTH && editText.getText().length() == 0); // Allow empty for Giphy positiveButton.setEnabled(isEnabled); positiveButton.setAlpha(isEnabled ? 1.0f : 0.5f); } else { Log.w(TAG, "Could not find positive button (android.R.id.button1) in dialog root view (post)."); } } else { Log.w(TAG, "Could not get root view from EditText to find positive button (post)."); } }); } // Inject dependencies @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ((Infinity) requireActivity().getApplication()).getAppComponent().inject(this); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/AboutPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.widget.Toast; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; /** * A simple {@link PreferenceFragmentCompat} subclass. */ public class AboutPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.about_preferences, rootKey); Preference openSourcePreference = findPreference(SharedPreferencesUtils.OPEN_SOURCE_KEY); Preference ratePreference = findPreference(SharedPreferencesUtils.RATE_KEY); Preference emailPreference = findPreference(SharedPreferencesUtils.EMAIL_KEY); Preference redditAccountPreference = findPreference(SharedPreferencesUtils.REDDIT_ACCOUNT_KEY); Preference subredditPreference = findPreference(SharedPreferencesUtils.SUBREDDIT_KEY); Preference sharePreference = findPreference(SharedPreferencesUtils.SHARE_KEY); Preference versionPreference = findPreference(SharedPreferencesUtils.VERSION_KEY); if (openSourcePreference != null) { openSourcePreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://github.com/cygnusx-1-org/continuum")); mActivity.startActivity(intent); return true; }); } if (ratePreference != null) { ratePreference.setOnPreferenceClickListener(preference -> { Intent playStoreIntent = new Intent(Intent.ACTION_VIEW); playStoreIntent.setData(Uri.parse("market://details?id=ml.docilealligator.infinityforreddit.plus")); if (playStoreIntent.resolveActivity(mActivity.getPackageManager()) != null) { mActivity.startActivity(playStoreIntent); } else { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://play.google.com/store/apps/details?id=ml.docilealligator.infinityforreddit")); mActivity.startActivity(intent); } return true; }); } if (emailPreference != null) { emailPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setData(Uri.parse("mailto:continuum@cygnusx-1.org")); try { mActivity.startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(mActivity, R.string.no_email_client, Toast.LENGTH_SHORT).show(); } return true; }); } if (redditAccountPreference != null) { redditAccountPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.reddit.com/user/edgan")); mActivity.startActivity(intent); return true; }); } if (subredditPreference != null) { subredditPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.reddit.com/r/continuumreddit")); mActivity.startActivity(intent); return true; }); } if (sharePreference != null) { sharePreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_this_app)); if (intent.resolveActivity(mActivity.getPackageManager()) != null) { mActivity.startActivity(intent); } else { Toast.makeText(mActivity, R.string.no_app, Toast.LENGTH_SHORT).show(); } return true; }); } if (versionPreference != null) { String appName = getString(R.string.application_name); String summary = String.format("%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.APPLICATION_ID); versionPreference.setSummary(summary); versionPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { int clickedTimes = 0; @Override public boolean onPreferenceClick(Preference preference) { clickedTimes++; if (clickedTimes > 6) { Toast.makeText(mActivity, R.string.no_developer_easter_egg, Toast.LENGTH_SHORT).show(); clickedTimes = 0; } return true; } }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/Acknowledgement.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.net.Uri; public class Acknowledgement { private final String name; private final String introduction; private final Uri link; Acknowledgement(String name, String introduction, Uri link) { this.name = name; this.introduction = introduction; this.link = link; } public String getName() { return name; } public String getIntroduction() { return introduction; } public Uri getLink() { return link; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/AcknowledgementFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.adapters.AcknowledgementRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; import ml.docilealligator.infinityforreddit.databinding.FragmentAcknowledgementBinding; import ml.docilealligator.infinityforreddit.utils.Utils; /** * A simple {@link Fragment} subclass. */ public class AcknowledgementFragment extends Fragment { private SettingsActivity mActivity; public AcknowledgementFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentAcknowledgementBinding binding = FragmentAcknowledgementBinding.inflate(inflater, container, false); ArrayList acknowledgements = new ArrayList<>(); acknowledgements.add(new Acknowledgement("ExoPlayer", "An application level media player for Android", Uri.parse("https://github.com/google/ExoPlayer"))); acknowledgements.add(new Acknowledgement("GestureViews", "ImageView and FrameLayout with gestures control and position animation", Uri.parse("https://github.com/alexvasilkov/GestureViews"))); acknowledgements.add(new Acknowledgement("Glide", "A fast and efficient open source media management and image loading framework for Android", Uri.parse("https://github.com/bumptech/glide"))); acknowledgements.add(new Acknowledgement("Retrofit", "Type-safe HTTP client for Android and Java by Square, Inc.", Uri.parse("https://github.com/square/retrofit"))); acknowledgements.add(new Acknowledgement("Dagger", "A fast dependency injector for Java and Android.", Uri.parse("https://github.com/google/dagger"))); acknowledgements.add(new Acknowledgement("Aspect Ratio ImageView", "A simple imageview which scales the width or height aspect with the given ratio", Uri.parse("https://github.com/santalu/aspect-ratio-imageview"))); acknowledgements.add(new Acknowledgement("MaterialLoadingProgressBar", "A styled ProgressBar", Uri.parse("https://github.com/lsjwzh/MaterialLoadingProgressBar"))); acknowledgements.add(new Acknowledgement("Markwon", "A markdown library for Android", Uri.parse("https://github.com/noties/Markwon"))); acknowledgements.add(new Acknowledgement("android-gif-drawable", "Views and Drawable for animated GIFs in Android.", Uri.parse("https://github.com/koral--/android-gif-drawable"))); acknowledgements.add(new Acknowledgement("SimpleSearchView", "A simple SearchView for Android based on Material Design", Uri.parse("https://github.com/Ferfalk/SimpleSearchView"))); acknowledgements.add(new Acknowledgement("EventBus", "A publish/subscribe event bus for Android and Java", Uri.parse("https://github.com/greenrobot/EventBus"))); acknowledgements.add(new Acknowledgement("Customized and Expandable TextView", "Simple library to change the Textview as rectangle, circle and square shapes", Uri.parse("https://github.com/Rajagopalr3/CustomizedTextView"))); acknowledgements.add(new Acknowledgement("Bridge", "A library for avoiding TransactionTooLargeException during state saving and restoration", Uri.parse("https://github.com/livefront/bridge"))); acknowledgements.add(new Acknowledgement("Android-State", "A utility library for Android to save objects in a Bundle without any boilerplate", Uri.parse("https://github.com/evernote/android-state"))); acknowledgements.add(new Acknowledgement("FlowLayout", "A FlowLayout for Android, which allows child views flow to next row when there is no enough space.", Uri.parse("https://github.com/nex3z/FlowLayout"))); acknowledgements.add(new Acknowledgement("Gson", "Gson is a Java library that can be used to convert Java Objects into their JSON representation.", Uri.parse("https://github.com/google/gson"))); acknowledgements.add(new Acknowledgement("Hauler", "Hauler is an Android library containing custom layout which enables to easily create swipe to dismiss Activity.", Uri.parse("https://github.com/futuredapp/hauler"))); acknowledgements.add(new Acknowledgement("Slidr", "Easily add slide to dismiss functionality to an Activity", Uri.parse("https://github.com/r0adkll/Slidr"))); acknowledgements.add(new Acknowledgement("commonmark-java", "Java library for parsing and rendering Markdown text according to the CommonMark specification (and some extensions).", Uri.parse("https://github.com/atlassian/commonmark-java"))); acknowledgements.add(new Acknowledgement("AndroidFastScroll", "Fast scroll for Android RecyclerView and more.", Uri.parse("https://github.com/zhanghai/AndroidFastScroll"))); acknowledgements.add(new Acknowledgement("Subsampling Scale Image View", "A custom image view for Android, designed for photo galleries and displaying " + "huge images (e.g. maps and building plans) without OutOfMemoryErrors.", Uri.parse("https://github.com/davemorrissey/subsampling-scale-image-view"))); acknowledgements.add(new Acknowledgement("BigImageViewer", "Big image viewer supporting pan and zoom, with very little memory " + "usage and full featured image loading choices. Powered by Subsampling Scale " + "Image View, Fresco, Glide, and Picasso. Even with gif and webp support!", Uri.parse("https://github.com/Piasy/BigImageViewer"))); acknowledgements.add(new Acknowledgement("BetterLinkMovementMethod", "Attempts to improve how clickable links are detected, highlighted and handled in TextView.", Uri.parse("https://github.com/saket/Better-Link-Movement-Method"))); acknowledgements.add(new Acknowledgement("ZoomLayout", "2D zoom and pan behavior for View hierarchies, images, video streams, and much more, written in Kotlin for Android.", Uri.parse("https://github.com/natario1/ZoomLayout"))); AcknowledgementRecyclerViewAdapter adapter = new AcknowledgementRecyclerViewAdapter(mActivity, acknowledgements); binding.getRoot().setLayoutManager(new LinearLayoutManagerBugFixed(mActivity)); binding.getRoot().setAdapter(adapter); binding.getRoot().setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.getRoot().setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/AdvancedPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import static android.app.Activity.RESULT_OK; import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.InputType; import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.greenrobot.eventbus.EventBus; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.asynctasks.BackupSettings; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllPostLayouts; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllReadPosts; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllSortTypes; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllSubreddits; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllThemes; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllUsers; import ml.docilealligator.infinityforreddit.asynctasks.RestoreSettings; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.readpost.ReadPostDao; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; /** * A simple {@link Fragment} subclass. */ public class AdvancedPreferenceFragment extends CustomFontPreferenceFragmentCompat { private static final int SELECT_BACKUP_SETTINGS_DIRECTORY_REQUEST_CODE = 1; private static final int SELECT_RESTORE_SETTINGS_DIRECTORY_REQUEST_CODE = 2; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject @Named("sort_type") SharedPreferences mSortTypeSharedPreferences; @Inject @Named("post_layout") SharedPreferences mPostLayoutSharedPreferences; @Inject @Named("post_details") SharedPreferences mPostDetailsSharedPreferences; @Inject @Named("light_theme") SharedPreferences lightThemeSharedPreferences; @Inject @Named("dark_theme") SharedPreferences darkThemeSharedPreferences; @Inject @Named("post_feed_scrolled_position_cache") SharedPreferences postFeedScrolledPositionSharedPreferences; @Inject @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences; @Inject @Named("main_activity_tabs") SharedPreferences mainActivityTabsSharedPreferences; @Inject @Named("proxy") SharedPreferences proxySharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences nsfwAndBlurringSharedPreferences; @Inject @Named("bottom_app_bar") SharedPreferences bottomAppBarSharedPreferences; @Inject @Named("post_history") SharedPreferences postHistorySharedPreferences; @Inject @Named("navigation_drawer") SharedPreferences navigationDrawerSharedPreferences; @Inject Executor executor; private Handler handler; private String backupPassword; private String restorePassword; private Uri restoreFileUri; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.advanced_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); Preference deleteSubredditsPreference = findPreference(SharedPreferencesUtils.DELETE_ALL_SUBREDDITS_DATA_IN_DATABASE); Preference deleteUsersPreference = findPreference(SharedPreferencesUtils.DELETE_ALL_USERS_DATA_IN_DATABASE); Preference deleteSortTypePreference = findPreference(SharedPreferencesUtils.DELETE_ALL_SORT_TYPE_DATA_IN_DATABASE); Preference deletePostLaoutPreference = findPreference(SharedPreferencesUtils.DELETE_ALL_POST_LAYOUT_DATA_IN_DATABASE); Preference deleteAllThemesPreference = findPreference(SharedPreferencesUtils.DELETE_ALL_THEMES_IN_DATABASE); Preference deletePostFeedScrolledPositionsPreference = findPreference(SharedPreferencesUtils.DELETE_FRONT_PAGE_SCROLLED_POSITIONS_IN_DATABASE); Preference deleteReadPostsPreference = findPreference(SharedPreferencesUtils.DELETE_READ_POSTS_IN_DATABASE); Preference deleteAllLegacySettingsPreference = findPreference(SharedPreferencesUtils.DELETE_ALL_LEGACY_SETTINGS); Preference resetAllSettingsPreference = findPreference(SharedPreferencesUtils.RESET_ALL_SETTINGS); Preference backupSettingsPreference = findPreference(SharedPreferencesUtils.BACKUP_SETTINGS); Preference restoreSettingsPreference = findPreference(SharedPreferencesUtils.RESTORE_SETTINGS); handler = new Handler(Looper.getMainLooper()); if (deleteSubredditsPreference != null) { deleteSubredditsPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> DeleteAllSubreddits.deleteAllSubreddits(executor, handler, mRedditDataRoomDatabase, () -> Toast.makeText(mActivity, R.string.delete_all_subreddits_success, Toast.LENGTH_SHORT).show())) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (deleteUsersPreference != null) { deleteUsersPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> DeleteAllUsers.deleteAllUsers(executor, handler, mRedditDataRoomDatabase, () -> Toast.makeText(mActivity, R.string.delete_all_users_success, Toast.LENGTH_SHORT).show())) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (deleteSortTypePreference != null) { deleteSortTypePreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> DeleteAllSortTypes.deleteAllSortTypes(executor, handler, mSharedPreferences, mSortTypeSharedPreferences, () -> { Toast.makeText(mActivity, R.string.delete_all_sort_types_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new RecreateActivityEvent()); })) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (deletePostLaoutPreference != null) { deletePostLaoutPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> DeleteAllPostLayouts.deleteAllPostLayouts(executor, handler, mSharedPreferences, mPostLayoutSharedPreferences, () -> { Toast.makeText(mActivity, R.string.delete_all_post_layouts_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new RecreateActivityEvent()); })) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (deleteAllThemesPreference != null) { deleteAllThemesPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> DeleteAllThemes.deleteAllThemes(executor, handler, mRedditDataRoomDatabase, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, () -> { Toast.makeText(mActivity, R.string.delete_all_themes_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new RecreateActivityEvent()); })) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (deletePostFeedScrolledPositionsPreference != null) { deletePostFeedScrolledPositionsPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> { postFeedScrolledPositionSharedPreferences.edit().clear().apply(); Toast.makeText(mActivity, R.string.delete_all_front_page_scrolled_positions_success, Toast.LENGTH_SHORT).show(); }) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (deleteReadPostsPreference != null) { executor.execute(() -> { ReadPostDao readPostDao = mRedditDataRoomDatabase.readPostDao(); int tableCount = readPostDao.getReadPostsCount(mActivity.accountName); long tableEntrySize = readPostDao.getMaxReadPostEntrySize(); long tableSize = tableEntrySize * tableCount / 1024; handler.post(() -> deleteReadPostsPreference.setSummary(getString(R.string.settings_read_posts_db_summary, tableSize, tableCount))); }); deleteReadPostsPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> DeleteAllReadPosts.deleteAllReadPosts(executor, handler, mRedditDataRoomDatabase, () -> { Toast.makeText(mActivity, R.string.delete_all_read_posts_success, Toast.LENGTH_SHORT).show(); })) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (deleteAllLegacySettingsPreference != null) { deleteAllLegacySettingsPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> { SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_1_TITLE_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_2_TITLE_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_3_TITLE_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_1_POST_TYPE_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_2_POST_TYPE_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_3_POST_TYPE_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_1_NAME_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_2_NAME_LEGACY); editor.remove(SharedPreferencesUtils.MAIN_PAGE_TAB_3_NAME_LEGACY); editor.remove(SharedPreferencesUtils.NSFW_KEY_LEGACY); editor.remove(SharedPreferencesUtils.BLUR_NSFW_KEY_LEGACY); editor.remove(SharedPreferencesUtils.BLUR_SPOILER_KEY_LEGACY); editor.remove(SharedPreferencesUtils.CONFIRM_TO_EXIT_LEGACY); editor.remove(SharedPreferencesUtils.OPEN_LINK_IN_APP_LEGACY); editor.remove(SharedPreferencesUtils.AUTOMATICALLY_TRY_REDGIFS_LEGACY); editor.remove(SharedPreferencesUtils.DO_NOT_SHOW_REDDIT_API_INFO_AGAIN_LEGACY); editor.remove(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_AWARDS_LEGACY); editor.remove(SharedPreferencesUtils.HIDE_COMMENT_AWARDS_LEGACY); editor.remove(SharedPreferencesUtils.IMMERSIVE_INTERFACE_IGNORE_NAV_BAR_KEY_LEGACY); SharedPreferences.Editor sortTypeEditor = mSortTypeSharedPreferences.edit(); sortTypeEditor.remove(SharedPreferencesUtils.SORT_TYPE_ALL_POST_LEGACY); sortTypeEditor.remove(SharedPreferencesUtils.SORT_TIME_ALL_POST_LEGACY); sortTypeEditor.remove(SharedPreferencesUtils.SORT_TYPE_POPULAR_POST_LEGACY); sortTypeEditor.remove(SharedPreferencesUtils.SORT_TIME_POPULAR_POST_LEGACY); SharedPreferences.Editor postLayoutEditor = mPostLayoutSharedPreferences.edit(); postLayoutEditor.remove(SharedPreferencesUtils.POST_LAYOUT_ALL_POST_LEGACY); postLayoutEditor.remove(SharedPreferencesUtils.POST_LAYOUT_POPULAR_POST_LEGACY); SharedPreferences.Editor currentAccountEditor = mCurrentAccountSharedPreferences.edit(); currentAccountEditor.remove(SharedPreferencesUtils.APPLICATION_ONLY_ACCESS_TOKEN_LEGACY); editor.apply(); sortTypeEditor.apply(); postLayoutEditor.apply(); currentAccountEditor.apply(); Toast.makeText(mActivity, R.string.delete_all_legacy_settings_success, Toast.LENGTH_SHORT).show(); }) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (resetAllSettingsPreference != null) { resetAllSettingsPreference.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialogInterface, i) -> { boolean disableNsfwForever = mSharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false); mSharedPreferences.edit().clear().apply(); mainActivityTabsSharedPreferences.edit().clear().apply(); nsfwAndBlurringSharedPreferences.edit().clear().apply(); if (disableNsfwForever) { mSharedPreferences.edit().putBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, true).apply(); } Toast.makeText(mActivity, R.string.reset_all_settings_success, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new RecreateActivityEvent()); }) .setNegativeButton(R.string.no, null) .show(); return true; }); } if (backupSettingsPreference != null) { backupSettingsPreference.setOnPreferenceClickListener(preference -> { showPasswordDialog(); return true; }); } if (restoreSettingsPreference != null) { restoreSettingsPreference.setOnPreferenceClickListener(preference -> { Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT); chooseFile.setType("application/zip"); chooseFile = Intent.createChooser(chooseFile, "Choose a backup file"); startActivityForResult(chooseFile, SELECT_RESTORE_SETTINGS_DIRECTORY_REQUEST_CODE); return true; }); } } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (resultCode == RESULT_OK) { if (requestCode == SELECT_BACKUP_SETTINGS_DIRECTORY_REQUEST_CODE) { Uri uri = data.getData(); BackupSettings.backupSettings(mActivity, executor, handler, mActivity.getContentResolver(), uri, backupPassword, mRedditDataRoomDatabase, mSharedPreferences, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, mSortTypeSharedPreferences, mPostLayoutSharedPreferences, mPostDetailsSharedPreferences, postFeedScrolledPositionSharedPreferences, mainActivityTabsSharedPreferences, proxySharedPreferences, nsfwAndBlurringSharedPreferences, bottomAppBarSharedPreferences, postHistorySharedPreferences, navigationDrawerSharedPreferences, new BackupSettings.BackupSettingsListener() { @Override public void success() { Toast.makeText(mActivity, R.string.backup_settings_success, Toast.LENGTH_LONG).show(); // Clear the password from memory after use backupPassword = null; } @Override public void failed(String errorMessage) { Toast.makeText(mActivity, errorMessage, Toast.LENGTH_LONG).show(); // Clear the password from memory after use backupPassword = null; } }); } else if (requestCode == SELECT_RESTORE_SETTINGS_DIRECTORY_REQUEST_CODE) { restoreFileUri = data.getData(); showRestorePasswordDialog(); } } } private void showPasswordDialog() { EditText passwordEditText = new EditText(mActivity); passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); passwordEditText.setHint(R.string.enter_backup_password); CheckBox showPasswordCheckBox = new CheckBox(mActivity); showPasswordCheckBox.setText(R.string.show_password); showPasswordCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } else { passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } passwordEditText.setSelection(passwordEditText.getText().length()); }); LinearLayout layout = new LinearLayout(mActivity); layout.setOrientation(LinearLayout.VERTICAL); int padding = (int) (16 * getResources().getDisplayMetrics().density); // 16dp layout.setPadding(padding, padding, padding, padding); layout.addView(passwordEditText); layout.addView(showPasswordCheckBox); MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.backup_password_dialog_title) .setMessage(R.string.backup_password_dialog_message) .setView(layout) .setPositiveButton(R.string.ok, (dialog, which) -> { String password = passwordEditText.getText().toString().trim(); // Password length validation is now handled by enabling/disabling the button backupPassword = password; Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, SELECT_BACKUP_SETTINGS_DIRECTORY_REQUEST_CODE); }) .setNegativeButton(R.string.cancel, null); androidx.appcompat.app.AlertDialog dialog = dialogBuilder.create(); dialog.show(); // Initially disable the OK button dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setEnabled(false); passwordEditText.addTextChangedListener(new android.text.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(android.text.Editable s) { String password = s.toString().trim(); boolean isValid = password.length() >= 6 && password.length() <= 32; dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setEnabled(isValid); if (!isValid && password.length() > 0) { // Show error only if user has typed something and it's invalid if (password.length() < 6) { passwordEditText.setError(getString(R.string.password_too_short_error, 6)); } else if (password.length() > 32) { passwordEditText.setError(getString(R.string.password_too_long_error, 32)); } } else { passwordEditText.setError(null); // Clear error when valid or empty } } }); } private void showRestorePasswordDialog() { EditText passwordEditText = new EditText(mActivity); passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); passwordEditText.setHint(R.string.enter_restore_password); CheckBox showPasswordCheckBox = new CheckBox(mActivity); showPasswordCheckBox.setText(R.string.show_password); showPasswordCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } else { passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } passwordEditText.setSelection(passwordEditText.getText().length()); }); LinearLayout layout = new LinearLayout(mActivity); layout.setOrientation(LinearLayout.VERTICAL); int padding = (int) (16 * getResources().getDisplayMetrics().density); // 16dp layout.setPadding(padding, padding, padding, padding); layout.addView(passwordEditText); layout.addView(showPasswordCheckBox); MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.restore_password_dialog_title) .setMessage(R.string.restore_password_dialog_message) .setView(layout) .setPositiveButton(R.string.ok, (dialog, which) -> { String password = passwordEditText.getText().toString().trim(); // Password length validation is now handled by enabling/disabling the button restorePassword = password; performRestore(); }) .setNegativeButton(R.string.cancel, null); androidx.appcompat.app.AlertDialog dialog = dialogBuilder.create(); dialog.show(); // Initially disable the OK button dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setEnabled(false); passwordEditText.addTextChangedListener(new android.text.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(android.text.Editable s) { String password = s.toString().trim(); boolean isValid = password.length() >= 6 && password.length() <= 32; dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setEnabled(isValid); if (!isValid && password.length() > 0) { // Show error only if user has typed something and it's invalid if (password.length() < 6) { passwordEditText.setError(getString(R.string.password_too_short_error, 6)); } else if (password.length() > 32) { passwordEditText.setError(getString(R.string.password_too_long_error, 32)); } } else { passwordEditText.setError(null); // Clear error when valid or empty } } }); } private void performRestore() { RestoreSettings.restoreSettings(mActivity, executor, handler, mActivity.getContentResolver(), restoreFileUri, restorePassword, mRedditDataRoomDatabase, mSharedPreferences, mCurrentAccountSharedPreferences, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, mSortTypeSharedPreferences, mPostLayoutSharedPreferences, mPostDetailsSharedPreferences, postFeedScrolledPositionSharedPreferences, mainActivityTabsSharedPreferences, proxySharedPreferences, nsfwAndBlurringSharedPreferences, bottomAppBarSharedPreferences, postHistorySharedPreferences, navigationDrawerSharedPreferences, new RestoreSettings.RestoreSettingsListener() { @Override public void success() { Toast.makeText(mActivity, R.string.restore_settings_success, Toast.LENGTH_LONG).show(); // Clear the password from memory after use restorePassword = null; restoreFileUri = null; } @Override public void failed(String errorMessage) { Toast.makeText(mActivity, errorMessage, Toast.LENGTH_LONG).show(); // Clear the password from memory after use restorePassword = null; restoreFileUri = null; } @Override public void failedWithWrongPassword(String errorMessage) { Toast.makeText(mActivity, errorMessage, Toast.LENGTH_LONG).show(); // Don't clear restoreFileUri so it can be reused restorePassword = null; showRestorePasswordDialog(); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/CommentPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.SharedPreferences; import android.os.Bundle; import androidx.preference.ListPreference; import androidx.preference.SwitchPreference; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.customviews.preference.SliderPreference; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class CommentPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("default") SharedPreferences sharedPreferences; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.comment_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); SwitchPreference showCommentDividerSwitchPreference = findPreference(SharedPreferencesUtils.SHOW_COMMENT_DIVIDER); ListPreference commentDividerTypeListPreference = findPreference(SharedPreferencesUtils.COMMENT_DIVIDER_TYPE); SliderPreference showFewerToolbarOptionsThresholdSliderPreference = findPreference(SharedPreferencesUtils.SHOW_FEWER_TOOLBAR_OPTIONS_THRESHOLD); if (showCommentDividerSwitchPreference != null && commentDividerTypeListPreference != null) { commentDividerTypeListPreference.setVisible(sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_COMMENT_DIVIDER, false)); showCommentDividerSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { commentDividerTypeListPreference.setVisible((Boolean) newValue); return true; }); } if (showFewerToolbarOptionsThresholdSliderPreference != null) { showFewerToolbarOptionsThresholdSliderPreference.setSummaryTemplate(R.string.settings_show_fewer_toolbar_options_threshold_summary); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/CrashReportsFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.view.menu.MenuItemImpl; import androidx.core.graphics.Insets; import androidx.core.view.MenuItemCompat; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import com.crazylegend.crashyreporter.CrashyReporter; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; import javax.inject.Inject; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.adapters.CrashReportsRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.utils.Utils; public class CrashReportsFragment extends Fragment { @Inject CustomThemeWrapper mCustomThemeWrapper; private SettingsActivity mActivity; public CrashReportsFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment RecyclerView recyclerView = (RecyclerView) inflater.inflate(R.layout.fragment_crash_reports, container, false); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(recyclerView, new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); recyclerView.setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); setHasOptionsMenu(true); recyclerView.setAdapter(new CrashReportsRecyclerViewAdapter(mActivity, CrashyReporter.INSTANCE.getLogsAsStrings())); recyclerView.setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); return recyclerView; } @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.crash_reports_fragment, menu); applyMenuItemTheme(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_delete_logs_crash_reports_fragment) { CrashyReporter.INSTANCE.purgeLogs(); Toast.makeText(mActivity, R.string.crash_reports_deleted, Toast.LENGTH_SHORT).show(); return true; } else if (item.getItemId() == R.id.action_export_logs_crash_reports_fragment) { return createGithubIssueWithLogs(); } return false; } /** * Fetch the logs from CrashyReporter and open browser to create GitHub issue page. * Issue will have logs, device model, app version, and Android version prefilled. * @return if successful */ private boolean createGithubIssueWithLogs() { Intent intent = new Intent(getContext(), LinkResolverActivity.class); String logs, model, appVersion, androidVersion; try { List logLines = CrashyReporter.INSTANCE.getLogsAsStrings(); if (logLines == null) { return false; } logs = String.join("\n", logLines); // limit size to 6800 characters to avoid `414 URI Too Long` logs = URLEncoder.encode("```\n" + (logs.length() > 0 ? logs.substring(0, Math.min(6800, logs.length())) : "No logs found.") + "\n```", "UTF-8"); model = URLEncoder.encode(Build.MANUFACTURER + " " + Build.MODEL, "UTF-8"); appVersion = URLEncoder.encode(BuildConfig.VERSION_NAME, "UTF-8"); androidVersion = URLEncoder.encode(Build.VERSION.RELEASE, "UTF-8"); } catch (UnsupportedEncodingException e) { return false; } Uri githubIssueUri = Uri.parse(String.format("https://github.com/Docile-Alligator/Infinity-For-Reddit/issues/new?labels=possible-bug&device=%s&version=%s&android_version=%s&logs=%s&&template=BUG_REPORT.yml", model, appVersion, androidVersion, logs)); intent.setData(githubIssueUri); startActivity(intent); return true; } @SuppressLint("RestrictedApi") protected boolean applyMenuItemTheme(Menu menu) { if (mCustomThemeWrapper != null) { int size = Math.min(menu.size(), 2); for (int i = 0; i < size; i++) { MenuItem item = menu.getItem(i); if (((MenuItemImpl) item).requestsActionButton()) { MenuItemCompat.setIconTintList(item, ColorStateList .valueOf(mCustomThemeWrapper.getToolbarPrimaryTextAndIconColor())); } Utils.setTitleWithCustomFontToMenuItem(mActivity.typeface, item, null); } } return true; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/CreditsPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; /** * A simple {@link PreferenceFragmentCompat} subclass. */ public class CreditsPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.credits_preferences, rootKey); Preference iconForegroundPreference = findPreference(SharedPreferencesUtils.ICON_FOREGROUND_KEY); Preference iconBackgroundPreference = findPreference(SharedPreferencesUtils.ICON_BACKGROUND_KEY); Preference errorImagePreference = findPreference(SharedPreferencesUtils.ERROR_IMAGE_KEY); Preference crosspostIconPreference = findPreference(SharedPreferencesUtils.CROSSPOST_ICON_KEY); Preference thumbtackIconPreference = findPreference(SharedPreferencesUtils.THUMBTACK_ICON_KEY); Preference bestRocketIconPreference = findPreference(SharedPreferencesUtils.BEST_ROCKET_ICON_KEY); Preference materialIconsPreference = findPreference(SharedPreferencesUtils.MATERIAL_ICONS_KEY); Preference nationalFlagsPreference = findPreference(SharedPreferencesUtils.NATIONAL_FLAGS); Preference ufoAndCowPreference = findPreference(SharedPreferencesUtils.UFO_CAPTURING_ANIMATION); Preference loveAnimationPreference = findPreference(SharedPreferencesUtils.LOVE_ANIMATION); Preference lockScreenPreference = findPreference(SharedPreferencesUtils.LOCK_SCREEN_ANIMATION); if (iconForegroundPreference != null) { iconForegroundPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.freepik.com/free-photos-vectors/technology")); startActivity(intent); return true; }); } if (iconBackgroundPreference != null) { iconBackgroundPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.freepik.com/free-photos-vectors/background")); startActivity(intent); return true; }); } if (errorImagePreference != null) { errorImagePreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.freepik.com/free-photos-vectors/technology")); startActivity(intent); return true; }); } if (crosspostIconPreference != null) { crosspostIconPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.flaticon.com/free-icon/crossed-arrows_2291")); startActivity(intent); return true; }); } if (thumbtackIconPreference != null) { thumbtackIconPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.flaticon.com/free-icon/tack-save-button_61845#term=thumbtack&page=1&position=3")); startActivity(intent); return true; }); } if (bestRocketIconPreference != null) { bestRocketIconPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.flaticon.com/free-icon/spring-swing-rocket_2929322?term=space%20ship&page=1&position=18")); startActivity(intent); return true; }); } if (materialIconsPreference != null) { materialIconsPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://material.io/resources/icons/")); startActivity(intent); return true; }); } if (nationalFlagsPreference != null) { nationalFlagsPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://www.flaticon.com/packs/countrys-flags")); startActivity(intent); return true; }); } if (ufoAndCowPreference != null) { ufoAndCowPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://lottiefiles.com/33858-ufo-capturing-animation")); startActivity(intent); return true; }); } if (loveAnimationPreference != null) { loveAnimationPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://lottiefiles.com/52103-love")); startActivity(intent); return true; }); } if (lockScreenPreference != null) { lockScreenPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://lottiefiles.com/69178-cool")); startActivity(intent); return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/CustomizeBottomAppBarFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import java.util.Arrays; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.databinding.FragmentCustomizeBottomAppBarBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class CustomizeBottomAppBarFragment extends Fragment { private FragmentCustomizeBottomAppBarBinding binding; @Inject @Named("bottom_app_bar") SharedPreferences sharedPreferences; private SettingsActivity mActivity; private int mainActivityOptionCount; private int mainActivityOption1; private int mainActivityOption2; private int mainActivityOption3; private int mainActivityOption4; private int mainActivityFAB; private int otherActivitiesOptionCount; private int otherActivitiesOption1; private int otherActivitiesOption2; private int otherActivitiesOption3; private int otherActivitiesOption4; private int otherActivitiesFAB; public CustomizeBottomAppBarFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentCustomizeBottomAppBarBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); binding.getRoot().setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); applyCustomTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.getRoot().setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } if (mActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mActivity.typeface); } Resources resources = mActivity.getResources(); String[] mainActivityOptions = resources.getStringArray(R.array.settings_main_activity_bottom_app_bar_options); String[] mainActivityOptionAnonymous = resources.getStringArray(R.array.settings_main_activity_bottom_app_bar_options_anonymous); String[] mainActivityOptionAnonymousValues = resources.getStringArray(R.array.settings_main_activity_bottom_app_bar_options_anonymous_values); String[] fabOptions; mainActivityOptionCount = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_COUNT, 4); mainActivityOption1 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_1, 0); mainActivityOption2 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_2, 1); mainActivityOption3 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_3, 2); mainActivityOption4 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_4, 3); mainActivityFAB = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? 7: 0); mainActivityOption1 = Utils.fixIndexOutOfBounds(mainActivityOptions, mainActivityOption1); mainActivityOption2 = Utils.fixIndexOutOfBounds(mainActivityOptions, mainActivityOption2); mainActivityOption3 = Utils.fixIndexOutOfBounds(mainActivityOptions, mainActivityOption3); mainActivityOption4 = Utils.fixIndexOutOfBounds(mainActivityOptions, mainActivityOption4); // Additional bounds checking to prevent ArrayIndexOutOfBoundsException if (mainActivityOption1 < 0 || mainActivityOption1 >= mainActivityOptions.length) mainActivityOption1 = 0; if (mainActivityOption2 < 0 || mainActivityOption2 >= mainActivityOptions.length) mainActivityOption2 = 0; if (mainActivityOption3 < 0 || mainActivityOption3 >= mainActivityOptions.length) mainActivityOption3 = 0; if (mainActivityOption4 < 0 || mainActivityOption4 >= mainActivityOptions.length) mainActivityOption4 = 0; binding.mainActivityOptionCountTextViewCustomizeBottomAppBarFragment.setText(Integer.toString(mainActivityOptionCount)); binding.mainActivityOption1TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[mainActivityOption1]); binding.mainActivityOption2TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[mainActivityOption2]); binding.mainActivityOption3TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[mainActivityOption3]); binding.mainActivityOption4TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[mainActivityOption4]); if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { fabOptions = resources.getStringArray(R.array.settings_bottom_app_bar_fab_options_anonymous); ArrayList mainActivityOptionAnonymousValuesList = new ArrayList<>(Arrays.asList(mainActivityOptionAnonymousValues)); mainActivityOption1 = mainActivityOptionAnonymousValuesList.indexOf(Integer.toString(mainActivityOption1)); mainActivityOption2 = mainActivityOptionAnonymousValuesList.indexOf(Integer.toString(mainActivityOption2)); mainActivityOption3 = mainActivityOptionAnonymousValuesList.indexOf(Integer.toString(mainActivityOption3)); mainActivityOption4 = mainActivityOptionAnonymousValuesList.indexOf(Integer.toString(mainActivityOption4)); mainActivityFAB = mainActivityFAB >= 9 ? mainActivityFAB - 2 : mainActivityFAB - 1; } else { fabOptions = resources.getStringArray(R.array.settings_bottom_app_bar_fab_options); } // Additional bounds checking for FAB option if (mainActivityFAB < 0 || mainActivityFAB >= fabOptions.length) mainActivityFAB = 0; binding.mainActivityFabTextViewCustomizeBottomAppBarFragment.setText(fabOptions[mainActivityFAB]); binding.mainActivityOptionCountLinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_count) .setSingleChoiceItems(R.array.settings_bottom_app_bar_option_count_options, mainActivityOptionCount / 2 - 1, (dialogInterface, i) -> { mainActivityOptionCount = (i + 1) * 2; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_COUNT, mainActivityOptionCount).apply(); binding.mainActivityOptionCountTextViewCustomizeBottomAppBarFragment.setText(Integer.toString(mainActivityOptionCount)); dialogInterface.dismiss(); }) .show(); }); binding.mainActivityOption1LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_1) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mainActivityOptionAnonymous : mainActivityOptions, mainActivityOption1, (dialogInterface, i) -> { mainActivityOption1 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(mainActivityOptionAnonymousValues[i]) : mainActivityOption1; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_1, optionToSaveToPreference).apply(); binding.mainActivityOption1TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.mainActivityOption2LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_2) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mainActivityOptionAnonymous : mainActivityOptions, mainActivityOption2, (dialogInterface, i) -> { mainActivityOption2 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(mainActivityOptionAnonymousValues[i]) : mainActivityOption2; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_2, optionToSaveToPreference).apply(); binding.mainActivityOption2TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.mainActivityOption3LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_3) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mainActivityOptionAnonymous : mainActivityOptions, mainActivityOption3, (dialogInterface, i) -> { mainActivityOption3 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(mainActivityOptionAnonymousValues[i]) : mainActivityOption3; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_3, optionToSaveToPreference).apply(); binding.mainActivityOption3TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.mainActivityOption4LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_4) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? mainActivityOptionAnonymous : mainActivityOptions, mainActivityOption4, (dialogInterface, i) -> { mainActivityOption4 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(mainActivityOptionAnonymousValues[i]) : mainActivityOption4; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_4, optionToSaveToPreference).apply(); binding.mainActivityOption4TextViewCustomizeBottomAppBarFragment.setText(mainActivityOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.mainActivityFabLinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_fab) .setSingleChoiceItems(fabOptions, mainActivityFAB, (dialogInterface, i) -> { mainActivityFAB = i; int optionToSaveToPreference; if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (i >= 7) { optionToSaveToPreference = i + 2; } else { optionToSaveToPreference = i + 1; } } else { optionToSaveToPreference = i; } sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB, optionToSaveToPreference).apply(); binding.mainActivityFabTextViewCustomizeBottomAppBarFragment.setText(fabOptions[mainActivityFAB]); dialogInterface.dismiss(); }) .show(); }); String[] otherActivitiesOptions = resources.getStringArray(R.array.settings_other_activities_bottom_app_bar_options); String[] otherActivitiesOptionAnonymous = resources.getStringArray(R.array.settings_other_activities_bottom_app_bar_options_anonymous); String[] otherActivitiesOptionAnonymousValues = resources.getStringArray(R.array.settings_other_activities_bottom_app_bar_options_anonymous_values); otherActivitiesOptionCount = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_COUNT, 4); otherActivitiesOption1 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_1, 0); otherActivitiesOption2 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_2, 1); otherActivitiesOption3 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_3, 2); otherActivitiesOption4 = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_4, 3); otherActivitiesFAB = sharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB, mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? 7: 0); otherActivitiesOption1 = Utils.fixIndexOutOfBounds(otherActivitiesOptions, otherActivitiesOption1); otherActivitiesOption2 = Utils.fixIndexOutOfBounds(otherActivitiesOptions, otherActivitiesOption2); otherActivitiesOption3 = Utils.fixIndexOutOfBounds(otherActivitiesOptions, otherActivitiesOption3); otherActivitiesOption4 = Utils.fixIndexOutOfBounds(otherActivitiesOptions, otherActivitiesOption4); // Additional bounds checking to prevent ArrayIndexOutOfBoundsException if (otherActivitiesOption1 < 0 || otherActivitiesOption1 >= otherActivitiesOptions.length) otherActivitiesOption1 = 0; if (otherActivitiesOption2 < 0 || otherActivitiesOption2 >= otherActivitiesOptions.length) otherActivitiesOption2 = 0; if (otherActivitiesOption3 < 0 || otherActivitiesOption3 >= otherActivitiesOptions.length) otherActivitiesOption3 = 0; if (otherActivitiesOption4 < 0 || otherActivitiesOption4 >= otherActivitiesOptions.length) otherActivitiesOption4 = 0; binding.otherActivitiesOptionCountTextViewCustomizeBottomAppBarFragment.setText(Integer.toString(otherActivitiesOptionCount)); binding.otherActivitiesOption1TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[otherActivitiesOption1]); binding.otherActivitiesOption2TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[otherActivitiesOption2]); binding.otherActivitiesOption3TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[otherActivitiesOption3]); binding.otherActivitiesOption4TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[otherActivitiesOption4]); if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { ArrayList otherActivitiesOptionAnonymousValuesList = new ArrayList<>(Arrays.asList(otherActivitiesOptionAnonymousValues)); otherActivitiesOption1 = otherActivitiesOptionAnonymousValuesList.indexOf(Integer.toString(otherActivitiesOption1)); otherActivitiesOption2 = otherActivitiesOptionAnonymousValuesList.indexOf(Integer.toString(otherActivitiesOption2)); otherActivitiesOption3 = otherActivitiesOptionAnonymousValuesList.indexOf(Integer.toString(otherActivitiesOption3)); otherActivitiesOption4 = otherActivitiesOptionAnonymousValuesList.indexOf(Integer.toString(otherActivitiesOption4)); otherActivitiesFAB = otherActivitiesFAB >= 9 ? otherActivitiesFAB - 2 : otherActivitiesFAB - 1; } // Additional bounds checking for FAB option if (otherActivitiesFAB < 0 || otherActivitiesFAB >= fabOptions.length) otherActivitiesFAB = 0; binding.otherActivitiesFabTextViewCustomizeBottomAppBarFragment.setText(fabOptions[otherActivitiesFAB]); binding.otherActivitiesOptionCountLinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_count) .setSingleChoiceItems(R.array.settings_bottom_app_bar_option_count_options, otherActivitiesOptionCount / 2 - 1, (dialogInterface, i) -> { otherActivitiesOptionCount = (i + 1) * 2; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_COUNT, otherActivitiesOptionCount).apply(); binding.otherActivitiesOptionCountTextViewCustomizeBottomAppBarFragment.setText(Integer.toString(otherActivitiesOptionCount)); dialogInterface.dismiss(); }) .show(); }); binding.otherActivitiesOption1LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_1) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? otherActivitiesOptionAnonymous : otherActivitiesOptions, otherActivitiesOption1, (dialogInterface, i) -> { otherActivitiesOption1 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(otherActivitiesOptionAnonymousValues[i]) : otherActivitiesOption1; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_1, optionToSaveToPreference).apply(); binding.otherActivitiesOption1TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.otherActivitiesOption2LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_2) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? otherActivitiesOptionAnonymous : otherActivitiesOptions, otherActivitiesOption2, (dialogInterface, i) -> { otherActivitiesOption2 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(otherActivitiesOptionAnonymousValues[i]) : otherActivitiesOption2; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_2, optionToSaveToPreference).apply(); binding.otherActivitiesOption2TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.otherActivitiesOption3LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_3) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? otherActivitiesOptionAnonymous : otherActivitiesOptions, otherActivitiesOption3, (dialogInterface, i) -> { otherActivitiesOption3 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(otherActivitiesOptionAnonymousValues[i]) : otherActivitiesOption3; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_3, optionToSaveToPreference).apply(); binding.otherActivitiesOption3TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.otherActivitiesOption4LinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_option_4) .setSingleChoiceItems(mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? otherActivitiesOptionAnonymous : otherActivitiesOptions, otherActivitiesOption4, (dialogInterface, i) -> { otherActivitiesOption4 = i; int optionToSaveToPreference = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Integer.parseInt(otherActivitiesOptionAnonymousValues[i]) : otherActivitiesOption4; sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_4, optionToSaveToPreference).apply(); binding.otherActivitiesOption4TextViewCustomizeBottomAppBarFragment.setText(otherActivitiesOptions[optionToSaveToPreference]); dialogInterface.dismiss(); }) .show(); }); binding.otherActivitiesFabLinearLayoutCustomizeBottomAppBarFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_bottom_app_bar_fab) .setSingleChoiceItems(fabOptions, otherActivitiesFAB, (dialogInterface, i) -> { otherActivitiesFAB = i; int optionToSaveToPreference; if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (i >= 7) { optionToSaveToPreference = i + 2; } else { optionToSaveToPreference = i + 1; } } else { optionToSaveToPreference = i; } sharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? Account.ANONYMOUS_ACCOUNT : "") + SharedPreferencesUtils.OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB, optionToSaveToPreference).apply(); binding.otherActivitiesFabTextViewCustomizeBottomAppBarFragment.setText(fabOptions[otherActivitiesFAB]); dialogInterface.dismiss(); }) .show(); }); return binding.getRoot(); } private void applyCustomTheme() { int primaryTextColor = mActivity.customThemeWrapper.getPrimaryTextColor(); int secondaryTextColor = mActivity.customThemeWrapper.getSecondaryTextColor(); int accentColor = mActivity.customThemeWrapper.getColorAccent(); binding.infoTextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); Drawable infoDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_info_preference_day_night_24dp, mActivity.customThemeWrapper.getPrimaryIconColor()); binding.infoTextViewCustomizeBottomAppBarFragment.setCompoundDrawablesWithIntrinsicBounds(infoDrawable, null, null, null); binding.mainActivityGroupSummaryCustomizeBottomAppBarFragment.setTextColor(accentColor); binding.mainActivityOptionCountTitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.mainActivityOptionCountTextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.mainActivityOption1TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.mainActivityOption1TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.mainActivityOption2TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.mainActivityOption2TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.mainActivityOption3TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.mainActivityOption3TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.mainActivityOption4TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.mainActivityOption4TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.mainActivityFabTitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.mainActivityFabTextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.otherActivitiesGroupSummaryCustomizeBottomAppBarFragment.setTextColor(accentColor); binding.otherActivitiesOptionCountTitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.otherActivitiesOptionCountTextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.otherActivitiesOption1TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.otherActivitiesOption1TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.otherActivitiesOption2TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.otherActivitiesOption2TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.otherActivitiesOption3TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.otherActivitiesOption3TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.otherActivitiesOption4TitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.otherActivitiesOption4TextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); binding.otherActivitiesFabTitleTextViewCustomizeBottomAppBarFragment.setTextColor(primaryTextColor); binding.otherActivitiesFabTextViewCustomizeBottomAppBarFragment.setTextColor(secondaryTextColor); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/CustomizeMainPageTabsFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Constants; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.SelectThingReturnKey; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.SearchActivity; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.activities.SubscribedThingListingActivity; import ml.docilealligator.infinityforreddit.databinding.FragmentCustomizeMainPageTabsBinding; import ml.docilealligator.infinityforreddit.multireddit.MultiReddit; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import ml.docilealligator.infinityforreddit.utils.AppRestartHelper; public class CustomizeMainPageTabsFragment extends Fragment { private FragmentCustomizeMainPageTabsBinding binding; @Inject @Named("main_activity_tabs") SharedPreferences mainActivityTabsSharedPreferences; private SettingsActivity mActivity; private int tabCount; private String tab1CurrentTitle; private int tab1CurrentPostType; private String tab1CurrentName; private String tab2CurrentTitle; private int tab2CurrentPostType; private String tab2CurrentName; private String tab3CurrentTitle; private int tab3CurrentPostType; private String tab3CurrentName; private String tab4CurrentTitle; private int tab4CurrentPostType; private String tab4CurrentName; private String tab5CurrentTitle; private int tab5CurrentPostType; private String tab5CurrentName; private String tab6CurrentTitle; private int tab6CurrentPostType; private String tab6CurrentName; private Button restartButton; private boolean mSettingsChanged = false; public CustomizeMainPageTabsFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentCustomizeMainPageTabsBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); // Initialize the restart button restartButton = binding.getRoot().findViewById(R.id.restart_button_customize_main_page_tabs); if (restartButton != null) { restartButton.setOnClickListener(v -> { if (mActivity != null) { AppRestartHelper.triggerAppRestart(mActivity); mSettingsChanged = false; // Reset flag updateRestartButtonVisibility(); // Hide button } }); } updateRestartButtonVisibility(); // Set initial visibility (should be hidden) binding.getRoot().setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); applyCustomTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.getRoot().setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } if (mActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mActivity.typeface); } String[] typeValues; if (mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { typeValues = mActivity.getResources().getStringArray(R.array.settings_tab_post_type_anonymous); } else { typeValues = mActivity.getResources().getStringArray(R.array.settings_tab_post_type); } tabCount = mainActivityTabsSharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_COUNT, Constants.DEFAULT_TAB_COUNT); binding.tabCountTextViewCustomizeMainPageTabsFragment.setText(Integer.toString(tabCount)); binding.tabCountLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_count) .setSingleChoiceItems(R.array.settings_main_page_tab_count, tabCount - 1, (dialogInterface, i) -> { tabCount = i + 1; mainActivityTabsSharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_COUNT, tabCount).apply(); binding.tabCountTextViewCustomizeMainPageTabsFragment.setText(Integer.toString(tabCount)); updateTabViewsVisibility(tabCount); mSettingsChanged = true; updateRestartButtonVisibility(); dialogInterface.dismiss(); }) .show(); }); boolean showTabNames = mainActivityTabsSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_TAB_NAMES, true); binding.showTabNamesSwitchMaterialCustomizeMainPageTabsFragment.setChecked(showTabNames); binding.showTabNamesSwitchMaterialCustomizeMainPageTabsFragment.setOnCheckedChangeListener((compoundButton, b) -> { mainActivityTabsSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_TAB_NAMES, b).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); }); binding.showTabNamesLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> binding.showTabNamesSwitchMaterialCustomizeMainPageTabsFragment.performClick()); tab1CurrentTitle = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_TITLE, getString(R.string.home)); tab1CurrentPostType = mainActivityTabsSharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_POST_TYPE, SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_HOME); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { tab1CurrentPostType = Utils.fixIndexOutOfBoundsUsingPredetermined(typeValues, tab1CurrentPostType, 1); } tab1CurrentName = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_NAME, ""); binding.tab1TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[tab1CurrentPostType]); binding.tab1TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab1CurrentTitle); binding.tab1NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab1CurrentName); applyTab1NameView(binding.tab1NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab1NameTitleTextViewCustomizeMainPageTabsFragment, tab1CurrentPostType); View dialogView = mActivity.getLayoutInflater().inflate(R.layout.dialog_edit_text, null); EditText editText = dialogView.findViewById(R.id.edit_text_edit_text_dialog); binding.tab1TitleLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { editText.setHint(R.string.settings_tab_title); editText.setText(tab1CurrentTitle); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab1CurrentTitle = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_TITLE, tab1CurrentTitle).apply(); binding.tab1TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab1CurrentTitle); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab1TypeLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setSingleChoiceItems(typeValues, tab1CurrentPostType, (dialogInterface, i) -> { tab1CurrentPostType = i; mainActivityTabsSharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_POST_TYPE, i).apply(); binding.tab1TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[i]); applyTab1NameView(binding.tab1NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab1NameTitleTextViewCustomizeMainPageTabsFragment, i); mSettingsChanged = true; updateRestartButtonVisibility(); dialogInterface.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); }); binding.tab1NameConstraintLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { int titleId; switch (tab1CurrentPostType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: titleId = R.string.settings_tab_subreddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: titleId = R.string.settings_tab_multi_reddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: titleId = R.string.settings_tab_username; break; default: return; } editText.setText(tab1CurrentName); editText.setHint(titleId); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(titleId) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab1CurrentName = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_NAME, tab1CurrentName).apply(); binding.tab1NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab1CurrentName); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab1NameAddImageViewCustomizeMainPageTabsFragment.setOnClickListener(view -> selectName(0)); tab2CurrentTitle = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_TITLE, getString(R.string.popular)); tab2CurrentPostType = mainActivityTabsSharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_POST_TYPE, SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_POPULAR); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { tab2CurrentPostType = Utils.fixIndexOutOfBoundsUsingPredetermined(typeValues, tab2CurrentPostType, 1); } tab2CurrentName = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_NAME, ""); binding.tab2TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[tab2CurrentPostType]); binding.tab2TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab2CurrentTitle); binding.tab2NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab2CurrentName); applyTab2NameView(binding.tab2NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab2NameTitleTextViewCustomizeMainPageTabsFragment, tab2CurrentPostType); binding.tab2TitleLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { editText.setHint(R.string.settings_tab_title); editText.setText(tab2CurrentTitle); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab2CurrentTitle = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_TITLE, tab2CurrentTitle).apply(); binding.tab2TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab2CurrentTitle); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab2TypeLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setSingleChoiceItems(typeValues, tab2CurrentPostType, (dialogInterface, i) -> { tab2CurrentPostType = i; mainActivityTabsSharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_POST_TYPE, i).apply(); binding.tab2TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[i]); applyTab2NameView(binding.tab2NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab2NameTitleTextViewCustomizeMainPageTabsFragment, i); mSettingsChanged = true; updateRestartButtonVisibility(); dialogInterface.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); }); binding.tab2NameConstraintLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { int titleId; switch (tab2CurrentPostType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: titleId = R.string.settings_tab_subreddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: titleId = R.string.settings_tab_multi_reddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: titleId = R.string.settings_tab_username; break; default: return; } editText.setText(tab2CurrentName); editText.setHint(titleId); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(titleId) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab2CurrentName = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_NAME, tab2CurrentName).apply(); binding.tab2NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab2CurrentName); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab2NameAddImageViewCustomizeMainPageTabsFragment.setOnClickListener(view -> selectName(1)); tab3CurrentTitle = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_TITLE, getString(R.string.all)); tab3CurrentPostType = mainActivityTabsSharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_POST_TYPE, SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { tab3CurrentPostType = Utils.fixIndexOutOfBoundsUsingPredetermined(typeValues, tab3CurrentPostType, 1); } tab3CurrentName = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_NAME, ""); binding.tab3TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[tab3CurrentPostType]); binding.tab3TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab3CurrentTitle); binding.tab3NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab3CurrentName); applyTab3NameView(binding.tab3NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab3NameTitleTextViewCustomizeMainPageTabsFragment, tab3CurrentPostType); binding.tab3TitleLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { editText.setHint(R.string.settings_tab_title); editText.setText(tab3CurrentTitle); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab3CurrentTitle = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_TITLE, tab3CurrentTitle).apply(); binding.tab3TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab3CurrentTitle); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab3TypeLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setSingleChoiceItems(typeValues, tab3CurrentPostType, (dialogInterface, i) -> { tab3CurrentPostType = i; mainActivityTabsSharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_POST_TYPE, i).apply(); binding.tab3TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[i]); applyTab3NameView(binding.tab3NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab3NameTitleTextViewCustomizeMainPageTabsFragment, i); mSettingsChanged = true; updateRestartButtonVisibility(); dialogInterface.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); }); binding.tab3NameConstraintLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { int titleId; switch (tab3CurrentPostType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: titleId = R.string.settings_tab_subreddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: titleId = R.string.settings_tab_multi_reddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: titleId = R.string.settings_tab_username; break; default: return; } editText.setText(tab3CurrentName); editText.setHint(titleId); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(titleId) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab3CurrentName = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_NAME, tab3CurrentName).apply(); binding.tab3NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab3CurrentName); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab3NameAddImageViewCustomizeMainPageTabsFragment.setOnClickListener(view -> selectName(2)); tab4CurrentTitle = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_4_TITLE, getString(R.string.upvoted)); tab4CurrentPostType = mainActivityTabsSharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_4_POST_TYPE, SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { tab4CurrentPostType = Utils.fixIndexOutOfBoundsUsingPredetermined(typeValues, tab4CurrentPostType, 1); } tab4CurrentName = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_4_NAME, ""); binding.tab4TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[tab4CurrentPostType]); binding.tab4TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab4CurrentTitle); binding.tab4NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab4CurrentName); applyTab4NameView(binding.tab4NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab4NameTitleTextViewCustomizeMainPageTabsFragment, tab4CurrentPostType); binding.tab4TitleLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { editText.setHint(R.string.settings_tab_title); editText.setText(tab4CurrentTitle); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab4CurrentTitle = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_4_TITLE, tab4CurrentTitle).apply(); binding.tab4TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab4CurrentTitle); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab4TypeLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setSingleChoiceItems(typeValues, tab4CurrentPostType, (dialogInterface, i) -> { tab4CurrentPostType = i; mainActivityTabsSharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_4_POST_TYPE, i).apply(); binding.tab4TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[i]); applyTab4NameView(binding.tab4NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab4NameTitleTextViewCustomizeMainPageTabsFragment, i); mSettingsChanged = true; updateRestartButtonVisibility(); dialogInterface.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); }); binding.tab4NameConstraintLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { int titleId; switch (tab4CurrentPostType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: titleId = R.string.settings_tab_subreddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: titleId = R.string.settings_tab_multi_reddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: titleId = R.string.settings_tab_username; break; default: return; } editText.setText(tab4CurrentName); editText.setHint(titleId); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(titleId) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab4CurrentName = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_4_NAME, tab4CurrentName).apply(); binding.tab4NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab4CurrentName); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab4NameAddImageViewCustomizeMainPageTabsFragment.setOnClickListener(view -> selectName(3)); tab5CurrentTitle = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_5_TITLE, getString(R.string.downvoted)); tab5CurrentPostType = mainActivityTabsSharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_5_POST_TYPE, SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { tab5CurrentPostType = Utils.fixIndexOutOfBoundsUsingPredetermined(typeValues, tab5CurrentPostType, 1); } tab5CurrentName = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_5_NAME, ""); binding.tab5TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[tab5CurrentPostType]); binding.tab5TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab5CurrentTitle); binding.tab5NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab5CurrentName); applyTab5NameView(binding.tab5NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab5NameTitleTextViewCustomizeMainPageTabsFragment, tab5CurrentPostType); binding.tab5TitleLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { editText.setHint(R.string.settings_tab_title); editText.setText(tab5CurrentTitle); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab5CurrentTitle = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_5_TITLE, tab5CurrentTitle).apply(); binding.tab5TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab5CurrentTitle); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab5TypeLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setSingleChoiceItems(typeValues, tab5CurrentPostType, (dialogInterface, i) -> { tab5CurrentPostType = i; mainActivityTabsSharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_5_POST_TYPE, i).apply(); binding.tab5TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[i]); applyTab5NameView(binding.tab5NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab5NameTitleTextViewCustomizeMainPageTabsFragment, i); mSettingsChanged = true; updateRestartButtonVisibility(); dialogInterface.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); }); binding.tab5NameConstraintLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { int titleId; switch (tab5CurrentPostType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: titleId = R.string.settings_tab_subreddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: titleId = R.string.settings_tab_multi_reddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: titleId = R.string.settings_tab_username; break; default: return; } editText.setText(tab5CurrentName); editText.setHint(titleId); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(titleId) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab5CurrentName = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_5_NAME, tab5CurrentName).apply(); binding.tab5NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab5CurrentName); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab5NameAddImageViewCustomizeMainPageTabsFragment.setOnClickListener(view -> selectName(4)); tab6CurrentTitle = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_6_TITLE, getString(R.string.saved)); tab6CurrentPostType = mainActivityTabsSharedPreferences.getInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_6_POST_TYPE, SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_ALL); if (!mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT)) { tab6CurrentPostType = Utils.fixIndexOutOfBoundsUsingPredetermined(typeValues, tab6CurrentPostType, 1); } tab6CurrentName = mainActivityTabsSharedPreferences.getString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_6_NAME, ""); binding.tab6TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[tab6CurrentPostType]); binding.tab6TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab6CurrentTitle); binding.tab6NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab6CurrentName); applyTab6NameView(binding.tab6NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab6NameTitleTextViewCustomizeMainPageTabsFragment, tab6CurrentPostType); binding.tab6TitleLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { editText.setHint(R.string.settings_tab_title); editText.setText(tab6CurrentTitle); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab6CurrentTitle = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_6_TITLE, tab6CurrentTitle).apply(); binding.tab6TitleSummaryTextViewCustomizeMainPageTabsFragment.setText(tab6CurrentTitle); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab6TypeLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.settings_tab_title) .setSingleChoiceItems(typeValues, tab6CurrentPostType, (dialogInterface, i) -> { tab6CurrentPostType = i; mainActivityTabsSharedPreferences.edit().putInt((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_6_POST_TYPE, i).apply(); binding.tab6TypeSummaryTextViewCustomizeMainPageTabsFragment.setText(typeValues[i]); applyTab6NameView(binding.tab6NameConstraintLayoutCustomizeMainPageTabsFragment, binding.tab6NameTitleTextViewCustomizeMainPageTabsFragment, i); mSettingsChanged = true; updateRestartButtonVisibility(); dialogInterface.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); }); binding.tab6NameConstraintLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { int titleId; switch (tab6CurrentPostType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: titleId = R.string.settings_tab_subreddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: titleId = R.string.settings_tab_multi_reddit_name; break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: titleId = R.string.settings_tab_username; break; default: return; } editText.setText(tab6CurrentName); editText.setHint(titleId); editText.requestFocus(); Utils.showKeyboard(mActivity, new Handler(), editText); if (dialogView.getParent() != null) { ((ViewGroup) dialogView.getParent()).removeView(dialogView); } new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(titleId) .setView(dialogView) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { tab6CurrentName = editText.getText().toString(); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_6_NAME, tab6CurrentName).apply(); binding.tab6NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab6CurrentName); mSettingsChanged = true; updateRestartButtonVisibility(); Utils.hideKeyboard(mActivity); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { Utils.hideKeyboard(mActivity); }) .show(); }); binding.tab6NameAddImageViewCustomizeMainPageTabsFragment.setOnClickListener(view -> selectName(5)); binding.showMultiredditsSwitchMaterialCustomizeMainPageTabsFragment.setChecked(mainActivityTabsSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_MULTIREDDITS, false)); binding.showMultiredditsSwitchMaterialCustomizeMainPageTabsFragment.setOnCheckedChangeListener((compoundButton, b) -> { mainActivityTabsSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_MULTIREDDITS, b).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); }); binding.showMultiredditsLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { binding.showMultiredditsSwitchMaterialCustomizeMainPageTabsFragment.performClick(); }); binding.showFavoriteMultiredditsSwitchMaterialCustomizeMainPageTabsFragment.setChecked(mainActivityTabsSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_FAVORITE_MULTIREDDITS, false)); binding.showFavoriteMultiredditsSwitchMaterialCustomizeMainPageTabsFragment.setOnCheckedChangeListener((compoundButton, b) -> { mainActivityTabsSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_FAVORITE_MULTIREDDITS, b).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); }); binding.showFavoriteMultiredditsLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { binding.showFavoriteMultiredditsSwitchMaterialCustomizeMainPageTabsFragment.performClick(); }); binding.showSubscribedSubredditsSwitchMaterialCustomizeMainPageTabsFragment.setChecked(mainActivityTabsSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_SUBSCRIBED_SUBREDDITS, false)); binding.showSubscribedSubredditsSwitchMaterialCustomizeMainPageTabsFragment.setOnCheckedChangeListener((compoundButton, b) -> { mainActivityTabsSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_SUBSCRIBED_SUBREDDITS, b).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); }); binding.showSubscribedSubredditsLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { binding.showSubscribedSubredditsSwitchMaterialCustomizeMainPageTabsFragment.performClick(); }); binding.showFavoriteSubscribedSubredditsSwitchMaterialCustomizeMainPageTabsFragment.setChecked(mainActivityTabsSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_FAVORITE_SUBSCRIBED_SUBREDDITS, false)); binding.showFavoriteSubscribedSubredditsSwitchMaterialCustomizeMainPageTabsFragment.setOnCheckedChangeListener((compoundButton, b) -> { mainActivityTabsSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_SHOW_FAVORITE_SUBSCRIBED_SUBREDDITS, b).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); }); binding.showFavoriteSubscribedSubredditsLinearLayoutCustomizeMainPageTabsFragment.setOnClickListener(view -> { binding.showFavoriteSubscribedSubredditsSwitchMaterialCustomizeMainPageTabsFragment.performClick(); }); updateTabViewsVisibility(tabCount); return binding.getRoot(); } private void updateTabViewsVisibility(int currentTabCount) { // Tab 1 binding.tab1GroupSummaryCustomizeMainPageTabsFragment.setVisibility(View.VISIBLE); // Always visible binding.tab1TitleLinearLayoutCustomizeMainPageTabsFragment.setVisibility(View.VISIBLE); binding.tab1TypeLinearLayoutCustomizeMainPageTabsFragment.setVisibility(View.VISIBLE); binding.tab1NameConstraintLayoutCustomizeMainPageTabsFragment.setVisibility((tab1CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT || tab1CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT || tab1CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER) ? View.VISIBLE : View.GONE); // divider_1_customize_main_page_tabs_fragment seems to be static, after "show tab names" - not controlled by tabCount // Tab 2 binding.divider2CustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 2 ? View.VISIBLE : View.GONE); binding.tab2GroupSummaryCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 2 ? View.VISIBLE : View.GONE); binding.tab2TitleLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 2 ? View.VISIBLE : View.GONE); binding.tab2TypeLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 2 ? View.VISIBLE : View.GONE); binding.tab2NameConstraintLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 2 && (tab2CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT || tab2CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT || tab2CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER) ? View.VISIBLE : View.GONE); // Tab 3 binding.divider3CustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 3 ? View.VISIBLE : View.GONE); binding.tab3GroupSummaryCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 3 ? View.VISIBLE : View.GONE); binding.tab3TitleLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 3 ? View.VISIBLE : View.GONE); binding.tab3TypeLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 3 ? View.VISIBLE : View.GONE); binding.tab3NameConstraintLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 3 && (tab3CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT || tab3CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT || tab3CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER) ? View.VISIBLE : View.GONE); // Tab 4 binding.divider4CustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 4 ? View.VISIBLE : View.GONE); binding.tab4GroupSummaryCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 4 ? View.VISIBLE : View.GONE); binding.tab4TitleLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 4 ? View.VISIBLE : View.GONE); binding.tab4TypeLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 4 ? View.VISIBLE : View.GONE); binding.tab4NameConstraintLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 4 && (tab4CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT || tab4CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT || tab4CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER) ? View.VISIBLE : View.GONE); // Tab 5 binding.divider5CustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 5 ? View.VISIBLE : View.GONE); binding.tab5GroupSummaryCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 5 ? View.VISIBLE : View.GONE); binding.tab5TitleLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 5 ? View.VISIBLE : View.GONE); binding.tab5TypeLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 5 ? View.VISIBLE : View.GONE); binding.tab5NameConstraintLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 5 && (tab5CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT || tab5CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT || tab5CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER) ? View.VISIBLE : View.GONE); // Tab 6 // Assuming there's a divider6 after tab 5 if tab 6 is shown. // If it's missing in XML, this line will cause a new error. // binding.divider6CustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 6 ? View.VISIBLE : View.GONE); binding.tab6GroupSummaryCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 6 ? View.VISIBLE : View.GONE); binding.tab6TitleLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 6 ? View.VISIBLE : View.GONE); binding.tab6TypeLinearLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 6 ? View.VISIBLE : View.GONE); binding.tab6NameConstraintLayoutCustomizeMainPageTabsFragment.setVisibility(currentTabCount >= 6 && (tab6CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT || tab6CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT || tab6CurrentPostType == SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER) ? View.VISIBLE : View.GONE); // "More Tabs" sections - these titles/info texts refer to tabs 4, 5, 6 binding.moreTabsGroupSummaryCustomizeMainPageTabsFragment.setVisibility(currentTabCount > 3 ? View.VISIBLE : View.GONE); binding.moreTabsInfoTextViewCustomizeMainPageTabsFragment.setVisibility(currentTabCount > 3 ? View.VISIBLE : View.GONE); } private void updateRestartButtonVisibility() { if (restartButton != null) { restartButton.setVisibility(mSettingsChanged ? View.VISIBLE : View.GONE); } } private void applyCustomTheme() { int primaryTextColor = mActivity.customThemeWrapper.getPrimaryTextColor(); int secondaryTextColor = mActivity.customThemeWrapper.getSecondaryTextColor(); int colorAccent = mActivity.customThemeWrapper.getColorAccent(); int primaryIconColor = mActivity.customThemeWrapper.getPrimaryIconColor(); binding.moreTabsInfoTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); Drawable infoDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_info_preference_day_night_24dp, secondaryTextColor); binding.moreTabsInfoTextViewCustomizeMainPageTabsFragment.setCompoundDrawablesWithIntrinsicBounds(infoDrawable, null, null, null); binding.tabCountTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tabCountTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.showTabNamesTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab1GroupSummaryCustomizeMainPageTabsFragment.setTextColor(colorAccent); binding.tab1TitleTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab1TitleSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab1TypeTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab1TypeSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab1NameTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab1NameSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab1NameAddImageViewCustomizeMainPageTabsFragment.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.tab2GroupSummaryCustomizeMainPageTabsFragment.setTextColor(colorAccent); binding.tab2TitleTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab2TitleSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab2TypeTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab2TypeSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab2NameTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab2NameSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab2NameAddImageViewCustomizeMainPageTabsFragment.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.tab3GroupSummaryCustomizeMainPageTabsFragment.setTextColor(colorAccent); binding.tab3TitleTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab3TitleSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab3TypeTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab3TypeSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab3NameTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab3NameSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab3NameAddImageViewCustomizeMainPageTabsFragment.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.tab4GroupSummaryCustomizeMainPageTabsFragment.setTextColor(colorAccent); binding.moreTabsGroupSummaryCustomizeMainPageTabsFragment.setTextColor(colorAccent); binding.tab4TitleTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab4TitleSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab4TypeTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab4TypeSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab4NameTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab4NameSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab4NameAddImageViewCustomizeMainPageTabsFragment.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.tab5GroupSummaryCustomizeMainPageTabsFragment.setTextColor(colorAccent); binding.tab5TitleTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab5TitleSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab5TypeTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab5TypeSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab5NameTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab5NameSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab5NameAddImageViewCustomizeMainPageTabsFragment.setColorFilter(primaryIconColor, android.graphics.PorterDuff.Mode.SRC_IN); binding.tab6GroupSummaryCustomizeMainPageTabsFragment.setTextColor(colorAccent); binding.tab6TitleTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab6TitleSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.tab6TypeTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.tab6TypeSummaryTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.moreTabsInfoTextViewCustomizeMainPageTabsFragment.setTextColor(secondaryTextColor); binding.moreTabsInfoTextViewCustomizeMainPageTabsFragment.setCompoundDrawablesWithIntrinsicBounds(infoDrawable, null, null, null); binding.showFavoriteMultiredditsTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.showMultiredditsTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.showSubscribedSubredditsTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); binding.showFavoriteSubscribedSubredditsTitleTextViewCustomizeMainPageTabsFragment.setTextColor(primaryTextColor); } private void applyTab1NameView(ConstraintLayout constraintLayout, TextView titleTextView, int postType) { switch (postType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_subreddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_multi_reddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_username); break; default: constraintLayout.setVisibility(View.GONE); } } private void applyTab2NameView(ConstraintLayout linearLayout, TextView titleTextView, int postType) { switch (postType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: linearLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_subreddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: linearLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_multi_reddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: linearLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_username); break; default: linearLayout.setVisibility(View.GONE); } } private void applyTab3NameView(ConstraintLayout constraintLayout, TextView titleTextView, int postType) { switch (postType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_subreddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_multi_reddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_username); break; default: constraintLayout.setVisibility(View.GONE); } } private void applyTab4NameView(ConstraintLayout constraintLayout, TextView titleTextView, int postType) { switch (postType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_subreddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_multi_reddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_username); break; default: constraintLayout.setVisibility(View.GONE); } } private void applyTab5NameView(ConstraintLayout constraintLayout, TextView titleTextView, int postType) { switch (postType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_subreddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_multi_reddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_username); break; default: constraintLayout.setVisibility(View.GONE); } } private void applyTab6NameView(ConstraintLayout constraintLayout, TextView titleTextView, int postType) { switch (postType) { case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_subreddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_multi_reddit_name); break; case SharedPreferencesUtils.MAIN_PAGE_TAB_POST_TYPE_USER: constraintLayout.setVisibility(View.VISIBLE); titleTextView.setText(R.string.settings_tab_username); break; default: constraintLayout.setVisibility(View.GONE); } } private void selectName(int tab) { switch (tab) { case 0: switch (tab1CurrentPostType) { case 3: { Intent intent = new Intent(mActivity, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, tab); break; } case 4: { Intent intent = new Intent(mActivity, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_MULTIREDDIT); startActivityForResult(intent, tab); break; } case 5: { Intent intent = new Intent(mActivity, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); startActivityForResult(intent, tab); break; } } break; case 1: switch (tab2CurrentPostType) { case 3: { Intent intent = new Intent(mActivity, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, tab); break; } case 4: { Intent intent = new Intent(mActivity, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_MULTIREDDIT); startActivityForResult(intent, tab); break; } case 5: { Intent intent = new Intent(mActivity, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); startActivityForResult(intent, tab); break; } } break; case 2: switch (tab3CurrentPostType) { case 3: { Intent intent = new Intent(mActivity, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_SUBREDDIT); startActivityForResult(intent, tab); break; } case 4: { Intent intent = new Intent(mActivity, SubscribedThingListingActivity.class); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_MODE, true); intent.putExtra(SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE, SubscribedThingListingActivity.EXTRA_THING_SELECTION_TYPE_MULTIREDDIT); startActivityForResult(intent, tab); break; } case 5: { Intent intent = new Intent(mActivity, SearchActivity.class); intent.putExtra(SearchActivity.EXTRA_SEARCH_ONLY_USERS, true); startActivityForResult(intent, tab); break; } } break; } } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == Activity.RESULT_OK && data != null) { int thingType = data.getIntExtra(SelectThingReturnKey.RETURN_EXTRA_THING_TYPE, SelectThingReturnKey.THING_TYPE.SUBREDDIT); switch (requestCode) { case 0: if (thingType == SelectThingReturnKey.THING_TYPE.SUBREDDIT) { tab1CurrentName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); binding.tab1NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab1CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_NAME, tab1CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } else if (thingType == SelectThingReturnKey.THING_TYPE.MULTIREDDIT) { MultiReddit multireddit = data.getParcelableExtra(SelectThingReturnKey.RETRUN_EXTRA_MULTIREDDIT); if (multireddit != null) { tab1CurrentName = multireddit.getPath(); binding.tab1NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab1CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_NAME, tab1CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } } else if (thingType == SelectThingReturnKey.THING_TYPE.USER) { tab1CurrentName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); binding.tab1NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab1CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_1_NAME, tab1CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } break; case 1: if (thingType == SelectThingReturnKey.THING_TYPE.SUBREDDIT) { tab2CurrentName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); binding.tab2NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab2CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_NAME, tab2CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } else if (thingType == SelectThingReturnKey.THING_TYPE.MULTIREDDIT) { MultiReddit multireddit = data.getParcelableExtra(SelectThingReturnKey.RETRUN_EXTRA_MULTIREDDIT); if (multireddit != null) { tab2CurrentName = multireddit.getPath(); binding.tab2NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab2CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_NAME, tab2CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } } else if (thingType == SelectThingReturnKey.THING_TYPE.USER) { tab2CurrentName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); binding.tab2NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab2CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_2_NAME, tab2CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } break; case 2: if (thingType == SelectThingReturnKey.THING_TYPE.SUBREDDIT) { tab3CurrentName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); binding.tab3NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab3CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_NAME, tab3CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } else if (thingType == SelectThingReturnKey.THING_TYPE.MULTIREDDIT) { MultiReddit multireddit = data.getParcelableExtra(SelectThingReturnKey.RETRUN_EXTRA_MULTIREDDIT); if (multireddit != null) { tab3CurrentName = multireddit.getPath(); binding.tab3NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab3CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_NAME, tab3CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } } else if (thingType == SelectThingReturnKey.THING_TYPE.USER) { tab3CurrentName = data.getStringExtra(SelectThingReturnKey.RETURN_EXTRA_SUBREDDIT_OR_USER_NAME); binding.tab3NameSummaryTextViewCustomizeMainPageTabsFragment.setText(tab3CurrentName); mainActivityTabsSharedPreferences.edit().putString((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.MAIN_PAGE_TAB_3_NAME, tab3CurrentName).apply(); mSettingsChanged = true; updateRestartButtonVisibility(); } break; } } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/DataSavingModePreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import androidx.preference.ListPreference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeDataSavingModeEvent; import ml.docilealligator.infinityforreddit.events.ChangeDisableImagePreviewEvent; import ml.docilealligator.infinityforreddit.events.ChangeOnlyDisablePreviewInVideoAndGifPostsEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class DataSavingModePreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.data_saving_mode_preferences, rootKey); ListPreference dataSavingModeListPreference = findPreference(SharedPreferencesUtils.DATA_SAVING_MODE); SwitchPreference disableImagePreviewPreference = findPreference(SharedPreferencesUtils.DISABLE_IMAGE_PREVIEW); SwitchPreference onlyDisablePreviewInVideoAndGifPostsPreference = findPreference(SharedPreferencesUtils.ONLY_DISABLE_PREVIEW_IN_VIDEO_AND_GIF_POSTS); ListPreference redditVideoDefaultResolutionListPreference = findPreference(SharedPreferencesUtils.REDDIT_VIDEO_DEFAULT_RESOLUTION); if (dataSavingModeListPreference != null) { if (dataSavingModeListPreference.getValue().equals("0")) { if (onlyDisablePreviewInVideoAndGifPostsPreference != null) { onlyDisablePreviewInVideoAndGifPostsPreference.setVisible(false); } if (disableImagePreviewPreference != null) { disableImagePreviewPreference.setVisible(false); } if (redditVideoDefaultResolutionListPreference != null) { redditVideoDefaultResolutionListPreference.setVisible(false); } } dataSavingModeListPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeDataSavingModeEvent((String) newValue)); if (newValue.equals("0")) { if (onlyDisablePreviewInVideoAndGifPostsPreference != null) { onlyDisablePreviewInVideoAndGifPostsPreference.setVisible(false); } if (disableImagePreviewPreference != null) { disableImagePreviewPreference.setVisible(false); } if (redditVideoDefaultResolutionListPreference != null) { redditVideoDefaultResolutionListPreference.setVisible(false); } } else { if (onlyDisablePreviewInVideoAndGifPostsPreference != null) { onlyDisablePreviewInVideoAndGifPostsPreference.setVisible(true); } if (disableImagePreviewPreference != null) { disableImagePreviewPreference.setVisible(true); } if (redditVideoDefaultResolutionListPreference != null) { redditVideoDefaultResolutionListPreference.setVisible(true); } } return true; }); } if (disableImagePreviewPreference != null) { disableImagePreviewPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeDisableImagePreviewEvent((Boolean) newValue)); if ((Boolean) newValue) { EventBus.getDefault().post(new ChangeOnlyDisablePreviewInVideoAndGifPostsEvent(false)); if (onlyDisablePreviewInVideoAndGifPostsPreference != null) { onlyDisablePreviewInVideoAndGifPostsPreference.setChecked(false); } } return true; }); } if (onlyDisablePreviewInVideoAndGifPostsPreference != null) { onlyDisablePreviewInVideoAndGifPostsPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeOnlyDisablePreviewInVideoAndGifPostsEvent((Boolean) newValue)); if ((Boolean) newValue) { EventBus.getDefault().post(new ChangeDisableImagePreviewEvent(false)); if (disableImagePreviewPreference != null) { disableImagePreviewPreference.setChecked(false); } } return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/DebugPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.res.Configuration; import android.os.Bundle; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; /** * A simple {@link PreferenceFragmentCompat} subclass. */ public class DebugPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.debug_preferences, rootKey); Preference screenWidthDpPreference = findPreference(SharedPreferencesUtils.SCREEN_WIDTH_DP_KEY); Preference smallestScreenWidthDpPreference = findPreference(SharedPreferencesUtils.SMALLEST_SCREEN_WIDTH_DP_KEY); Preference isTabletPreference = findPreference(SharedPreferencesUtils.IS_TABLET_KEY); if (screenWidthDpPreference != null) { Configuration config = getResources().getConfiguration(); int screenWidthDp = config.screenWidthDp; screenWidthDpPreference.setSummary(getString(R.string.settings_screen_width_dp_summary, screenWidthDp)); } if (smallestScreenWidthDpPreference != null) { Configuration config = getResources().getConfiguration(); int smallestScreenWidthDp = config.smallestScreenWidthDp; smallestScreenWidthDpPreference.setSummary(getString(R.string.settings_smallest_screen_width_dp_summary, smallestScreenWidthDp)); } if (isTabletPreference != null) { boolean isTablet = getResources().getBoolean(R.bool.isTablet); isTabletPreference.setSummary(isTablet ? getString(R.string.settings_is_tablet_summary_true) : getString(R.string.settings_is_tablet_summary_false)); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/DownloadLocationPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.preference.Preference; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; // Imports for long click handling import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; public class DownloadLocationPreferenceFragment extends CustomFontPreferenceFragmentCompat { private static final int IMAGE_DOWNLOAD_LOCATION_REQUEST_CODE = 10; private static final int GIF_DOWNLOAD_LOCATION_REQUEST_CODE = 11; private static final int VIDEO_DOWNLOAD_LOCATION_REQUEST_CODE = 12; private static final int NSFW_DOWNLOAD_LOCATION_REQUEST_CODE = 13; Preference imageDownloadLocationPreference; Preference gifDownloadLocationPreference; Preference videoDownloadLocationPreference; Preference nsfwDownloadLocationPreference; @Inject @Named("default") SharedPreferences sharedPreferences; // Flag to track if a long press occurred private boolean isLongPressing = false; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); setPreferencesFromResource(R.xml.download_location_preferences, rootKey); imageDownloadLocationPreference = findPreference(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION); gifDownloadLocationPreference = findPreference(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION); videoDownloadLocationPreference = findPreference(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION); nsfwDownloadLocationPreference = findPreference(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION); if (nsfwDownloadLocationPreference != null) { String downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, ""); if (!downloadLocation.equals("")) { nsfwDownloadLocationPreference.setSummary(formatDownloadPath(downloadLocation)); } else { nsfwDownloadLocationPreference.setSummary(R.string.settings_download_location_not_set); } nsfwDownloadLocationPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, NSFW_DOWNLOAD_LOCATION_REQUEST_CODE); return true; }); // No long click for NSFW location for now, can be added if needed. } if (imageDownloadLocationPreference != null) { String downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); if (!downloadLocation.equals("")) { imageDownloadLocationPreference.setSummary(formatDownloadPath(downloadLocation)); } else { imageDownloadLocationPreference.setSummary(R.string.settings_download_location_not_set); } imageDownloadLocationPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, IMAGE_DOWNLOAD_LOCATION_REQUEST_CODE); return true; }); } if (gifDownloadLocationPreference != null) { String downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); if (!downloadLocation.equals("")) { gifDownloadLocationPreference.setSummary(formatDownloadPath(downloadLocation)); } else { gifDownloadLocationPreference.setSummary(R.string.settings_download_location_not_set); } gifDownloadLocationPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, GIF_DOWNLOAD_LOCATION_REQUEST_CODE); return true; }); } if (videoDownloadLocationPreference != null) { String downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); if (!downloadLocation.equals("")) { videoDownloadLocationPreference.setSummary(formatDownloadPath(downloadLocation)); } else { videoDownloadLocationPreference.setSummary(R.string.settings_download_location_not_set); } videoDownloadLocationPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, VIDEO_DOWNLOAD_LOCATION_REQUEST_CODE); return true; }); } } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { if (requestCode == IMAGE_DOWNLOAD_LOCATION_REQUEST_CODE) { mActivity.getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); sharedPreferences.edit().putString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, data.getDataString()).apply(); if (imageDownloadLocationPreference != null) { imageDownloadLocationPreference.setSummary(formatDownloadPath(data.getDataString())); } } else if (requestCode == GIF_DOWNLOAD_LOCATION_REQUEST_CODE) { mActivity.getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); sharedPreferences.edit().putString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, data.getDataString()).apply(); if (gifDownloadLocationPreference != null) { gifDownloadLocationPreference.setSummary(formatDownloadPath(data.getDataString())); } } else if (requestCode == VIDEO_DOWNLOAD_LOCATION_REQUEST_CODE) { mActivity.getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); sharedPreferences.edit().putString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, data.getDataString()).apply(); if (videoDownloadLocationPreference != null) { videoDownloadLocationPreference.setSummary(formatDownloadPath(data.getDataString())); } } else if (requestCode == NSFW_DOWNLOAD_LOCATION_REQUEST_CODE) { mActivity.getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); sharedPreferences.edit().putString(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION, data.getDataString()).apply(); if (nsfwDownloadLocationPreference != null) { nsfwDownloadLocationPreference.setSummary(formatDownloadPath(data.getDataString())); } } } } private String formatDownloadPath(String uriString) { if (uriString == null || uriString.isEmpty()) { return ""; } String prefix = "content://com.android.externalstorage.documents/tree/primary%3A"; if (uriString.startsWith(prefix)) { String encodedPath = uriString.substring(prefix.length()); try { // Decode URL encoding (e.g., %2F -> /, %20 -> space) return "/" + URLDecoder.decode(encodedPath, StandardCharsets.UTF_8.name()); } catch (Exception e) { // Fallback to original string if decoding fails return uriString; } } // Return original string if prefix doesn't match return uriString; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final RecyclerView recyclerView = getListView(); if (recyclerView != null) { GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent e) { View childView = recyclerView.findChildViewUnder(e.getX(), e.getY()); if (childView != null) { int position = recyclerView.getChildAdapterPosition(childView); if (position != RecyclerView.NO_POSITION) { Preference preference = getPreferenceScreen().getPreference(position); if (preference != null) { String key = preference.getKey(); boolean handled = false; if (SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION.equals(key)) { sharedPreferences.edit().remove(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION).apply(); preference.setSummary(R.string.settings_download_location_not_set); handled = true; } else if (SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION.equals(key)) { sharedPreferences.edit().remove(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION).apply(); preference.setSummary(R.string.settings_download_location_not_set); handled = true; } else if (SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION.equals(key)) { sharedPreferences.edit().remove(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION).apply(); preference.setSummary(R.string.settings_download_location_not_set); handled = true; } else if (SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION.equals(key)) { sharedPreferences.edit().remove(SharedPreferencesUtils.NSFW_DOWNLOAD_LOCATION).apply(); preference.setSummary(R.string.settings_download_location_not_set); handled = true; } if (handled) { isLongPressing = true; // Set the flag when long press is handled } } } } } }); recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { // Let the GestureDetector analyze the event first. boolean detected = gestureDetector.onTouchEvent(e); // IMPORTANT: Reset flag on ACTION_DOWN to start fresh for each gesture. if (e.getAction() == MotionEvent.ACTION_DOWN) { isLongPressing = false; } // If gesture detector detected something OR if we are in a long press state, // intercept the event (especially the ACTION_UP after the long press). // Returning true consumes the event and prevents children (like the preference item) // from receiving it. return isLongPressing || detected; // Let's try just isLongPressing for interception } @Override public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { // Let the GestureDetector handle the event. gestureDetector.onTouchEvent(e); // If a long press was handled (flag is set) and the event is ACTION_UP or ACTION_CANCEL, // we simply reset the flag. Returning true from onInterceptTouchEvent should handle consumption. if (isLongPressing && (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL)) { isLongPressing = false; } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { // No-op } }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/FontPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.preference.ListPreference; import androidx.preference.Preference; import org.greenrobot.eventbus.EventBus; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.font.ContentFontFamily; import ml.docilealligator.infinityforreddit.font.FontFamily; import ml.docilealligator.infinityforreddit.font.TitleFontFamily; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class FontPreferenceFragment extends CustomFontPreferenceFragmentCompat { private static final int CUSTOM_FONT_FAMILY_REQUEST_CODE = 20; private static final int CUSTOM_TITLE_FONT_FAMILY_REQUEST_CODE = 21; private static final int CUSTOM_CONTENT_FONT_FAMILY_REQUEST_CODE = 22; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject Executor executor; private Preference customFontFamilyPreference; private Preference customTitleFontFamilyPreference; private Preference customContentFontFamilyPreference; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.font_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); ListPreference fontFamilyPreference = findPreference(SharedPreferencesUtils.FONT_FAMILY_KEY); customFontFamilyPreference = findPreference(SharedPreferencesUtils.CUSTOM_FONT_FAMILY_KEY); ListPreference titleFontFamilyPreference = findPreference(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY); customTitleFontFamilyPreference = findPreference(SharedPreferencesUtils.CUSTOM_TITLE_FONT_FAMILY_KEY); ListPreference contentFontFamilyPreference = findPreference(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY); customContentFontFamilyPreference = findPreference(SharedPreferencesUtils.CUSTOM_CONTENT_FONT_FAMILY_KEY); ListPreference fontSizePreference = findPreference(SharedPreferencesUtils.FONT_SIZE_KEY); ListPreference titleFontSizePreference = findPreference(SharedPreferencesUtils.TITLE_FONT_SIZE_KEY); ListPreference contentFontSizePreference = findPreference(SharedPreferencesUtils.CONTENT_FONT_SIZE_KEY); if (customFontFamilyPreference != null) { if (sharedPreferences.getString(SharedPreferencesUtils.FONT_FAMILY_KEY, FontFamily.Default.name()).equals(FontFamily.Custom.name())) { customFontFamilyPreference.setVisible(true); } customFontFamilyPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(); intent.setType("*/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getString(R.string.select_a_ttf_font)), CUSTOM_FONT_FAMILY_REQUEST_CODE); return true; }); } if (fontFamilyPreference != null) { fontFamilyPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); ActivityCompat.recreate(mActivity); return true; }); } if (customTitleFontFamilyPreference != null) { if (sharedPreferences.getString(SharedPreferencesUtils.TITLE_FONT_FAMILY_KEY, TitleFontFamily.Default.name()).equals(TitleFontFamily.Custom.name())) { customTitleFontFamilyPreference.setVisible(true); } customTitleFontFamilyPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(); intent.setType("*/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getString(R.string.select_a_ttf_font)), CUSTOM_TITLE_FONT_FAMILY_REQUEST_CODE); return true; }); } if (titleFontFamilyPreference != null) { titleFontFamilyPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } if (customContentFontFamilyPreference != null) { if (sharedPreferences.getString(SharedPreferencesUtils.CONTENT_FONT_FAMILY_KEY, ContentFontFamily.Default.name()).equals(ContentFontFamily.Custom.name())) { customContentFontFamilyPreference.setVisible(true); } customContentFontFamilyPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(); intent.setType("*/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getString(R.string.select_a_ttf_font)), CUSTOM_CONTENT_FONT_FAMILY_REQUEST_CODE); return true; }); } if (contentFontFamilyPreference != null) { contentFontFamilyPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } if (fontSizePreference != null) { fontSizePreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); ActivityCompat.recreate(mActivity); return true; }); } if (titleFontSizePreference != null) { titleFontSizePreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } if (contentFontSizePreference != null) { contentFontSizePreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { if (requestCode == CUSTOM_FONT_FAMILY_REQUEST_CODE) { copyFontToInternalStorage(data.getData(), 0); if (customFontFamilyPreference != null) { customFontFamilyPreference.setSummary(data.getDataString()); } } else if (requestCode == CUSTOM_TITLE_FONT_FAMILY_REQUEST_CODE) { copyFontToInternalStorage(data.getData(), 1); } else if (requestCode == CUSTOM_CONTENT_FONT_FAMILY_REQUEST_CODE) { copyFontToInternalStorage(data.getData(), 2); if (customContentFontFamilyPreference != null) { customContentFontFamilyPreference.setSummary(data.getDataString()); } } } } private void copyFontToInternalStorage(Uri uri, int type) { String destinationFontName; switch (type) { case 1: destinationFontName = "title_font_family.ttf"; break; case 2: destinationFontName = "content_font_family.ttf"; break; default: destinationFontName = "font_family.ttf"; } File fontDestinationPath = mActivity.getExternalFilesDir("fonts"); Handler handler = new Handler(); executor.execute(() -> { File destinationFontFile = new File(fontDestinationPath, destinationFontName); try (InputStream in = mActivity.getContentResolver().openInputStream(uri); OutputStream out = new FileOutputStream(destinationFontFile)) { if (in != null) { byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } try { switch (type) { case 1: ((Infinity) mActivity.getApplication()).titleTypeface = Typeface.createFromFile(destinationFontFile); break; case 2: ((Infinity) mActivity.getApplication()).contentTypeface = Typeface.createFromFile(destinationFontFile); break; default: ((Infinity) mActivity.getApplication()).typeface = Typeface.createFromFile(destinationFontFile); } } catch (RuntimeException e) { e.printStackTrace(); handler.post(() -> Toast.makeText(mActivity, R.string.unable_to_load_font, Toast.LENGTH_SHORT).show()); return; } } else { handler.post(() -> Toast.makeText(mActivity, R.string.unable_to_get_font_file, Toast.LENGTH_SHORT).show()); return; } handler.post(() -> { EventBus.getDefault().post(new RecreateActivityEvent()); ActivityCompat.recreate(mActivity); }); } catch (IOException e) { e.printStackTrace(); handler.post(() -> { Toast.makeText(mActivity, R.string.unable_to_copy_font_file, Toast.LENGTH_SHORT).show(); }); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/FontPreviewFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; 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.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.utils.Utils; public class FontPreviewFragment extends Fragment { private SettingsActivity mActivity; public FontPreviewFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_font_preview, container, false); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(rootView, new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); rootView.setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } rootView.setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); LinearLayout linearLayout = rootView.findViewById(R.id.linear_layout_font_preview_fragment); int primaryTextColor = mActivity.customThemeWrapper.getPrimaryTextColor(); for (int i = 0; i < linearLayout.getChildCount(); i++) { View view = linearLayout.getChildAt(i); if (view instanceof TextView) { ((TextView) view).setTextColor(primaryTextColor); } } return rootView; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/GesturesAndButtonsPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.SharedPreferences; import android.os.Bundle; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeLockBottomAppBarEvent; import ml.docilealligator.infinityforreddit.events.ChangePullToRefreshEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; /** * A simple {@link Fragment} subclass. */ public class GesturesAndButtonsPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("default") SharedPreferences sharedPreferences; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.gestures_and_buttons_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); SwitchPreference lockJumpToNextTopLevelCommentButtonSwitch = findPreference(SharedPreferencesUtils.LOCK_JUMP_TO_NEXT_TOP_LEVEL_COMMENT_BUTTON); SwitchPreference lockBottomAppBarSwitch = findPreference(SharedPreferencesUtils.LOCK_BOTTOM_APP_BAR); SwitchPreference swipeUpToHideJumpToNextTopLevelCommentButtonSwitch = findPreference(SharedPreferencesUtils.SWIPE_UP_TO_HIDE_JUMP_TO_NEXT_TOP_LEVEL_COMMENT_BUTTON); SwitchPreference pullToRefreshSwitch = findPreference(SharedPreferencesUtils.PULL_TO_REFRESH); if (lockJumpToNextTopLevelCommentButtonSwitch != null && lockBottomAppBarSwitch != null && swipeUpToHideJumpToNextTopLevelCommentButtonSwitch != null) { lockJumpToNextTopLevelCommentButtonSwitch.setOnPreferenceChangeListener((preference, newValue) -> { swipeUpToHideJumpToNextTopLevelCommentButtonSwitch.setVisible(!((Boolean) newValue)); return true; }); if (sharedPreferences.getBoolean(SharedPreferencesUtils.BOTTOM_APP_BAR_KEY, false)) { lockBottomAppBarSwitch.setVisible(true); lockBottomAppBarSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeLockBottomAppBarEvent((Boolean) newValue)); return true; }); } if (!sharedPreferences.getBoolean(SharedPreferencesUtils.LOCK_JUMP_TO_NEXT_TOP_LEVEL_COMMENT_BUTTON, false)) { swipeUpToHideJumpToNextTopLevelCommentButtonSwitch.setVisible(true); } } if (pullToRefreshSwitch != null) { pullToRefreshSwitch.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { EventBus.getDefault().post(new ChangePullToRefreshEvent((Boolean) newValue)); return true; } }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/ImmersiveInterfacePreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class ImmersiveInterfacePreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.immersive_interface_preferences, rootKey); SwitchPreference immersiveInterfaceSwitch = findPreference(SharedPreferencesUtils.IMMERSIVE_INTERFACE_KEY); SwitchPreference disableImmersiveInterfaceInLandscapeModeSwitch = findPreference(SharedPreferencesUtils.DISABLE_IMMERSIVE_INTERFACE_IN_LANDSCAPE_MODE); if (immersiveInterfaceSwitch != null && disableImmersiveInterfaceInLandscapeModeSwitch != null) { immersiveInterfaceSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); disableImmersiveInterfaceInLandscapeModeSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/InterfacePreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Build; import android.os.Bundle; import androidx.preference.Preference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeHideFabInPostFeedEvent; import ml.docilealligator.infinityforreddit.events.ChangeVoteButtonsPositionEvent; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class InterfacePreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.interface_preferences, rootKey); Preference immersiveInterfaceEntryPreference = findPreference(SharedPreferencesUtils.IMMERSIVE_INTERFACE_ENTRY_KEY); SwitchPreference hideFabInPostFeedSwitchPreference = findPreference(SharedPreferencesUtils.HIDE_FAB_IN_POST_FEED); SwitchPreference bottomAppBarSwitch = findPreference(SharedPreferencesUtils.BOTTOM_APP_BAR_KEY); SwitchPreference voteButtonsOnTheRightSwitch = findPreference(SharedPreferencesUtils.VOTE_BUTTONS_ON_THE_RIGHT_KEY); if (immersiveInterfaceEntryPreference != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { immersiveInterfaceEntryPreference.setVisible(true); } if (hideFabInPostFeedSwitchPreference != null) { hideFabInPostFeedSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHideFabInPostFeedEvent((Boolean) newValue)); return true; }); } if (bottomAppBarSwitch != null) { bottomAppBarSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } if (voteButtonsOnTheRightSwitch != null) { voteButtonsOnTheRightSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeVoteButtonsPositionEvent((Boolean) newValue)); return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/MainPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.biometric.BiometricManager; import androidx.preference.Preference; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.CommentFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.PostFilterPreferenceActivity; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceWithBackground; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class MainPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("default") SharedPreferences sharedPreferences; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.main_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); Preference securityPreference = findPreference(SharedPreferencesUtils.SECURITY); CustomFontPreferenceWithBackground dataSavingModePreference = findPreference(SharedPreferencesUtils.DATA_SAVING_MODE_PREFERENCE); Preference postFilterPreference = findPreference(SharedPreferencesUtils.POST_FILTER); Preference commentFilterPreference = findPreference(SharedPreferencesUtils.COMMENT_FILTER); Preference privacyPolicyPreference = findPreference(SharedPreferencesUtils.PRIVACY_POLICY_KEY); BiometricManager biometricManager = BiometricManager.from(mActivity); if (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) != BiometricManager.BIOMETRIC_SUCCESS) { if (securityPreference != null) { securityPreference.setVisible(false); if (dataSavingModePreference != null) { dataSavingModePreference.setTop(true); } } } if (postFilterPreference != null) { postFilterPreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, PostFilterPreferenceActivity.class); mActivity.startActivity(intent); return true; }); } if (commentFilterPreference != null) { commentFilterPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(@NonNull Preference preference) { Intent intent = new Intent(mActivity, CommentFilterPreferenceActivity.class); mActivity.startActivity(intent); return true; } }); } if (privacyPolicyPreference != null) { privacyPolicyPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse("https://github.com/cygnusx-1-org/continuum")); mActivity.startActivity(intent); return true; } }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/MiscellaneousPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.SharedPreferences; import android.os.Bundle; import android.widget.Toast; import androidx.preference.EditTextPreference; import androidx.preference.ListPreference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangePostFeedMaxResolutionEvent; import ml.docilealligator.infinityforreddit.events.ChangeSavePostFeedScrolledPositionEvent; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class MiscellaneousPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("post_feed_scrolled_position_cache") SharedPreferences cache; public MiscellaneousPreferenceFragment() { // Required empty public constructor } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.miscellaneous_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); ListPreference mainPageBackButtonActionListPreference = findPreference(SharedPreferencesUtils.MAIN_PAGE_BACK_BUTTON_ACTION); SwitchPreference savePostFeedScrolledPositionSwitch = findPreference(SharedPreferencesUtils.SAVE_FRONT_PAGE_SCROLLED_POSITION); ListPreference languageListPreference = findPreference(SharedPreferencesUtils.LANGUAGE); EditTextPreference postFeedMaxResolution = findPreference(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION); if (mainPageBackButtonActionListPreference != null) { mainPageBackButtonActionListPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } if (savePostFeedScrolledPositionSwitch != null) { savePostFeedScrolledPositionSwitch.setOnPreferenceChangeListener((preference, newValue) -> { if (!(Boolean) newValue) { cache.edit().clear().apply(); } EventBus.getDefault().post(new ChangeSavePostFeedScrolledPositionEvent((Boolean) newValue)); return true; }); } if (languageListPreference != null) { languageListPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new RecreateActivityEvent()); return true; }); } if (postFeedMaxResolution != null) { postFeedMaxResolution.setOnPreferenceChangeListener((preference, newValue) -> { try { int resolution = Integer.parseInt((String) newValue); if (resolution <= 0) { Toast.makeText(mActivity, R.string.not_a_valid_number, Toast.LENGTH_SHORT).show(); return false; } EventBus.getDefault().post(new ChangePostFeedMaxResolutionEvent(resolution)); } catch (NumberFormatException e) { Toast.makeText(mActivity, R.string.not_a_valid_number, Toast.LENGTH_SHORT).show(); return false; } return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/NavigationDrawerPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeHideKarmaEvent; import ml.docilealligator.infinityforreddit.events.ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class NavigationDrawerPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { PreferenceManager preferenceManager = getPreferenceManager(); preferenceManager.setSharedPreferencesName(SharedPreferencesUtils.NAVIGATION_DRAWER_SHARED_PREFERENCES_FILE); setPreferencesFromResource(R.xml.navigation_drawer_preferences, rootKey); SwitchPreference showAvatarOnTheRightSwitch = findPreference(SharedPreferencesUtils.SHOW_AVATAR_ON_THE_RIGHT); SwitchPreference hideKarmaSwitch = findPreference(SharedPreferencesUtils.HIDE_ACCOUNT_KARMA_NAV_BAR); if (showAvatarOnTheRightSwitch != null) { showAvatarOnTheRightSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeShowAvatarOnTheRightInTheNavigationDrawerEvent((Boolean) newValue)); return true; }); } if (hideKarmaSwitch != null) { hideKarmaSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHideKarmaEvent((Boolean) newValue)); return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/NotificationPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.View; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.preference.ListPreference; import androidx.preference.SwitchPreference; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.worker.PullNotificationWorker; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; /** * A simple {@link Fragment} subclass. */ public class NotificationPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("internal") SharedPreferences mInternalSharedPreferences; private boolean enableNotification; private long notificationInterval; private WorkManager workManager; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.notification_preferences, rootKey); workManager = WorkManager.getInstance(mActivity); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); SwitchPreference enableNotificationSwitchPreference = findPreference(SharedPreferencesUtils.ENABLE_NOTIFICATION_KEY); ListPreference notificationIntervalListPreference = findPreference(SharedPreferencesUtils.NOTIFICATION_INTERVAL_KEY); enableNotification = sharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_NOTIFICATION_KEY, true); notificationInterval = Long.parseLong(sharedPreferences.getString(SharedPreferencesUtils.NOTIFICATION_INTERVAL_KEY, "1")); if (enableNotification) { if (notificationIntervalListPreference != null) { notificationIntervalListPreference.setVisible(true); } } if (enableNotificationSwitchPreference != null) { enableNotificationSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { enableNotification = ((Boolean) newValue); if (notificationIntervalListPreference != null) { notificationIntervalListPreference.setVisible(enableNotification); } if (enableNotification) { TimeUnit timeUnit = (notificationInterval == 15 || notificationInterval == 30) ? TimeUnit.MINUTES : TimeUnit.HOURS; Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build(); PeriodicWorkRequest pullNotificationRequest = new PeriodicWorkRequest.Builder(PullNotificationWorker.class, notificationInterval, timeUnit) .setConstraints(constraints) .setInitialDelay(notificationInterval, timeUnit) .build(); workManager.enqueueUniquePeriodicWork(PullNotificationWorker.UNIQUE_WORKER_NAME, ExistingPeriodicWorkPolicy.REPLACE, pullNotificationRequest); } else { workManager.cancelUniqueWork(PullNotificationWorker.UNIQUE_WORKER_NAME); } return true; }); } if (notificationIntervalListPreference != null) { notificationIntervalListPreference.setOnPreferenceChangeListener((preference, newValue) -> { notificationInterval = Long.parseLong((String) newValue); if (enableNotification) { TimeUnit timeUnit = (notificationInterval == 15 || notificationInterval == 30) ? TimeUnit.MINUTES : TimeUnit.HOURS; Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build(); PeriodicWorkRequest pullNotificationRequest = new PeriodicWorkRequest.Builder(PullNotificationWorker.class, notificationInterval, timeUnit) .setConstraints(constraints) .setInitialDelay(notificationInterval, timeUnit) .build(); workManager.enqueueUniquePeriodicWork(PullNotificationWorker.UNIQUE_WORKER_NAME, ExistingPeriodicWorkPolicy.REPLACE, pullNotificationRequest); } else { workManager.cancelUniqueWork(PullNotificationWorker.UNIQUE_WORKER_NAME); } return true; }); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ActivityResultLauncher requestNotificationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), result -> { mInternalSharedPreferences.edit().putBoolean(SharedPreferencesUtils.HAS_REQUESTED_NOTIFICATION_PERMISSION, true).apply(); if (!result) { mActivity.showSnackbar(R.string.denied_notification_permission, R.string.go_to_settings, new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri uri = Uri.fromParts("package", mActivity.getPackageName(), null); intent.setData(uri); startActivity(intent); } }); } }); if (ContextCompat.checkSelfPermission(mActivity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/NsfwAndSpoilerFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.text.SpannableString; import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.greenrobot.eventbus.EventBus; import javax.inject.Inject; import javax.inject.Named; import me.saket.bettermovementmethod.BetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.databinding.FragmentNsfwAndSpoilerBinding; import ml.docilealligator.infinityforreddit.events.ChangeNSFWBlurEvent; import ml.docilealligator.infinityforreddit.events.ChangeNSFWEvent; import ml.docilealligator.infinityforreddit.events.ChangeSpoilerBlurEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class NsfwAndSpoilerFragment extends Fragment { private FragmentNsfwAndSpoilerBinding binding; @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("nsfw_and_spoiler") SharedPreferences nsfwAndBlurringSharedPreferences; private SettingsActivity mActivity; private boolean blurNsfw; private boolean doNotBlurNsfwInNsfwSubreddits; private boolean disableNsfwForever; private boolean manuallyCheckDisableNsfwForever = true; public NsfwAndSpoilerFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentNsfwAndSpoilerBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); applyCustomTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.getRoot().setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } binding.getRoot().setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); if (mActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mActivity.typeface); } boolean enableNsfw = nsfwAndBlurringSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.NSFW_BASE, false); blurNsfw = nsfwAndBlurringSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.BLUR_NSFW_BASE, true); doNotBlurNsfwInNsfwSubreddits = nsfwAndBlurringSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.DO_NOT_BLUR_NSFW_IN_NSFW_SUBREDDITS, false); boolean blurSpoiler = nsfwAndBlurringSharedPreferences.getBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.BLUR_SPOILER_BASE, false); disableNsfwForever = sharedPreferences.getBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, false); if (enableNsfw) { binding.blurNsfwLinearLayoutNsfwAndSpoilerFragment.setVisibility(View.VISIBLE); binding.doNotBlurNsfwInNsfwSubredditsLinearLayoutNsfwAndSpoilerFragment.setVisibility(View.VISIBLE); } binding.enableNsfwSwitchNsfwAndSpoilerFragment.setChecked(enableNsfw); binding.blurNsfwSwitchNsfwAndSpoilerFragment.setChecked(blurNsfw); binding.doNotBlurNsfwInNsfwSubredditsSwitchNsfwAndSpoilerFragment.setChecked(doNotBlurNsfwInNsfwSubreddits); binding.blurSpoilerSwitchNsfwAndSpoilerFragment.setChecked(blurSpoiler); binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.setChecked(disableNsfwForever); binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.setEnabled(!disableNsfwForever); if (disableNsfwForever) { binding.disableNsfwForeverTextViewNsfwAndSpoilerFragment.setTextColor(mActivity.customThemeWrapper.getSecondaryTextColor()); binding.disableNsfwForeverLinearLayoutNsfwAndSpoilerFragment.setEnabled(false); } binding.enableNsfwLinearLayoutNsfwAndSpoilerFragment.setOnClickListener(view -> binding.enableNsfwSwitchNsfwAndSpoilerFragment.performClick()); binding.enableNsfwSwitchNsfwAndSpoilerFragment.setOnCheckedChangeListener((compoundButton, b) -> { nsfwAndBlurringSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.NSFW_BASE, b).apply(); if (b) { binding.blurNsfwLinearLayoutNsfwAndSpoilerFragment.setVisibility(View.VISIBLE); binding.doNotBlurNsfwInNsfwSubredditsLinearLayoutNsfwAndSpoilerFragment.setVisibility(View.VISIBLE); } else { binding.blurNsfwLinearLayoutNsfwAndSpoilerFragment.setVisibility(View.GONE); binding.doNotBlurNsfwInNsfwSubredditsLinearLayoutNsfwAndSpoilerFragment.setVisibility(View.GONE); } EventBus.getDefault().post(new ChangeNSFWEvent(b)); }); binding.blurNsfwLinearLayoutNsfwAndSpoilerFragment.setOnClickListener(view -> binding.blurNsfwSwitchNsfwAndSpoilerFragment.performClick()); binding.blurNsfwSwitchNsfwAndSpoilerFragment.setOnCheckedChangeListener((compoundButton, b) -> { nsfwAndBlurringSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.BLUR_NSFW_BASE, b).apply(); EventBus.getDefault().post(new ChangeNSFWBlurEvent(b, doNotBlurNsfwInNsfwSubreddits)); }); binding.doNotBlurNsfwInNsfwSubredditsLinearLayoutNsfwAndSpoilerFragment.setOnClickListener(view -> { binding.doNotBlurNsfwInNsfwSubredditsSwitchNsfwAndSpoilerFragment.performClick(); }); binding.doNotBlurNsfwInNsfwSubredditsSwitchNsfwAndSpoilerFragment.setOnCheckedChangeListener((compoundButton, b) -> { nsfwAndBlurringSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.DO_NOT_BLUR_NSFW_IN_NSFW_SUBREDDITS, b).apply(); EventBus.getDefault().post(new ChangeNSFWBlurEvent(blurNsfw, b)); }); binding.blurSpoilerLinearLayoutNsfwAndSpoilerFragment.setOnClickListener(view -> binding.blurSpoilerSwitchNsfwAndSpoilerFragment.performClick()); binding.blurSpoilerSwitchNsfwAndSpoilerFragment.setOnCheckedChangeListener((compoundButton, b) -> { nsfwAndBlurringSharedPreferences.edit().putBoolean((mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT) ? "" : mActivity.accountName) + SharedPreferencesUtils.BLUR_SPOILER_BASE, b).apply(); EventBus.getDefault().post(new ChangeSpoilerBlurEvent(b)); }); binding.disableNsfwForeverLinearLayoutNsfwAndSpoilerFragment.setOnClickListener(view -> { binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.performClick(); }); binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.setOnCheckedChangeListener((compoundButton, b) -> { if (manuallyCheckDisableNsfwForever) { manuallyCheckDisableNsfwForever = false; new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(R.string.warning) .setMessage(R.string.disable_over_18_forever_message) .setPositiveButton(R.string.yes, (dialogInterface, i) -> { sharedPreferences.edit().putBoolean(SharedPreferencesUtils.DISABLE_NSFW_FOREVER, true).apply(); disableNsfwForever = true; binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.setEnabled(false); binding.disableNsfwForeverLinearLayoutNsfwAndSpoilerFragment.setEnabled(false); binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.setChecked(true); binding.disableNsfwForeverTextViewNsfwAndSpoilerFragment.setTextColor(mActivity.customThemeWrapper.getSecondaryTextColor()); EventBus.getDefault().post(new ChangeNSFWEvent(false)); }) .setNegativeButton(R.string.no, (dialogInterface, i) -> { binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.setChecked(false); manuallyCheckDisableNsfwForever = true; }) .setOnDismissListener(dialogInterface -> { if (!disableNsfwForever) { binding.disableNsfwForeverSwitchNsfwAndSpoilerFragment.setChecked(false); } manuallyCheckDisableNsfwForever = true; }) .show(); } }); TextView messageTextView = new TextView(mActivity); int padding = (int) Utils.convertDpToPixel(24, mActivity); messageTextView.setPaddingRelative(padding, padding, padding, padding); SpannableString message = new SpannableString(getString(R.string.warning_message_nsfw_content, "https://www.redditinc.com/policies/user-agreement", "https://github.com/cygnusx-1-org/continuum")); Linkify.addLinks(message, Linkify.WEB_URLS); messageTextView.setMovementMethod(BetterLinkMovementMethod.newInstance().setOnLinkClickListener((textView, url) -> { Intent intent = new Intent(mActivity, LinkResolverActivity.class); intent.setData(Uri.parse(url)); startActivity(intent); return true; })); messageTextView.setLinkTextColor(getResources().getColor(R.color.colorAccent)); messageTextView.setText(message); new MaterialAlertDialogBuilder(mActivity, R.style.MaterialAlertDialogTheme) .setTitle(getString(R.string.warning)) .setView(messageTextView) .setPositiveButton(R.string.agree, (dialogInterface, i) -> dialogInterface.dismiss()) .setNegativeButton(R.string.do_not_agree, (dialogInterface, i) -> mActivity.triggerBackPress()) .setCancelable(false) .show(); return binding.getRoot(); } private void applyCustomTheme() { int primaryTextColor = mActivity.customThemeWrapper.getPrimaryTextColor(); binding.enableNsfwTextViewNsfwAndSpoilerFragment.setCompoundDrawablesWithIntrinsicBounds(Utils.getTintedDrawable(mActivity, R.drawable.ic_nsfw_on_day_night_24dp, mActivity.customThemeWrapper.getPrimaryIconColor()), null, null, null); binding.enableNsfwTextViewNsfwAndSpoilerFragment.setTextColor(primaryTextColor); binding.blurNsfwTextViewNsfwAndSpoilerFragment.setTextColor(primaryTextColor); binding.doNotBlurNsfwTextViewNsfwAndSpoilerFragment.setTextColor(primaryTextColor); binding.blurSpoilerTextViewNsfwAndSpoilerFragment.setTextColor(primaryTextColor); binding.dangerousTextViewNsfwAndSpoilerFragment.setTextColor(primaryTextColor); binding.disableNsfwForeverTextViewNsfwAndSpoilerFragment.setTextColor(primaryTextColor); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/NumberOfColumnsInPostFeedPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.SharedPreferences; import android.os.Bundle; import androidx.preference.Preference; import androidx.preference.PreferenceManager; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class NumberOfColumnsInPostFeedPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.number_of_columns_in_post_feed_preferences, rootKey); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); boolean foldEnabled = sharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_FOLD_SUPPORT, false); Preference portraitUnfolded = findPreference(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_UNFOLDED); if (portraitUnfolded != null) { portraitUnfolded.setVisible(foldEnabled); } Preference landscapeUnfolded = findPreference(SharedPreferencesUtils.NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_UNFOLDED); if (landscapeUnfolded != null) { landscapeUnfolded.setVisible(foldEnabled); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/PostDetailsPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import androidx.preference.PreferenceManager; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class PostDetailsPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { PreferenceManager preferenceManager = getPreferenceManager(); preferenceManager.setSharedPreferencesName(SharedPreferencesUtils.POST_DETAILS_SHARED_PREFERENCES_FILE); setPreferencesFromResource(R.xml.post_details_preferences, rootKey); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/PostHistoryFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import java.lang.reflect.Field; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.databinding.FragmentPostHistoryBinding; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class PostHistoryFragment extends Fragment { private FragmentPostHistoryBinding binding; @Inject @Named("post_history") SharedPreferences postHistorySharedPreferences; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject Executor mExecutor; private SettingsActivity mActivity; public PostHistoryFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment binding = FragmentPostHistoryBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); binding.getRoot().setBackgroundColor(mActivity.customThemeWrapper.getBackgroundColor()); applyCustomTheme(); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.getRoot().setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } if (mActivity.typeface != null) { Utils.setFontToAllTextViews(binding.getRoot(), mActivity.typeface); } boolean isAnonymous = mActivity.accountName.equals(Account.ANONYMOUS_ACCOUNT); if (isAnonymous) { binding.infoTextViewPostHistoryFragment.setText(R.string.only_for_logged_in_user); binding.markPostsAsReadLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.readPostsLimitLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.readPostsLimitTextInputLayoutPostHistoryFragment.setVisibility(View.GONE); binding.markPostsAsReadAfterVotingLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.markPostsAsReadOnScrollLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyInSubredditsLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyInUsersLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyInSearchLinearLayoutPostHistoryFragment.setVisibility(View.GONE); return binding.getRoot(); } binding.markPostsAsReadSwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.MARK_POSTS_AS_READ_BASE, false)); binding.readPostsLimitSwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.READ_POSTS_LIMIT_ENABLED, true)); binding.readPostsLimitTextInputEditTextPostHistoryFragment.setText(String.valueOf(postHistorySharedPreferences.getInt( mActivity.accountName + SharedPreferencesUtils.READ_POSTS_LIMIT, 500))); binding.markPostsAsReadAfterVotingSwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.MARK_POSTS_AS_READ_AFTER_VOTING_BASE, false)); binding.markPostsAsReadOnScrollSwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.MARK_POSTS_AS_READ_ON_SCROLL_BASE, false)); binding.hideReadPostsAutomaticallySwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_BASE, false)); binding.hideReadPostsAutomaticallyInSubredditsSwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_SUBREDDITS_BASE, false)); binding.hideReadPostsAutomaticallyInUsersSwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_USERS_BASE, false)); binding.hideReadPostsAutomaticallyInSearchSwitchPostHistoryFragment.setChecked(postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_SEARCH_BASE, false)); updateOptions(); binding.markPostsAsReadLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.markPostsAsReadSwitchPostHistoryFragment.performClick()); binding.markPostsAsReadSwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> { postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.MARK_POSTS_AS_READ_BASE, b).apply(); updateOptions(); }); binding.readPostsLimitLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.readPostsLimitSwitchPostHistoryFragment.performClick()); binding.readPostsLimitSwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> { postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.READ_POSTS_LIMIT_ENABLED, b).apply(); updateOptions(); }); binding.readPostsLimitTextInputEditTextPostHistoryFragment.setOnFocusChangeListener((view, b) -> { if (!b) { String readPostsLimitString = binding.readPostsLimitTextInputEditTextPostHistoryFragment.getText().toString(); if (readPostsLimitString.isEmpty()) { binding.readPostsLimitTextInputEditTextPostHistoryFragment.setText("500"); } else { int readPostsLimit = Integer.parseInt(readPostsLimitString); if (readPostsLimit < 100) { binding.readPostsLimitTextInputEditTextPostHistoryFragment.setText("100"); } } postHistorySharedPreferences.edit().putInt(mActivity.accountName + SharedPreferencesUtils.READ_POSTS_LIMIT, Integer.parseInt(binding.readPostsLimitTextInputEditTextPostHistoryFragment.getText().toString())).apply(); } }); binding.markPostsAsReadAfterVotingLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.markPostsAsReadAfterVotingSwitchPostHistoryFragment.performClick()); binding.markPostsAsReadAfterVotingSwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.MARK_POSTS_AS_READ_AFTER_VOTING_BASE, b).apply()); binding.markPostsAsReadOnScrollLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.markPostsAsReadOnScrollSwitchPostHistoryFragment.performClick()); binding.markPostsAsReadOnScrollSwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.MARK_POSTS_AS_READ_ON_SCROLL_BASE, b).apply()); binding.hideReadPostsAutomaticallyLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.hideReadPostsAutomaticallySwitchPostHistoryFragment.performClick()); binding.hideReadPostsAutomaticallySwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> { postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_BASE, b).apply(); updateOptions(); }); binding.hideReadPostsAutomaticallyInSubredditsLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.hideReadPostsAutomaticallyInSubredditsSwitchPostHistoryFragment.performClick()); binding.hideReadPostsAutomaticallyInSubredditsSwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_SUBREDDITS_BASE, b).apply()); binding.hideReadPostsAutomaticallyInUsersLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.hideReadPostsAutomaticallyInUsersSwitchPostHistoryFragment.performClick()); binding.hideReadPostsAutomaticallyInUsersSwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_USERS_BASE, b).apply()); binding.hideReadPostsAutomaticallyInSearchLinearLayoutPostHistoryFragment.setOnClickListener(view -> binding.hideReadPostsAutomaticallyInSearchSwitchPostHistoryFragment.performClick()); binding.hideReadPostsAutomaticallyInSearchSwitchPostHistoryFragment.setOnCheckedChangeListener((compoundButton, b) -> postHistorySharedPreferences.edit().putBoolean(mActivity.accountName + SharedPreferencesUtils.HIDE_READ_POSTS_AUTOMATICALLY_IN_SEARCH_BASE, b).apply()); return binding.getRoot(); } private void updateOptions() { if (binding.markPostsAsReadSwitchPostHistoryFragment.isChecked()) { binding.readPostsLimitLinearLayoutPostHistoryFragment.setVisibility(View.VISIBLE); binding.readPostsLimitTextInputLayoutPostHistoryFragment.setVisibility(View.VISIBLE); binding.markPostsAsReadAfterVotingLinearLayoutPostHistoryFragment.setVisibility(View.VISIBLE); binding.markPostsAsReadOnScrollLinearLayoutPostHistoryFragment.setVisibility(View.VISIBLE); binding.hideReadPostsAutomaticallyLinearLayoutPostHistoryFragment.setVisibility(View.VISIBLE); binding.hideReadPostsAutomaticallyInSubredditsLinearLayoutPostHistoryFragment.setVisibility(binding.hideReadPostsAutomaticallySwitchPostHistoryFragment.isChecked() ? View.VISIBLE : View.GONE); binding.hideReadPostsAutomaticallyInUsersLinearLayoutPostHistoryFragment.setVisibility(binding.hideReadPostsAutomaticallySwitchPostHistoryFragment.isChecked() ? View.VISIBLE : View.GONE); binding.hideReadPostsAutomaticallyInSearchLinearLayoutPostHistoryFragment.setVisibility(binding.hideReadPostsAutomaticallySwitchPostHistoryFragment.isChecked() ? View.VISIBLE : View.GONE); boolean limitReadPosts = postHistorySharedPreferences.getBoolean( mActivity.accountName + SharedPreferencesUtils.READ_POSTS_LIMIT_ENABLED, true); binding.readPostsLimitTextInputLayoutPostHistoryFragment.setVisibility(limitReadPosts ? View.VISIBLE : View.GONE); } else { binding.readPostsLimitLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.readPostsLimitTextInputLayoutPostHistoryFragment.setVisibility(View.GONE); binding.markPostsAsReadAfterVotingLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.markPostsAsReadOnScrollLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyInSubredditsLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyInUsersLinearLayoutPostHistoryFragment.setVisibility(View.GONE); binding.hideReadPostsAutomaticallyInSearchLinearLayoutPostHistoryFragment.setVisibility(View.GONE); } } private void applyCustomTheme() { binding.infoTextViewPostHistoryFragment.setTextColor(mActivity.customThemeWrapper.getSecondaryTextColor()); Drawable infoDrawable = Utils.getTintedDrawable(mActivity, R.drawable.ic_info_preference_day_night_24dp, mActivity.customThemeWrapper.getPrimaryIconColor()); binding.infoTextViewPostHistoryFragment.setCompoundDrawablesWithIntrinsicBounds(infoDrawable, null, null, null); int primaryTextColor = mActivity.customThemeWrapper.getPrimaryTextColor(); binding.markPostsAsReadTextViewPostHistoryFragment.setTextColor(primaryTextColor); binding.readPostsLimitTextViewPostHistoryFragment.setTextColor(primaryTextColor); binding.readPostsLimitTextInputLayoutPostHistoryFragment.setBoxStrokeColor(primaryTextColor); binding.readPostsLimitTextInputLayoutPostHistoryFragment.setDefaultHintTextColor(ColorStateList.valueOf(primaryTextColor)); binding.readPostsLimitTextInputEditTextPostHistoryFragment.setTextColor(primaryTextColor); binding.markPostsAsReadAfterVotingTextViewPostHistoryFragment.setTextColor(primaryTextColor); binding.markPostsAsReadOnScrollTextViewPostHistoryFragment.setTextColor(primaryTextColor); binding.hideReadPostsAutomaticallyTextViewPostHistoryFragment.setTextColor(primaryTextColor); binding.hideReadPostsAutomaticallyInSubredditsTextViewPostHistoryFragment.setTextColor(primaryTextColor); binding.hideReadPostsAutomaticallyInUsersTextViewPostHistoryFragment.setTextColor(primaryTextColor); binding.hideReadPostsAutomaticallyInSearchTextViewPostHistoryFragment.setTextColor(primaryTextColor); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { binding.readPostsLimitTextInputLayoutPostHistoryFragment.setCursorColor(ColorStateList.valueOf(primaryTextColor)); } else { setCursorDrawableColor(binding.readPostsLimitTextInputEditTextPostHistoryFragment, primaryTextColor); } } private void setCursorDrawableColor(EditText editText, int color) { try { Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); fCursorDrawableRes.setAccessible(true); int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); Field fEditor = TextView.class.getDeclaredField("mEditor"); fEditor.setAccessible(true); Object editor = fEditor.get(editText); Class clazz = editor.getClass(); Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); fCursorDrawable.setAccessible(true); Drawable[] drawables = new Drawable[2]; drawables[0] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[1] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); fCursorDrawable.set(editor, drawables); } catch (Throwable ignored) { } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); this.mActivity = (SettingsActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/PostPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import android.content.SharedPreferences; import androidx.preference.ListPreference; import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeCompactLayoutToolbarHiddenByDefaultEvent; import ml.docilealligator.infinityforreddit.events.ChangeDefaultLinkPostLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangeDefaultPostLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangeDefaultPostLayoutUnfoldedEvent; import ml.docilealligator.infinityforreddit.events.ChangeFixedHeightPreviewInCardEvent; import ml.docilealligator.infinityforreddit.events.ChangeHidePostFlairEvent; import ml.docilealligator.infinityforreddit.events.ChangeHidePostTypeEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideSubredditAndUserPrefixEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideTextPostContent; import ml.docilealligator.infinityforreddit.events.ChangeHideTheNumberOfCommentsEvent; import ml.docilealligator.infinityforreddit.events.ChangeHideTheNumberOfVotesEvent; import ml.docilealligator.infinityforreddit.events.ChangeLongPressToHideToolbarInCompactLayoutEvent; import ml.docilealligator.infinityforreddit.events.ChangeShowAbsoluteNumberOfVotesEvent; import ml.docilealligator.infinityforreddit.events.ShowDividerInCompactLayoutPreferenceEvent; import ml.docilealligator.infinityforreddit.events.ShowThumbnailOnTheLeftInCompactLayoutEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class PostPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.post_preferences, rootKey); ListPreference defaultPostLayoutList = findPreference(SharedPreferencesUtils.DEFAULT_POST_LAYOUT_KEY); ListPreference defaultPostLayoutUnfoldedList = findPreference(SharedPreferencesUtils.DEFAULT_POST_LAYOUT_UNFOLDED_KEY); ListPreference defaultLinkPostLayoutList = findPreference(SharedPreferencesUtils.DEFAULT_LINK_POST_LAYOUT_KEY); SwitchPreference showDividerInCompactLayoutSwitch = findPreference(SharedPreferencesUtils.SHOW_DIVIDER_IN_COMPACT_LAYOUT); SwitchPreference showThumbnailOnTheLeftInCompactLayoutSwitch = findPreference(SharedPreferencesUtils.SHOW_THUMBNAIL_ON_THE_LEFT_IN_COMPACT_LAYOUT); SwitchPreference showAbsoluteNumberOfVotesSwitch = findPreference(SharedPreferencesUtils.SHOW_ABSOLUTE_NUMBER_OF_VOTES); SwitchPreference longPressToHideToolbarInCompactLayoutSwitch = findPreference(SharedPreferencesUtils.LONG_PRESS_TO_HIDE_TOOLBAR_IN_COMPACT_LAYOUT); SwitchPreference postCompactLayoutToolbarHiddenByDefaultSwitch = findPreference(SharedPreferencesUtils.POST_COMPACT_LAYOUT_TOOLBAR_HIDDEN_BY_DEFAULT); SwitchPreference hidePostTypeSwitch = findPreference(SharedPreferencesUtils.HIDE_POST_TYPE); SwitchPreference hidePostFlairSwitch = findPreference(SharedPreferencesUtils.HIDE_POST_FLAIR); SwitchPreference hideSubredditAndUserPrefixSwitch = findPreference(SharedPreferencesUtils.HIDE_SUBREDDIT_AND_USER_PREFIX); SwitchPreference hideTheNumberOfVotesSwitch = findPreference(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_VOTES); SwitchPreference hideTheNumberOfCommentsSwitch = findPreference(SharedPreferencesUtils.HIDE_THE_NUMBER_OF_COMMENTS); SwitchPreference hideTextPostContentSwitch = findPreference(SharedPreferencesUtils.HIDE_TEXT_POST_CONTENT); SwitchPreference fixedHeightPreviewInCardSwitch = findPreference(SharedPreferencesUtils.FIXED_HEIGHT_PREVIEW_IN_CARD); if (defaultPostLayoutList != null) { defaultPostLayoutList.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeDefaultPostLayoutEvent(Integer.parseInt((String) newValue))); return true; }); } if (defaultPostLayoutUnfoldedList != null) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); boolean foldEnabled = sharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_FOLD_SUPPORT, false); defaultPostLayoutUnfoldedList.setVisible(foldEnabled); defaultPostLayoutUnfoldedList.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeDefaultPostLayoutUnfoldedEvent(Integer.parseInt((String) newValue))); return true; }); } if (defaultLinkPostLayoutList != null) { defaultLinkPostLayoutList.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeDefaultLinkPostLayoutEvent(Integer.parseInt((String) newValue))); return true; }); } if (showDividerInCompactLayoutSwitch != null) { showDividerInCompactLayoutSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ShowDividerInCompactLayoutPreferenceEvent((Boolean) newValue)); return true; }); } if (showThumbnailOnTheLeftInCompactLayoutSwitch != null) { showThumbnailOnTheLeftInCompactLayoutSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ShowThumbnailOnTheLeftInCompactLayoutEvent((Boolean) newValue)); return true; }); } if (showAbsoluteNumberOfVotesSwitch != null) { showAbsoluteNumberOfVotesSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeShowAbsoluteNumberOfVotesEvent((Boolean) newValue)); return true; }); } if (longPressToHideToolbarInCompactLayoutSwitch != null) { longPressToHideToolbarInCompactLayoutSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeLongPressToHideToolbarInCompactLayoutEvent((Boolean) newValue)); return true; }); } if (postCompactLayoutToolbarHiddenByDefaultSwitch != null) { postCompactLayoutToolbarHiddenByDefaultSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeCompactLayoutToolbarHiddenByDefaultEvent((Boolean) newValue)); return true; }); } if (hidePostTypeSwitch != null) { hidePostTypeSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHidePostTypeEvent((Boolean) newValue)); return true; }); } if (hidePostFlairSwitch != null) { hidePostFlairSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHidePostFlairEvent((Boolean) newValue)); return true; }); } if (hideSubredditAndUserPrefixSwitch != null) { hideSubredditAndUserPrefixSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHideSubredditAndUserPrefixEvent((Boolean) newValue)); return true; }); } if (hideTheNumberOfVotesSwitch != null) { hideTheNumberOfVotesSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHideTheNumberOfVotesEvent((Boolean) newValue)); return true; }); } if (hideTheNumberOfCommentsSwitch != null) { hideTheNumberOfCommentsSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHideTheNumberOfCommentsEvent((Boolean) newValue)); return true; }); } if (hideTextPostContentSwitch != null) { hideTextPostContentSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeHideTextPostContent((Boolean) newValue)); return true; }); } if (fixedHeightPreviewInCardSwitch != null) { fixedHeightPreviewInCardSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeFixedHeightPreviewInCardEvent((Boolean) newValue)); return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/ProxyPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import static ml.docilealligator.infinityforreddit.utils.Utils.HOSTNAME_REGEX; import android.os.Bundle; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.preference.EditTextPreference; import androidx.preference.PreferenceManager; import com.google.common.net.InetAddresses; import java.util.regex.Pattern; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class ProxyPreferenceFragment extends CustomFontPreferenceFragmentCompat { public ProxyPreferenceFragment() {} @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { PreferenceManager preferenceManager = getPreferenceManager(); preferenceManager.setSharedPreferencesName(SharedPreferencesUtils.PROXY_SHARED_PREFERENCES_FILE); setPreferencesFromResource(R.xml.proxy_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); EditTextPreference proxyHostnamePref = findPreference(SharedPreferencesUtils.PROXY_HOSTNAME); EditTextPreference proxyPortPref = findPreference(SharedPreferencesUtils.PROXY_PORT); if (proxyHostnamePref != null) { proxyHostnamePref.setOnPreferenceChangeListener(((preference, newValue) -> { boolean isHostname = Pattern.matches(HOSTNAME_REGEX, (String) newValue); boolean isInetAddress = InetAddresses.isInetAddress((String) newValue); if (!isInetAddress && !isHostname) { Toast.makeText(mActivity, R.string.not_a_valid_ip_or_hostname, Toast.LENGTH_SHORT).show(); return false; } return true; })); } if (proxyPortPref != null) { proxyPortPref.setOnPreferenceChangeListener(((preference, newValue) -> { try { int port = Integer.parseInt((String) newValue); if (port < 0 || port > 65535) { Toast.makeText(mActivity, R.string.not_a_valid_port, Toast.LENGTH_SHORT).show(); return false; } } catch (NumberFormatException e) { Toast.makeText(mActivity, R.string.not_a_valid_number, Toast.LENGTH_SHORT).show(); return false; } return true; })); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/SecurityPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import android.content.SharedPreferences; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.biometric.BiometricPrompt; import androidx.core.content.ContextCompat; import androidx.preference.ListPreference; import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeAppLockEvent; import ml.docilealligator.infinityforreddit.events.ChangeRequireAuthToAccountSectionEvent; import ml.docilealligator.infinityforreddit.events.ToggleSecureModeEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class SecurityPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("default") SharedPreferences sharedPreferences; String rootKey; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { this.rootKey = rootKey; } private void createPreferences() { PreferenceManager preferenceManager = getPreferenceManager(); preferenceManager.setSharedPreferencesName(SharedPreferencesUtils.SECURITY_SHARED_PREFERENCES_FILE); setPreferencesFromResource(R.xml.security_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); SwitchPreference requireAuthToAccountSectionSwitch = findPreference(SharedPreferencesUtils.REQUIRE_AUTHENTICATION_TO_GO_TO_ACCOUNT_SECTION_IN_NAVIGATION_DRAWER); SwitchPreference secureModeSwitch = findPreference(SharedPreferencesUtils.SECURE_MODE); SwitchPreference appLockSwitch = findPreference(SharedPreferencesUtils.APP_LOCK); ListPreference appLockTimeoutListPreference = findPreference(SharedPreferencesUtils.APP_LOCK_TIMEOUT); if (requireAuthToAccountSectionSwitch != null) { requireAuthToAccountSectionSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeRequireAuthToAccountSectionEvent((Boolean) newValue)); return true; }); } if (secureModeSwitch != null) { secureModeSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ToggleSecureModeEvent((Boolean) newValue)); return true; }); } if (appLockSwitch != null && appLockTimeoutListPreference != null) { appLockSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeAppLockEvent((Boolean) newValue, Long.parseLong(appLockTimeoutListPreference.getValue()))); return true; }); appLockTimeoutListPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeAppLockEvent(appLockSwitch.isChecked(), Long.parseLong((String) newValue))); return true; }); } applyStyle(); } @Override public void onResume() { super.onResume(); Executor executor = ContextCompat.getMainExecutor(mActivity); BiometricPrompt biometricPrompt = new BiometricPrompt(SecurityPreferenceFragment.this, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); createPreferences(); } @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { mActivity.triggerBackPress(); } }); BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle(mActivity.getString(R.string.unlock)) .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) .build(); biometricPrompt.authenticate(promptInfo); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/SettingsSearchFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import ml.docilealligator.infinityforreddit.activities.SettingsActivity; import ml.docilealligator.infinityforreddit.adapters.SettingsSearchAdapter; import ml.docilealligator.infinityforreddit.databinding.FragmentSettingsSearchBinding; public class SettingsSearchFragment extends Fragment { private FragmentSettingsSearchBinding binding; private SettingsActivity mActivity; private SettingsSearchAdapter mAdapter; @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (SettingsActivity) context; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentSettingsSearchBinding.inflate(inflater, container, false); return binding.getRoot(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); binding.rootFragmentSettingsSearch.setBackgroundColor( mActivity.customThemeWrapper.getBackgroundColor()); binding.searchEditTextSettingsSearchFragment.setTextColor( mActivity.customThemeWrapper.getPrimaryTextColor()); binding.searchEditTextSettingsSearchFragment.setHintTextColor( mActivity.customThemeWrapper.getSecondaryTextColor()); binding.clearSearchSettingsSearchFragment.setColorFilter( mActivity.customThemeWrapper.getPrimaryIconColor()); binding.emptyTextSettingsSearchFragment.setTextColor( mActivity.customThemeWrapper.getSecondaryTextColor()); mAdapter = new SettingsSearchAdapter(item -> { Fragment fragment = mActivity.getSupportFragmentManager() .getFragmentFactory() .instantiate(requireActivity().getClassLoader(), item.fragmentClass.getName()); mActivity.navigateToSettingsFragment(fragment, item.fragmentTitleResId); }); binding.recyclerViewSettingsSearchFragment.setLayoutManager( new LinearLayoutManager(mActivity)); binding.recyclerViewSettingsSearchFragment.setAdapter(mAdapter); binding.searchEditTextSettingsSearchFragment.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void afterTextChanged(Editable s) { String query = s.toString(); mAdapter.filter(query); binding.clearSearchSettingsSearchFragment.setVisibility( query.isEmpty() ? View.GONE : View.VISIBLE); updateEmptyState(); } }); binding.clearSearchSettingsSearchFragment.setOnClickListener(v -> binding.searchEditTextSettingsSearchFragment.setText("")); // Show keyboard binding.searchEditTextSettingsSearchFragment.requestFocus(); InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { imm.showSoftInput(binding.searchEditTextSettingsSearchFragment, InputMethodManager.SHOW_IMPLICIT); } } private void updateEmptyState() { boolean hasText = !binding.searchEditTextSettingsSearchFragment.getText().toString().isEmpty(); boolean isEmpty = mAdapter.isEmpty(); if (hasText && isEmpty) { binding.recyclerViewSettingsSearchFragment.setVisibility(View.GONE); binding.emptyTextSettingsSearchFragment.setVisibility(View.VISIBLE); } else { binding.recyclerViewSettingsSearchFragment.setVisibility(View.VISIBLE); binding.emptyTextSettingsSearchFragment.setVisibility(View.GONE); } } @Override public void onDestroyView() { super.onDestroyView(); // Hide keyboard when leaving InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null && binding != null) { imm.hideSoftInputFromWindow( binding.searchEditTextSettingsSearchFragment.getWindowToken(), 0); } binding = null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/SettingsSearchItem.java ================================================ package ml.docilealligator.infinityforreddit.settings; import androidx.fragment.app.Fragment; public class SettingsSearchItem { public final String title; public final String summary; public final String breadcrumb; public final Class fragmentClass; public final int fragmentTitleResId; public SettingsSearchItem(String title, String summary, String breadcrumb, Class fragmentClass, int fragmentTitleResId) { this.title = title; this.summary = summary; this.breadcrumb = breadcrumb; this.fragmentClass = fragmentClass; this.fragmentTitleResId = fragmentTitleResId; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/SettingsSearchRegistry.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import java.util.ArrayList; import java.util.Collections; import java.util.List; import ml.docilealligator.infinityforreddit.R; /** * Singleton that holds a flat list of all searchable settings items. * Call buildRegistry(context) once (e.g. in SettingsActivity.onCreate) to populate. */ public class SettingsSearchRegistry { private static SettingsSearchRegistry sInstance; private List mItems; private SettingsSearchRegistry() {} public static SettingsSearchRegistry getInstance() { if (sInstance == null) { sInstance = new SettingsSearchRegistry(); } return sInstance; } public List getItems() { return mItems != null ? mItems : Collections.emptyList(); } public void buildRegistry(Context ctx) { if (mItems != null) return; List items = new ArrayList<>(); addApiKeysPreferences(ctx, items); addNotificationPreferences(ctx, items); addInterfacePreferences(ctx, items); addFontPreferences(ctx, items); addImmersiveInterfacePreferences(ctx, items); addNavigationDrawerPreferences(ctx, items); addTimeFormatPreferences(ctx, items); addPostPreferences(ctx, items); addNumberOfColumnsPreferences(ctx, items); addPostDetailsPreferences(ctx, items); addCommentPreferences(ctx, items); addThemePreferences(ctx, items); addVideoPreferences(ctx, items); addGesturesAndButtonsPreferences(ctx, items); addSwipeActionPreferences(ctx, items); addSecurityPreferences(ctx, items); addDataSavingModePreferences(ctx, items); addProxyPreferences(ctx, items); addSortTypePreferences(ctx, items); addDownloadLocationPreferences(ctx, items); addMiscellaneousPreferences(ctx, items); addAdvancedPreferences(ctx, items); addAboutPreferences(ctx, items); addCreditsPreferences(ctx, items); addDebugPreferences(ctx, items); mItems = Collections.unmodifiableList(items); } // ------------------------------------------------------------------------- // Helper // ------------------------------------------------------------------------- private static void add(List items, String title, String summary, String breadcrumb, Class fragmentClass, int fragmentTitleResId) { items.add(new SettingsSearchItem(title, summary, breadcrumb, fragmentClass, fragmentTitleResId)); } // ------------------------------------------------------------------------- // API Keys // ------------------------------------------------------------------------- private void addApiKeysPreferences(Context c, List items) { String bc = c.getString(R.string.settings_api_keys_title); add(items, c.getString(R.string.settings_client_id_title), null, bc, APIKeysPreferenceFragment.class, R.string.settings_api_keys_title); add(items, c.getString(R.string.settings_giphy_api_key_title), null, bc, APIKeysPreferenceFragment.class, R.string.settings_api_keys_title); } // ------------------------------------------------------------------------- // Notifications // ------------------------------------------------------------------------- private void addNotificationPreferences(Context c, List items) { String bc = c.getString(R.string.settings_notification_master_title); add(items, c.getString(R.string.settings_notification_enable_notification_title), null, bc, NotificationPreferenceFragment.class, R.string.settings_notification_master_title); } // ------------------------------------------------------------------------- // Interface (top-level items + sub-screen navigation entries) // ------------------------------------------------------------------------- private void addInterfacePreferences(Context c, List items) { String bc = c.getString(R.string.settings_interface_title); add(items, c.getString(R.string.settings_font_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); add(items, c.getString(R.string.settings_navigation_drawer_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_customize_tabs_in_main_page_title), null, bc, CustomizeMainPageTabsFragment.class, R.string.settings_customize_tabs_in_main_page_title); add(items, c.getString(R.string.settings_customize_bottom_app_bar_title), null, bc, CustomizeBottomAppBarFragment.class, R.string.settings_customize_bottom_app_bar_title); add(items, c.getString(R.string.settings_hide_fab_in_post_feed), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); add(items, c.getString(R.string.settings_enable_bottom_app_bar_title), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); add(items, c.getString(R.string.settings_hide_subreddit_description_title), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); add(items, c.getString(R.string.settings_use_bottom_toolbar_in_media_viewer_title), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); add(items, c.getString(R.string.settings_default_search_result_tab), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); add(items, c.getString(R.string.settings_time_format_title), null, bc, TimeFormatPreferenceFragment.class, R.string.settings_time_format_title); add(items, c.getString(R.string.settings_category_post_title), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_post_details_title), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_category_comment_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_lazy_mode_interval_title), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); add(items, c.getString(R.string.settings_vote_buttons_on_the_right_title), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); add(items, c.getString(R.string.settings_show_absolute_number_of_votes_title), null, bc, InterfacePreferenceFragment.class, R.string.settings_interface_title); } // ------------------------------------------------------------------------- // Font // ------------------------------------------------------------------------- private void addFontPreferences(Context c, List items) { String parent = c.getString(R.string.settings_interface_title); String self = c.getString(R.string.settings_font_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_preview_font_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); add(items, c.getString(R.string.settings_font_family_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); add(items, c.getString(R.string.settings_font_size_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); add(items, c.getString(R.string.settings_title_font_family_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); add(items, c.getString(R.string.settings_title_font_size_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); add(items, c.getString(R.string.settings_content_font_family_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); add(items, c.getString(R.string.settings_content_font_size_title), null, bc, FontPreferenceFragment.class, R.string.settings_font_title); } // ------------------------------------------------------------------------- // Immersive Interface // ------------------------------------------------------------------------- private void addImmersiveInterfacePreferences(Context c, List items) { String parent = c.getString(R.string.settings_interface_title); String self = c.getString(R.string.settings_immersive_interface_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_immersive_interface_title), c.getString(R.string.settings_immersive_interface_summary), bc, ImmersiveInterfacePreferenceFragment.class, R.string.settings_immersive_interface_title); add(items, c.getString(R.string.settings_disable_immersive_interface_in_landscape_mode), null, bc, ImmersiveInterfacePreferenceFragment.class, R.string.settings_immersive_interface_title); } // ------------------------------------------------------------------------- // Navigation Drawer // ------------------------------------------------------------------------- private void addNavigationDrawerPreferences(Context c, List items) { String parent = c.getString(R.string.settings_interface_title); String self = c.getString(R.string.settings_navigation_drawer_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_show_avatar_on_the_right), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_collapse_account_section_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_collapse_reddit_section_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_collapse_post_section_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_collapse_preferences_section_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_collapse_favorite_subreddits_section_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_collapse_subscribed_subreddits_section_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_hide_favorite_subreddits_sections_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_hide_subscribed_subreddits_sections_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); add(items, c.getString(R.string.settings_navigation_drawer_enable_hide_karma_title), null, bc, NavigationDrawerPreferenceFragment.class, R.string.settings_navigation_drawer_title); } // ------------------------------------------------------------------------- // Time Format // ------------------------------------------------------------------------- private void addTimeFormatPreferences(Context c, List items) { String parent = c.getString(R.string.settings_interface_title); String self = c.getString(R.string.settings_time_format_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_show_elapsed_time), null, bc, TimeFormatPreferenceFragment.class, R.string.settings_time_format_title); add(items, c.getString(R.string.settings_time_format_title), null, bc, TimeFormatPreferenceFragment.class, R.string.settings_time_format_title); } // ------------------------------------------------------------------------- // Post // ------------------------------------------------------------------------- private void addPostPreferences(Context c, List items) { String parent = c.getString(R.string.settings_interface_title); String self = c.getString(R.string.settings_category_post_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_default_post_layout), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_default_post_layout_unfolded), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_default_link_post_layout), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_number_of_columns_in_post_feed_title), null, bc, NumberOfColumnsInPostFeedPreferenceFragment.class, R.string.settings_number_of_columns_in_post_feed_title); add(items, c.getString(R.string.settings_hide_post_type), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_hide_post_flair), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_hide_subreddit_and_user_prefix), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_hide_the_number_of_votes), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_hide_the_number_of_comments), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_hide_text_post_content), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_fixed_height_preview_in_card_title), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_show_divider_in_compact_layout), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_show_thumbnail_on_the_left_in_compact_layout), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_long_press_to_hide_toolbar_in_compact_layout_title), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_post_compact_layout_toolbar_hidden_by_default_title), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); add(items, c.getString(R.string.settings_click_to_show_media_in_gallery_layout), null, bc, PostPreferenceFragment.class, R.string.settings_category_post_title); } // ------------------------------------------------------------------------- // Number of Columns // ------------------------------------------------------------------------- private void addNumberOfColumnsPreferences(Context c, List items) { String grandparent = c.getString(R.string.settings_interface_title); String parent = c.getString(R.string.settings_category_post_title); String self = c.getString(R.string.settings_number_of_columns_in_post_feed_title); String bc = grandparent + " \u203a " + parent + " \u203a " + self; Class frag = NumberOfColumnsInPostFeedPreferenceFragment.class; int titleRes = R.string.settings_number_of_columns_in_post_feed_title; add(items, c.getString(R.string.settings_number_of_columns_in_post_feed_portrait_title), null, bc, NumberOfColumnsInPostFeedPreferenceFragment.class, titleRes); add(items, c.getString(R.string.settings_number_of_columns_in_post_feed_landscape_title), null, bc, NumberOfColumnsInPostFeedPreferenceFragment.class, titleRes); add(items, c.getString(R.string.settings_number_of_columns_in_post_feed_unfolded_portrait_title), null, bc, NumberOfColumnsInPostFeedPreferenceFragment.class, titleRes); add(items, c.getString(R.string.settings_number_of_columns_in_post_feed_unfolded_landscape_title), null, bc, NumberOfColumnsInPostFeedPreferenceFragment.class, titleRes); } // ------------------------------------------------------------------------- // Post Details // ------------------------------------------------------------------------- private void addPostDetailsPreferences(Context c, List items) { String parent = c.getString(R.string.settings_interface_title); String self = c.getString(R.string.settings_post_details_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_separate_post_and_comments_in_landscape_mode_title), c.getString(R.string.settings_separate_post_and_comments_summary), bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_swap_post_and_comments_in_split_mode_title), c.getString(R.string.settings_swap_post_and_comments_in_split_mode_summary), bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_hide_post_type), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_hide_post_flair), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_hide_upvote_ratio_title), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_hide_subreddit_and_user_prefix), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_hide_the_number_of_votes), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_hide_the_number_of_comments), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_hide_fab_in_post_details), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); add(items, c.getString(R.string.settings_embedded_media_type_title), null, bc, PostDetailsPreferenceFragment.class, R.string.settings_post_details_title); } // ------------------------------------------------------------------------- // Comment // ------------------------------------------------------------------------- private void addCommentPreferences(Context c, List items) { String parent = c.getString(R.string.settings_interface_title); String self = c.getString(R.string.settings_category_comment_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_show_top_level_comments_first_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_show_comment_divider_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_show_only_one_comment_level_indicator), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_comment_toolbar_hidden), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_comment_toolbar_hide_on_click), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_fully_collapse_comment_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_remember_comment_scroll_position), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_show_author_avatar_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_show_user_prefix_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_show_fewer_toolbar_options_threshold_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); add(items, c.getString(R.string.settings_embedded_media_type_title), null, bc, CommentPreferenceFragment.class, R.string.settings_category_comment_title); } // ------------------------------------------------------------------------- // Theme // ------------------------------------------------------------------------- private void addThemePreferences(Context c, List items) { String bc = c.getString(R.string.settings_theme_title); add(items, c.getString(R.string.settings_theme_title), null, bc, ThemePreferenceFragment.class, R.string.settings_theme_title); add(items, c.getString(R.string.settings_amoled_dark_title), null, bc, ThemePreferenceFragment.class, R.string.settings_theme_title); add(items, c.getString(R.string.settings_manage_themes_title), null, bc, ThemePreferenceFragment.class, R.string.settings_theme_title); add(items, c.getString(R.string.settings_enable_material_you_title), c.getString(R.string.settings_enable_material_you_summary), bc, ThemePreferenceFragment.class, R.string.settings_theme_title); add(items, c.getString(R.string.settings_apply_material_you_title), c.getString(R.string.settings_apply_material_you_summary), bc, ThemePreferenceFragment.class, R.string.settings_theme_title); } // ------------------------------------------------------------------------- // Video // ------------------------------------------------------------------------- private void addVideoPreferences(Context c, List items) { String bc = c.getString(R.string.settigns_video_title); add(items, c.getString(R.string.settings_mute_video_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_mute_nsfw_video_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_video_player_ignore_nav_bar_title), c.getString(R.string.settings_video_player_ignore_nav_bar_summary), bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_video_player_automatic_landscape_orientation), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_loop_video_title), c.getString(R.string.settings_loop_video_summary), bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_default_playback_speed_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_reddit_video_default_resolution), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_video_autoplay_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_simultaneous_autoplay_limit_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_legacy_autoplay_video_controller_ui_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_mute_autoplaying_videos_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_remember_muting_option_in_post_feed), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_autoplay_nsfw_videos_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_easier_to_watch_in_full_screen_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_start_autoplay_visible_area_offset_portrait_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); add(items, c.getString(R.string.settings_start_autoplay_visible_area_offset_landscape_title), null, bc, VideoPreferenceFragment.class, R.string.settigns_video_title); } // ------------------------------------------------------------------------- // Gestures & Buttons // ------------------------------------------------------------------------- private void addGesturesAndButtonsPreferences(Context c, List items) { String bc = c.getString(R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_swipe_to_go_back_title), c.getString(R.string.settings_swipe_to_go_back_summary), bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_lock_toolbar_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_volume_keys_navigate_comments_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_volume_keys_navigate_posts_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_pull_to_refresh_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_swipe_between_posts_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_tab_switching_sensitivity), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_swipe_right_to_go_back_sensitivity), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_swipe_action_sensitivity_in_comments), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_navigation_drawer_swipe_area), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_swipe_vertically_to_go_back_from_media_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_pinch_to_zoom_video_title), c.getString(R.string.settings_experimental_feature), bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_lock_jump_to_next_top_level_comment_button_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_swap_tap_and_long_title), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.long_press_post_non_media_area), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.long_press_post_media), null, bc, GesturesAndButtonsPreferenceFragment.class, R.string.settings_gestures_and_buttons_title); add(items, c.getString(R.string.settings_swipe_action_title), null, bc, SwipeActionPreferenceFragment.class, R.string.settings_swipe_action_title); } // ------------------------------------------------------------------------- // Swipe Action // ------------------------------------------------------------------------- private void addSwipeActionPreferences(Context c, List items) { String parent = c.getString(R.string.settings_gestures_and_buttons_title); String self = c.getString(R.string.settings_swipe_action_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_enable_swipe_action_title), null, bc, SwipeActionPreferenceFragment.class, R.string.settings_swipe_action_title); add(items, c.getString(R.string.settings_swipe_action_swipe_left_title), null, bc, SwipeActionPreferenceFragment.class, R.string.settings_swipe_action_title); add(items, c.getString(R.string.settings_swipe_action_swipe_right_title), null, bc, SwipeActionPreferenceFragment.class, R.string.settings_swipe_action_title); add(items, c.getString(R.string.settings_swipe_action_threshold), null, bc, SwipeActionPreferenceFragment.class, R.string.settings_swipe_action_title); add(items, c.getString(R.string.settings_swipe_action_haptic_feedback_title), null, bc, SwipeActionPreferenceFragment.class, R.string.settings_swipe_action_title); add(items, c.getString(R.string.settings_disable_swiping_between_tabs_title), null, bc, SwipeActionPreferenceFragment.class, R.string.settings_swipe_action_title); } // ------------------------------------------------------------------------- // Security // ------------------------------------------------------------------------- private void addSecurityPreferences(Context c, List items) { String bc = c.getString(R.string.settings_security_title); add(items, c.getString(R.string.settings_require_authentication_to_show_accounts), null, bc, SecurityPreferenceFragment.class, R.string.settings_security_title); add(items, c.getString(R.string.settings_secure_mode_title), c.getString(R.string.settings_secure_mode_summary), bc, SecurityPreferenceFragment.class, R.string.settings_security_title); add(items, c.getString(R.string.settings_app_lock_title), c.getString(R.string.settings_app_lock_summary), bc, SecurityPreferenceFragment.class, R.string.settings_security_title); add(items, c.getString(R.string.settings_app_lock_timeout_title), null, bc, SecurityPreferenceFragment.class, R.string.settings_security_title); } // ------------------------------------------------------------------------- // Data Saving Mode // ------------------------------------------------------------------------- private void addDataSavingModePreferences(Context c, List items) { String bc = c.getString(R.string.settings_data_saving_mode); add(items, c.getString(R.string.settings_data_saving_mode), null, bc, DataSavingModePreferenceFragment.class, R.string.settings_data_saving_mode); add(items, c.getString(R.string.settings_disable_image_preview_title), null, bc, DataSavingModePreferenceFragment.class, R.string.settings_data_saving_mode); add(items, c.getString(R.string.settings_only_disable_preview_in_video_and_gif_posts_title), null, bc, DataSavingModePreferenceFragment.class, R.string.settings_data_saving_mode); add(items, c.getString(R.string.settings_reddit_video_default_resolution), null, bc, DataSavingModePreferenceFragment.class, R.string.settings_data_saving_mode); } // ------------------------------------------------------------------------- // Proxy // ------------------------------------------------------------------------- private void addProxyPreferences(Context c, List items) { String bc = c.getString(R.string.settings_proxy_title); add(items, c.getString(R.string.settings_proxy_enabled), c.getString(R.string.restart_app_see_changes), bc, ProxyPreferenceFragment.class, R.string.settings_proxy_title); add(items, c.getString(R.string.settings_proxy_type), null, bc, ProxyPreferenceFragment.class, R.string.settings_proxy_title); add(items, c.getString(R.string.settings_proxy_hostname), null, bc, ProxyPreferenceFragment.class, R.string.settings_proxy_title); add(items, c.getString(R.string.settings_proxy_port), null, bc, ProxyPreferenceFragment.class, R.string.settings_proxy_title); } // ------------------------------------------------------------------------- // Sort Type // ------------------------------------------------------------------------- private void addSortTypePreferences(Context c, List items) { String bc = c.getString(R.string.settings_sort_type_title); add(items, c.getString(R.string.settings_save_sort_type_title), null, bc, SortTypePreferenceFragment.class, R.string.settings_sort_type_title); add(items, c.getString(R.string.settings_subreddit_default_sort_type_title), null, bc, SortTypePreferenceFragment.class, R.string.settings_sort_type_title); add(items, c.getString(R.string.settings_subreddit_default_sort_time_title), null, bc, SortTypePreferenceFragment.class, R.string.settings_sort_type_title); add(items, c.getString(R.string.settings_user_default_sort_type_title), null, bc, SortTypePreferenceFragment.class, R.string.settings_sort_type_title); add(items, c.getString(R.string.settings_user_default_sort_time_title), null, bc, SortTypePreferenceFragment.class, R.string.settings_sort_type_title); add(items, c.getString(R.string.settings_respect_subreddit_recommended_comment_sort_type_title), c.getString(R.string.settings_respect_subreddit_recommended_comment_sort_type_summary), bc, SortTypePreferenceFragment.class, R.string.settings_sort_type_title); } // ------------------------------------------------------------------------- // Download Location // ------------------------------------------------------------------------- private void addDownloadLocationPreferences(Context c, List items) { String bc = c.getString(R.string.settings_download_location_title); add(items, c.getString(R.string.settings_image_download_location_title), null, bc, DownloadLocationPreferenceFragment.class, R.string.settings_download_location_title); add(items, c.getString(R.string.settings_gif_download_location_title), null, bc, DownloadLocationPreferenceFragment.class, R.string.settings_download_location_title); add(items, c.getString(R.string.settings_video_download_location_title), null, bc, DownloadLocationPreferenceFragment.class, R.string.settings_download_location_title); add(items, c.getString(R.string.settings_separate_folder_for_each_subreddit), null, bc, DownloadLocationPreferenceFragment.class, R.string.settings_download_location_title); add(items, c.getString(R.string.settings_save_nsfw_media_in_different_folder_title), null, bc, DownloadLocationPreferenceFragment.class, R.string.settings_download_location_title); add(items, c.getString(R.string.settings_nsfw_download_location_title), null, bc, DownloadLocationPreferenceFragment.class, R.string.settings_download_location_title); } // ------------------------------------------------------------------------- // Miscellaneous // ------------------------------------------------------------------------- private void addMiscellaneousPreferences(Context c, List items) { String bc = c.getString(R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_save_front_page_scrolled_position_title), c.getString(R.string.settings_save_front_page_scrolled_position_summary), bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_link_handler_title), null, bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_main_page_back_button_action), null, bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_enable_search_history_title), c.getString(R.string.only_for_logged_in_user), bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_disable_profile_avatar_animation_title), null, bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_language_title), null, bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_enable_fold_support_title), null, bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); add(items, c.getString(R.string.settings_post_feed_max_resolution_title), null, bc, MiscellaneousPreferenceFragment.class, R.string.settings_miscellaneous_title); } // ------------------------------------------------------------------------- // Advanced // ------------------------------------------------------------------------- private void addAdvancedPreferences(Context c, List items) { String bc = c.getString(R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_all_subreddits_data_in_database_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_all_users_data_in_database_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_all_sort_type_data_in_database_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_all_post_layout_data_in_database_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_all_themes_in_database_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_front_page_scrolled_positions_in_database_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_read_posts_in_database_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_delete_all_legacy_settings_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_reset_all_settings_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_backup_settings_title), c.getString(R.string.settings_backup_settings_summary), bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_restore_settings_title), null, bc, AdvancedPreferenceFragment.class, R.string.settings_advanced_master_title); add(items, c.getString(R.string.settings_crash_reports_title), null, bc, CrashReportsFragment.class, R.string.settings_crash_reports_title); } // ------------------------------------------------------------------------- // About // ------------------------------------------------------------------------- private void addAboutPreferences(Context c, List items) { String bc = c.getString(R.string.settings_about_master_title); add(items, c.getString(R.string.settings_acknowledgement_master_title), null, bc, AcknowledgementFragment.class, R.string.settings_acknowledgement_master_title); add(items, c.getString(R.string.settings_credits_master_title), null, bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_translation_title), c.getString(R.string.settings_translation_summary), bc, TranslationFragment.class, R.string.settings_translation_title); add(items, c.getString(R.string.settings_open_source_title), c.getString(R.string.settings_open_source_summary), bc, AboutPreferenceFragment.class, R.string.settings_about_master_title); add(items, c.getString(R.string.settings_email_title), c.getString(R.string.settings_email_summary), bc, AboutPreferenceFragment.class, R.string.settings_about_master_title); add(items, c.getString(R.string.settings_reddit_account_title), c.getString(R.string.settings_reddit_account_summary), bc, AboutPreferenceFragment.class, R.string.settings_about_master_title); add(items, c.getString(R.string.settings_subreddit_title), c.getString(R.string.settings_subreddit_summary), bc, AboutPreferenceFragment.class, R.string.settings_about_master_title); add(items, c.getString(R.string.settings_share_title), c.getString(R.string.settings_share_summary), bc, AboutPreferenceFragment.class, R.string.settings_about_master_title); add(items, c.getString(R.string.settings_version_title), null, bc, AboutPreferenceFragment.class, R.string.settings_about_master_title); } // ------------------------------------------------------------------------- // Credits // ------------------------------------------------------------------------- private void addCreditsPreferences(Context c, List items) { String parent = c.getString(R.string.settings_about_master_title); String self = c.getString(R.string.settings_credits_master_title); String bc = parent + " \u203a " + self; add(items, c.getString(R.string.settings_credits_icon_foreground_title), c.getString(R.string.settings_credits_icon_foreground_summary), bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_icon_background_title), c.getString(R.string.settings_credits_icon_background_summary), bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_error_image_title), c.getString(R.string.settings_credits_error_image_summary), bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_crosspost_icon_title), c.getString(R.string.settings_credits_crosspost_icon_summary), bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_thumbtack_icon_title), c.getString(R.string.settings_credits_thumbtack_icon_summary), bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_best_rocket_icon_title), c.getString(R.string.settings_credits_best_rocket_icon_summary), bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_material_icons_title), null, bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_national_flags), null, bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_ufo_capturing_animation_title), null, bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_love_animation_title), null, bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); add(items, c.getString(R.string.settings_credits_lock_screen_animation_title), null, bc, CreditsPreferenceFragment.class, R.string.settings_credits_master_title); } // ------------------------------------------------------------------------- // Debug // ------------------------------------------------------------------------- private void addDebugPreferences(Context c, List items) { String bc = c.getString(R.string.settings_debug_title); add(items, c.getString(R.string.settings_screen_width_dp_title), c.getString(R.string.settings_screen_width_dp_summary), bc, DebugPreferenceFragment.class, R.string.settings_debug_title); add(items, c.getString(R.string.settings_smallest_screen_width_dp_title), c.getString(R.string.settings_smallest_screen_width_dp_summary), bc, DebugPreferenceFragment.class, R.string.settings_debug_title); add(items, c.getString(R.string.settings_is_tablet_title), c.getString(R.string.settings_is_tablet_summary_false), bc, DebugPreferenceFragment.class, R.string.settings_debug_title); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/SortTypePreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; public class SortTypePreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.sort_type_preferences, rootKey); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/SwipeActionPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import androidx.preference.ListPreference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeDisableSwipingBetweenTabsEvent; import ml.docilealligator.infinityforreddit.events.ChangeEnableSwipeActionSwitchEvent; import ml.docilealligator.infinityforreddit.events.ChangeSwipeActionEvent; import ml.docilealligator.infinityforreddit.events.ChangeSwipeActionThresholdEvent; import ml.docilealligator.infinityforreddit.events.ChangeVibrateWhenActionTriggeredEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class SwipeActionPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.swipe_action_preferences, rootKey); SwitchPreference enableSwipeActionSwitch = findPreference(SharedPreferencesUtils.ENABLE_SWIPE_ACTION); ListPreference swipeLeftActionListPreference = findPreference(SharedPreferencesUtils.SWIPE_LEFT_ACTION); ListPreference swipeRightActionListPreference = findPreference(SharedPreferencesUtils.SWIPE_RIGHT_ACTION); SwitchPreference vibrateWhenActionTriggeredSwitch = findPreference(SharedPreferencesUtils.VIBRATE_WHEN_ACTION_TRIGGERED); SwitchPreference disableSwipingBetweenTabsSwitch = findPreference(SharedPreferencesUtils.DISABLE_SWIPING_BETWEEN_TABS); ListPreference swipeActionThresholdListPreference = findPreference(SharedPreferencesUtils.SWIPE_ACTION_THRESHOLD); if (enableSwipeActionSwitch != null) { enableSwipeActionSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeEnableSwipeActionSwitchEvent((Boolean) newValue)); return true; }); } if (swipeLeftActionListPreference != null) { swipeLeftActionListPreference.setOnPreferenceChangeListener((preference, newValue) -> { if (swipeRightActionListPreference != null) { EventBus.getDefault().post(new ChangeSwipeActionEvent(Integer.parseInt((String) newValue), Integer.parseInt(swipeRightActionListPreference.getValue()))); } else { EventBus.getDefault().post(new ChangeSwipeActionEvent(Integer.parseInt((String) newValue), -1)); } return true; }); } if (swipeRightActionListPreference != null) { swipeRightActionListPreference.setOnPreferenceChangeListener((preference, newValue) -> { if (swipeLeftActionListPreference != null) { EventBus.getDefault().post(new ChangeSwipeActionEvent(Integer.parseInt(swipeLeftActionListPreference.getValue()), Integer.parseInt((String) newValue))); } else { EventBus.getDefault().post(new ChangeSwipeActionEvent(-1, Integer.parseInt((String) newValue))); } return true; }); } if (vibrateWhenActionTriggeredSwitch != null) { vibrateWhenActionTriggeredSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeVibrateWhenActionTriggeredEvent((Boolean) newValue)); return true; }); } if (disableSwipingBetweenTabsSwitch != null) { disableSwipingBetweenTabsSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeDisableSwipingBetweenTabsEvent((Boolean) newValue)); return true; }); } if (swipeActionThresholdListPreference != null) { swipeActionThresholdListPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeSwipeActionThresholdEvent(Float.parseFloat((String) newValue))); return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/ThemePreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.activities.CustomThemeListingActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeViewModel; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.MaterialYouUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; /** * A simple {@link Fragment} subclass. */ public class ThemePreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("default") SharedPreferences sharedPreferences; @Inject @Named("light_theme") SharedPreferences lightThemeSharedPreferences; @Inject @Named("dark_theme") SharedPreferences darkThemeSharedPreferences; @Inject @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject @Named("internal") SharedPreferences mInternalSharedPreferences; @Inject CustomThemeWrapper customThemeWrapper; @Inject Executor executor; public CustomThemeViewModel customThemeViewModel; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.theme_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); ListPreference themePreference = findPreference(SharedPreferencesUtils.THEME_KEY); SwitchPreference amoledDarkSwitch = findPreference(SharedPreferencesUtils.AMOLED_DARK_KEY); Preference customizeLightThemePreference = findPreference(SharedPreferencesUtils.CUSTOMIZE_LIGHT_THEME); Preference customizeDarkThemePreference = findPreference(SharedPreferencesUtils.CUSTOMIZE_DARK_THEME); Preference customizeAmoledThemePreference = findPreference(SharedPreferencesUtils.CUSTOMIZE_AMOLED_THEME); Preference selectAndCustomizeThemePreference = findPreference(SharedPreferencesUtils.MANAGE_THEMES); SwitchPreference enableMaterialYouSwitchPreference = findPreference(SharedPreferencesUtils.ENABLE_MATERIAL_YOU); Preference applyMaterialYouPreference = findPreference(SharedPreferencesUtils.APPLY_MATERIAL_YOU); boolean systemDefault = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; if (themePreference != null && amoledDarkSwitch != null) { if (systemDefault) { themePreference.setEntries(R.array.settings_theme_q); } else { themePreference.setEntries(R.array.settings_theme); } themePreference.setOnPreferenceChangeListener((preference, newValue) -> { int option = Integer.parseInt((String) newValue); switch (option) { case 0: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO); customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.LIGHT); break; case 1: AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES); if (amoledDarkSwitch.isChecked()) { customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.AMOLED); } else { customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.DARK); } break; case 2: if (systemDefault) { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM); } else { AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY); } if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO) { customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.LIGHT); } else { if (amoledDarkSwitch.isChecked()) { customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.AMOLED); } else { customThemeWrapper.setThemeType(CustomThemeSharedPreferencesUtils.DARK); } } } return true; }); } if (amoledDarkSwitch != null) { amoledDarkSwitch.setOnPreferenceChangeListener((preference, newValue) -> { if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_NO) { EventBus.getDefault().post(new RecreateActivityEvent()); ActivityCompat.recreate(mActivity); } return true; }); } if (customizeLightThemePreference != null) { customizeLightThemePreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_TYPE, CustomizeThemeActivity.EXTRA_LIGHT_THEME); startActivity(intent); return true; }); } if (customizeDarkThemePreference != null) { customizeDarkThemePreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_TYPE, CustomizeThemeActivity.EXTRA_DARK_THEME); startActivity(intent); return true; }); } if (customizeAmoledThemePreference != null) { customizeAmoledThemePreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, CustomizeThemeActivity.class); intent.putExtra(CustomizeThemeActivity.EXTRA_THEME_TYPE, CustomizeThemeActivity.EXTRA_AMOLED_THEME); startActivity(intent); return true; }); } if (selectAndCustomizeThemePreference != null) { selectAndCustomizeThemePreference.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(mActivity, CustomThemeListingActivity.class); startActivity(intent); return true; }); } if (enableMaterialYouSwitchPreference != null && applyMaterialYouPreference != null) { applyMaterialYouPreference.setVisible( sharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_MATERIAL_YOU, false)); enableMaterialYouSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { if ((Boolean) newValue) { MaterialYouUtils.changeThemeASync(mActivity, executor, new Handler(Looper.getMainLooper()), redditDataRoomDatabase, customThemeWrapper, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, mInternalSharedPreferences, null); applyMaterialYouPreference.setVisible(true); } else { applyMaterialYouPreference.setVisible(false); } return true; }); applyMaterialYouPreference.setOnPreferenceClickListener(preference -> { MaterialYouUtils.changeThemeASync(mActivity, executor, new Handler(Looper.getMainLooper()), redditDataRoomDatabase, customThemeWrapper, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, mInternalSharedPreferences, null); return true; }); } customThemeViewModel = new ViewModelProvider(this, new CustomThemeViewModel.Factory(redditDataRoomDatabase)) .get(CustomThemeViewModel.class); customThemeViewModel.getCurrentLightThemeLiveData().observe(this, customTheme -> { if (customizeLightThemePreference != null) { if (customTheme != null) { customizeLightThemePreference.setVisible(true); customizeLightThemePreference.setSummary(customTheme.name); } else { customizeLightThemePreference.setVisible(false); } } }); customThemeViewModel.getCurrentDarkThemeLiveData().observe(this, customTheme -> { if (customizeDarkThemePreference != null) { if (customTheme != null) { customizeDarkThemePreference.setVisible(true); customizeDarkThemePreference.setSummary(customTheme.name); } else { customizeDarkThemePreference.setVisible(false); } } }); customThemeViewModel.getCurrentAmoledThemeLiveData().observe(this, customTheme -> { if (customizeAmoledThemePreference != null) { if (customTheme != null) { customizeAmoledThemePreference.setVisible(true); customizeAmoledThemePreference.setSummary(customTheme.name); } else { customizeAmoledThemePreference.setVisible(false); } } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/TimeFormatPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.os.Bundle; import androidx.preference.ListPreference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.events.ChangeShowElapsedTimeEvent; import ml.docilealligator.infinityforreddit.events.ChangeTimeFormatEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class TimeFormatPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.time_format_preferences, rootKey); SwitchPreference showElapsedTimeSwitch = findPreference(SharedPreferencesUtils.SHOW_ELAPSED_TIME_KEY); ListPreference timeFormatList = findPreference(SharedPreferencesUtils.TIME_FORMAT_KEY); if (showElapsedTimeSwitch != null) { showElapsedTimeSwitch.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeShowElapsedTimeEvent((Boolean) newValue)); return true; }); } if (timeFormatList != null) { timeFormatList.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeTimeFormatEvent((String) newValue)); return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/Translation.java ================================================ package ml.docilealligator.infinityforreddit.settings; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; public class Translation { public String language; public String contributors; public int flagDrawableId; public Translation(String language, String contributors, int flagDrawableId) { this.language = language; this.contributors = contributors; this.flagDrawableId = flagDrawableId; } public static ArrayList getTranslationContributors() { ArrayList translationContributors = new ArrayList<>(); translationContributors.add(new Translation("български", "Ana patriciaaguayogomez, Iliqiliev373, Nane, zerw, Кристиян", R.drawable.flag_bulgaria)); translationContributors.add(new Translation("简体中文", "1, 3273676671, AaronFeng, Angela Thayer, Bitlabwzh, cdggqa, deluxghost, Dwhite, Gloria, gzwoyikythba, History_exe, hyl, Initial_Reading_197, Justin, Kai yuan, Ray, Steps, Tunicar, wert, WhiCCX5, 王昱程", R.drawable.flag_china)); translationContributors.add(new Translation("繁體中文", "1, Angela Thayer, Hbhuh, Ray, shlp, Wolfy. coding", R.drawable.flag_china)); translationContributors.add(new Translation("Hrvatski", "Andrej Ivanusec, Branimir, Josip Biondić", R.drawable.flag_croatia)); translationContributors.add(new Translation("čeština", "Fjuro, Jeniktelefon, sidvic88", R.drawable.flag_czech)); translationContributors.add(new Translation("Nederlands", "a, Anthony, Heimen Stoffels, KevinHF, Knnf, Khawkfist, Losms67, Mert, Viktor", R.drawable.flag_netherlands)); translationContributors.add(new Translation("Esperanto", "Ana patriciaaguayogomez, AnimatorzPolski, LiftedStarfish", -1)); translationContributors.add(new Translation("Française", "367, Charlito33, Clement. wawszczyk, Darkempire78, Darlene Sonalder, escatrag, Finn Olmsted, Furax-31, Hypnoticbat9555, Imperator, Johan, Loïc, Me1s, oursonbleu, Owen, pinembour, Serviceclient3dmart, Thomas", R.drawable.flag_france)); translationContributors.add(new Translation("Deutsche", "adth03, Chris, ducc1, Fornball, Guerda, Hoangseidel02, James, Jan, Joe, Jorge, Justus, Lm41, Manuel, Maximilian. neumann2, Netto Hikari, Nilsrie1, Nikodiamond3, Nilsrie1, NotABot34, PhCamp, Splat, Tischleindeckdich, translatewingman, translatorwiz, vcdf", R.drawable.flag_germany)); translationContributors.add(new Translation("Ελληνικά", "fresh, Marios, Viktor, Winston", R.drawable.flag_greece)); translationContributors.add(new Translation("עִברִית", "Ofek Bortz, Yuval", R.drawable.flag_israel)); translationContributors.add(new Translation("हिंदी", "a, Anonymous, Arya, charu, EnArvy, Harshit S Lawaniya, Mrigendra Bhandari, Nikhilcaddilac, Niranjan, prat, raghav, raj, Roshan, Sachin, saqib, Ved", R.drawable.flag_india)); translationContributors.add(new Translation("Magyar", "Balázs, Bro momento, ekaktusz, Gilgames32, mdvhimself, Szmanndani, trebron, Zoltan", R.drawable.flag_hungary)); translationContributors.add(new Translation("Italiana", "Daniele Basso, DanOlivaw, Enri. braga, Gianni00palmieri, Gillauino, Gio. gavio01, Giovanni, Giovanni Donisi, Lorenzo, Marco, Marco, Matisse, Simoneg. work, ztiaa", R.drawable.flag_italy)); translationContributors.add(new Translation("日本語", "Hira, Issa, Mrigendra Bhandari, nazo6, Ryan", R.drawable.flag_japan)); translationContributors.add(new Translation("한국어", "Jcxmt125, Me, noname", R.drawable.flag_south_korea)); translationContributors.add(new Translation("norsk", "", R.drawable.flag_norway)); translationContributors.add(new Translation("Polskie", "Adam, bbaster, Chupacabra, crash, Erax, Exp, Indexerrowaty, Kajetan, Maks, needless, quark, ultrakox, XioR112, xmsc", R.drawable.flag_poland)); translationContributors.add(new Translation("Português", "., Bruno Guerreiro, Francisco, Gabriel, Henry, Henry, Lucas, Miguel, Ricardo Fontão, Ricky", R.drawable.flag_portugal)); translationContributors.add(new Translation("Português (BR)", "., Andreaugustoqueiroz999, Asfuri, Davy, Júlia Angst Coelho, João Vieira, John Seila, Kauã Azevedo, Laura Vasconcellos Pereira Felippe, luccipriano, menosmenos, Murilogs7002, Raul S., Ricardo, Ricky, Sousa, Super_Iguanna, T. tony. br01, vsc, Ryan Marcelo", R.drawable.flag_brazil)); translationContributors.add(new Translation("Română", "Arminandrey, BitterJames, Cosmin, Edward, Loading Official, Malinatranslates, RabdăInimăȘiTace", R.drawable.flag_romania)); translationContributors.add(new Translation("русский язык", "Angela Thayer, Anon, Arseniy Tsekh, aveblazer, CaZzzer, Coolant, Craysy, Draer, elena, flexagoon, Georgiy, InvisibleRain, Overseen, solokot, Stambro, Tysontl2007, Vova", R.drawable.flag_russia)); translationContributors.add(new Translation("Soomaali", "Nadir Nour", R.drawable.flag_somalia)); translationContributors.add(new Translation("Español", "Agustin, Alejandro, Alfredo, Alonso, Angel, Angela Thayer, Armando, Armando Leyvaleyva, Armando Leyvaleyva, Canutolab, Freddy, Galdric, Gaynus, Iván Peña, Joel. chrono, Jorge, Kai yuan, Luis Antonio, Marcelo, Mario, Meh, Miguel, mvstermoe, Nana Snixx, Sergio, Sergio Varela, Sofia Flores, Suol, Theofficialdork, Tirso Carranza", R.drawable.flag_spain)); translationContributors.add(new Translation("svenska", "Marcus Nordberg", R.drawable.flag_sweden)); translationContributors.add(new Translation("தமிழ்", "Gobinathal8", -1)); translationContributors.add(new Translation("Türkçe", "adth03, Bahasnyldz, Berk Bakır \"Faoiltiarna\", cevirgen, Emir481, Kerim, Faoiltiarna, Mehmet Yavuz, Mert, Serif, Tuna Mert", R.drawable.flag_turkey)); translationContributors.add(new Translation("Українська", "@andmizyk, Andrij Mizyk", R.drawable.flag_ukraine)); translationContributors.add(new Translation("Tiếng Việt", "bruh, Đỗ Quang Vinh, fanta, harrybruh-kun, Kai, Khai, Laezzy, Lmao, Opstober, Ryan, viecdet69", R.drawable.flag_vietnam)); return translationContributors; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/TranslationFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import javax.inject.Inject; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.activities.BaseActivity; import ml.docilealligator.infinityforreddit.adapters.TranslationFragmentRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.databinding.FragmentTranslationBinding; import ml.docilealligator.infinityforreddit.utils.Utils; public class TranslationFragment extends Fragment { @Inject CustomThemeWrapper customThemeWrapper; private BaseActivity mActivity; public TranslationFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FragmentTranslationBinding binding = FragmentTranslationBinding.inflate(inflater, container, false); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); TranslationFragmentRecyclerViewAdapter adapter = new TranslationFragmentRecyclerViewAdapter(mActivity, customThemeWrapper); binding.getRoot().setAdapter(adapter); binding.getRoot().setBackgroundColor(customThemeWrapper.getBackgroundColor()); if (mActivity.isImmersiveInterfaceRespectForcedEdgeToEdge()) { ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { Insets allInsets = Utils.getInsets(insets, false, mActivity.isForcedImmersiveInterface()); binding.getRoot().setPadding(allInsets.left, 0, allInsets.right, allInsets.bottom); return WindowInsetsCompat.CONSUMED; } }); } return binding.getRoot(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = (BaseActivity) context; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/settings/VideoPreferenceFragment.java ================================================ package ml.docilealligator.infinityforreddit.settings; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Bundle; import androidx.preference.ListPreference; import androidx.preference.SwitchPreference; import org.greenrobot.eventbus.EventBus; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.customviews.preference.CustomFontPreferenceFragmentCompat; import ml.docilealligator.infinityforreddit.customviews.preference.SliderPreference; import ml.docilealligator.infinityforreddit.events.ChangeAutoplayNsfwVideosEvent; import ml.docilealligator.infinityforreddit.events.ChangeEasierToWatchInFullScreenEvent; import ml.docilealligator.infinityforreddit.events.ChangeMuteAutoplayingVideosEvent; import ml.docilealligator.infinityforreddit.events.ChangeMuteNSFWVideoEvent; import ml.docilealligator.infinityforreddit.events.ChangeRememberMutingOptionInPostFeedEvent; import ml.docilealligator.infinityforreddit.events.ChangeStartAutoplayVisibleAreaOffsetEvent; import ml.docilealligator.infinityforreddit.events.ChangeVideoAutoplayEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class VideoPreferenceFragment extends CustomFontPreferenceFragmentCompat { @Inject @Named("default") SharedPreferences sharedPreferences; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.video_preferences, rootKey); ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); ListPreference videoAutoplayListPreference = findPreference(SharedPreferencesUtils.VIDEO_AUTOPLAY); SwitchPreference muteAutoplayingVideosSwitchPreference = findPreference(SharedPreferencesUtils.MUTE_AUTOPLAYING_VIDEOS); SwitchPreference rememberMutingOptionInPostFeedSwitchPreference = findPreference(SharedPreferencesUtils.REMEMBER_MUTING_OPTION_IN_POST_FEED); SwitchPreference muteNSFWVideosSwitchPreference = findPreference(SharedPreferencesUtils.MUTE_NSFW_VIDEO); SwitchPreference autoplayNsfwVideosSwitchPreference = findPreference(SharedPreferencesUtils.AUTOPLAY_NSFW_VIDEOS); SwitchPreference easierToWatchInFullScreenSwitchPreference = findPreference(SharedPreferencesUtils.EASIER_TO_WATCH_IN_FULL_SCREEN); SliderPreference startAutoplayVisibleAreaOffsetPortrait = findPreference(SharedPreferencesUtils.START_AUTOPLAY_VISIBLE_AREA_OFFSET_PORTRAIT); SliderPreference startAutoplayVisibleAreaOffsetLandscape = findPreference(SharedPreferencesUtils.START_AUTOPLAY_VISIBLE_AREA_OFFSET_LANDSCAPE); if (videoAutoplayListPreference != null && autoplayNsfwVideosSwitchPreference != null) { videoAutoplayListPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeVideoAutoplayEvent((String) newValue)); return true; }); autoplayNsfwVideosSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeAutoplayNsfwVideosEvent((Boolean) newValue)); return true; }); } if (easierToWatchInFullScreenSwitchPreference != null) { easierToWatchInFullScreenSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeEasierToWatchInFullScreenEvent((Boolean) newValue)); return true; }); } if (muteNSFWVideosSwitchPreference != null) { muteNSFWVideosSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeMuteNSFWVideoEvent((Boolean) newValue)); return true; }); } if (muteAutoplayingVideosSwitchPreference != null) { muteAutoplayingVideosSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeMuteAutoplayingVideosEvent((Boolean) newValue)); return true; }); } if (rememberMutingOptionInPostFeedSwitchPreference != null) { rememberMutingOptionInPostFeedSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { EventBus.getDefault().post(new ChangeRememberMutingOptionInPostFeedEvent((Boolean) newValue)); return true; }); } int orientation = getResources().getConfiguration().orientation; if (startAutoplayVisibleAreaOffsetPortrait != null) { startAutoplayVisibleAreaOffsetPortrait.setSummaryTemplate(R.string.settings_start_autoplay_visible_area_offset_portrait_summary); startAutoplayVisibleAreaOffsetPortrait.setOnPreferenceChangeListener((preference, newValue) -> { if (orientation == Configuration.ORIENTATION_PORTRAIT) { EventBus.getDefault().post(new ChangeStartAutoplayVisibleAreaOffsetEvent((Integer) newValue)); } return true; }); } if (startAutoplayVisibleAreaOffsetLandscape != null) { startAutoplayVisibleAreaOffsetLandscape.setSummaryTemplate(R.string.settings_start_autoplay_visible_area_offset_landscape_summary); startAutoplayVisibleAreaOffsetLandscape.setOnPreferenceChangeListener((preference, newValue) -> { if (orientation == Configuration.ORIENTATION_LANDSCAPE) { EventBus.getDefault().post(new ChangeStartAutoplayVisibleAreaOffsetEvent((Integer) newValue)); } return true; }); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/FetchFlairs.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; import org.json.JSONException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchFlairs { public static void fetchFlairsInSubreddit(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, String subredditName, FetchFlairsInSubredditListener fetchFlairsInSubredditListener) { oauthRetrofit.create(RedditAPI.class).getFlairs(APIUtils.getOAuthHeader(accessToken), subredditName) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { List flairs = parseFlairs(response.body()); if (flairs != null) { handler.post(() -> fetchFlairsInSubredditListener.fetchSuccessful(flairs)); } else { handler.post(fetchFlairsInSubredditListener::fetchFailed); } }); } else if (response.code() == 403) { //No flairs fetchFlairsInSubredditListener.fetchSuccessful(null); } else { fetchFlairsInSubredditListener.fetchFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { fetchFlairsInSubredditListener.fetchFailed(); } }); } @WorkerThread @Nullable private static List parseFlairs(String response) { try { JSONArray jsonArray = new JSONArray(response); List flairs = new ArrayList<>(); for (int i = 0; i < jsonArray.length(); i++) { try { String id = jsonArray.getJSONObject(i).getString(JSONUtils.ID_KEY); String text = jsonArray.getJSONObject(i).getString(JSONUtils.TEXT_KEY); boolean editable = jsonArray.getJSONObject(i).getBoolean(JSONUtils.TEXT_EDITABLE_KEY); flairs.add(new Flair(id, text, editable)); } catch (JSONException e) { e.printStackTrace(); } } return flairs; } catch (JSONException e) { e.printStackTrace(); } return null; } public interface FetchFlairsInSubredditListener { void fetchSuccessful(List flairs); void fetchFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/FetchSubredditData.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; public class FetchSubredditData { public static void fetchSubredditData(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit retrofit, String subredditName, String accessToken, final FetchSubredditDataListener fetchSubredditDataListener) { executor.execute(() -> { RedditAPI api = retrofit.create(RedditAPI.class); Call subredditData; if (oauthRetrofit == null) { subredditData = api.getSubredditData(subredditName); } else { RedditAPI oauthApi = oauthRetrofit.create(RedditAPI.class); subredditData = oauthApi.getSubredditDataOauth(subredditName, APIUtils.getOAuthHeader(accessToken)); } try { Response response = subredditData.execute(); if (response.isSuccessful()) { ParseSubredditData.parseSubredditDataSync(handler, response.body(), fetchSubredditDataListener); } else { handler.post(() -> fetchSubredditDataListener.onFetchSubredditDataFail(response.code() == 403)); } } catch (IOException e) { handler.post(() -> fetchSubredditDataListener.onFetchSubredditDataFail(false)); } }); } static void fetchSubredditListingData(Executor executor, Handler handler, Retrofit retrofit, String query, String after, SortType.Type sortType, @Nullable String accessToken, @NonNull String accountName, boolean nsfw, final FetchSubredditListingDataListener fetchSubredditListingDataListener) { executor.execute(() -> { RedditAPI api = retrofit.create(RedditAPI.class); Map map = new HashMap<>(); Map headers = accountName.equals(Account.ANONYMOUS_ACCOUNT) ? map : APIUtils.getOAuthHeader(accessToken); Call subredditDataCall = api.searchSubreddits(query, after, sortType, nsfw ? 1 : 0, headers); try { Response response = subredditDataCall.execute(); if (response.isSuccessful()) { ParseSubredditData.parseSubredditListingDataSync(handler, response.body(), nsfw, fetchSubredditListingDataListener); } else { handler.post(fetchSubredditListingDataListener::onFetchSubredditListingDataFail); } } catch (IOException e) { e.printStackTrace(); handler.post(fetchSubredditListingDataListener::onFetchSubredditListingDataFail); } }); } public interface FetchSubredditDataListener { void onFetchSubredditDataSuccess(SubredditData subredditData, int nCurrentOnlineSubscribers); void onFetchSubredditDataFail(boolean isQuarantined); } public interface FetchSubredditListingDataListener { void onFetchSubredditListingDataSuccess(ArrayList subredditData, String after); void onFetchSubredditListingDataFail(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/Flair.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Parcel; import android.os.Parcelable; import com.google.gson.Gson; import com.google.gson.JsonParseException; public class Flair implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public Flair createFromParcel(Parcel in) { return new Flair(in); } @Override public Flair[] newArray(int size) { return new Flair[size]; } }; private String id; private String text; private boolean editable; Flair(String id, String text, boolean editable) { this.id = id; this.text = text; this.editable = editable; } protected Flair(Parcel in) { id = in.readString(); text = in.readString(); editable = in.readByte() != 0; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getText() { return text; } public void setText(String text) { this.text = text; } public boolean isEditable() { return editable; } public void setEditable(boolean editable) { this.editable = editable; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(id); parcel.writeString(text); parcel.writeByte((byte) (editable ? 1 : 0)); } public String getJSONModel() { return new Gson().toJson(this); } public static Flair fromJson(String json) throws JsonParseException { return new Gson().fromJson(json, Flair.class); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/ParseSubredditData.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Handler; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.Utils; public class ParseSubredditData { public static void parseSubredditDataSync(Handler handler, @Nullable String response, FetchSubredditData.FetchSubredditDataListener fetchSubredditDataListener) { if (response == null) { handler.post(() -> fetchSubredditDataListener.onFetchSubredditDataFail(false)); return; } try { JSONObject data = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY); SubredditData subredditData = parseSubredditDataSync(data, true); if (subredditData == null) { handler.post(() -> fetchSubredditDataListener.onFetchSubredditDataFail(false)); } else { handler.post(() -> fetchSubredditDataListener.onFetchSubredditDataSuccess(subredditData, 0)); } } catch (JSONException e) { e.printStackTrace(); handler.post(() -> fetchSubredditDataListener.onFetchSubredditDataFail(false)); } } public static void parseSubredditListingDataSync(Handler handler, @Nullable String response, boolean nsfw, FetchSubredditData.FetchSubredditListingDataListener fetchSubredditListingDataListener) { if (response == null) { handler.post(fetchSubredditListingDataListener::onFetchSubredditListingDataFail); return; } try { JSONObject jsonObject = new JSONObject(response); JSONArray children = jsonObject.getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); ArrayList subredditListingData = new ArrayList<>(); for (int i = 0; i < children.length(); i++) { JSONObject data = children.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); SubredditData subredditData = parseSubredditDataSync(data, nsfw); if (subredditData != null) { subredditListingData.add(subredditData); } } String after = jsonObject.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.AFTER_KEY); handler.post(() -> fetchSubredditListingDataListener.onFetchSubredditListingDataSuccess(subredditListingData, after)); } catch (JSONException e) { e.printStackTrace(); handler.post(fetchSubredditListingDataListener::onFetchSubredditListingDataFail); } } public static void parseSubredditListingData(Executor executor, Handler handler, @Nullable String response, boolean nsfw, ParseSubredditListingDataListener parseSubredditListingDataListener) { if (response == null) { parseSubredditListingDataListener.onParseSubredditListingDataFail(); return; } executor.execute(() -> { try { JSONObject jsonObject = new JSONObject(response); JSONArray children = jsonObject.getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); ArrayList subredditListingData = new ArrayList<>(); for (int i = 0; i < children.length(); i++) { JSONObject data = children.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); SubredditData subredditData = parseSubredditDataSync(data, nsfw); if (subredditData != null) { subredditListingData.add(subredditData); } } String after = jsonObject.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.AFTER_KEY); handler.post(() -> parseSubredditListingDataListener.onParseSubredditListingDataSuccess(subredditListingData, after)); } catch (JSONException e) { e.printStackTrace(); handler.post(parseSubredditListingDataListener::onParseSubredditListingDataFail); } }); } @Nullable private static SubredditData parseSubredditDataSync(JSONObject subredditDataJsonObject, boolean nsfw) throws JSONException { boolean isNSFW = !subredditDataJsonObject.isNull(JSONUtils.OVER18_KEY) && subredditDataJsonObject.getBoolean(JSONUtils.OVER18_KEY); if (!nsfw && isNSFW) { return null; } String id = subredditDataJsonObject.getString(JSONUtils.NAME_KEY); String subredditFullName = subredditDataJsonObject.getString(JSONUtils.DISPLAY_NAME_KEY); String description = subredditDataJsonObject.getString(JSONUtils.PUBLIC_DESCRIPTION_KEY).trim(); String sidebarDescription = Utils.modifyMarkdown(subredditDataJsonObject.getString(JSONUtils.DESCRIPTION_KEY).trim()); long createdUTC = subredditDataJsonObject.getLong(JSONUtils.CREATED_UTC_KEY) * 1000; String suggestedCommentSort = subredditDataJsonObject.getString(JSONUtils.SUGGESTED_COMMENT_SORT_KEY); String bannerImageUrl; if (subredditDataJsonObject.isNull(JSONUtils.BANNER_BACKGROUND_IMAGE_KEY)) { bannerImageUrl = ""; } else { bannerImageUrl = subredditDataJsonObject.getString(JSONUtils.BANNER_BACKGROUND_IMAGE_KEY); } if (bannerImageUrl.equals("") && !subredditDataJsonObject.isNull(JSONUtils.BANNER_IMG_KEY)) { bannerImageUrl = subredditDataJsonObject.getString(JSONUtils.BANNER_IMG_KEY); } String iconUrl; if (subredditDataJsonObject.isNull(JSONUtils.COMMUNITY_ICON_KEY)) { iconUrl = ""; } else { iconUrl = subredditDataJsonObject.getString(JSONUtils.COMMUNITY_ICON_KEY); } if (iconUrl.equals("") && !subredditDataJsonObject.isNull(JSONUtils.ICON_IMG_KEY)) { iconUrl = subredditDataJsonObject.getString(JSONUtils.ICON_IMG_KEY); } int nSubscribers = 0; if (!subredditDataJsonObject.isNull(JSONUtils.SUBSCRIBERS_KEY)) { nSubscribers = subredditDataJsonObject.getInt(JSONUtils.SUBSCRIBERS_KEY); } return new SubredditData(id, subredditFullName, iconUrl, bannerImageUrl, description, sidebarDescription, nSubscribers, createdUTC, suggestedCommentSort, isNSFW); } public interface ParseSubredditListingDataListener { void onParseSubredditListingDataSuccess(ArrayList subredditData, String after); void onParseSubredditListingDataFail(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/Rule.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; public class Rule { private final String shortName; private final String descriptionHtml; public Rule(String shortName, String descriptionHtml) { this.shortName = shortName; this.descriptionHtml = descriptionHtml; } public String getShortName() { return shortName; } public String getDescriptionHtml() { return descriptionHtml; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditDao.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; @Dao public interface SubredditDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(SubredditData SubredditData); @Query("DELETE FROM subreddits") void deleteAllSubreddits(); @Query("SELECT * from subreddits WHERE name = :namePrefixed COLLATE NOCASE LIMIT 1") LiveData getSubredditLiveDataByName(String namePrefixed); @Query("SELECT * from subreddits WHERE name = :namePrefixed COLLATE NOCASE LIMIT 1") SubredditData getSubredditData(String namePrefixed); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditData.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; @Entity(tableName = "subreddits") public class SubredditData implements Parcelable { @PrimaryKey @NonNull @ColumnInfo(name = "id") private final String id; @ColumnInfo(name = "name") private final String name; @ColumnInfo(name = "icon") private final String iconUrl; @ColumnInfo(name = "banner") private final String bannerUrl; @ColumnInfo(name = "description") private final String description; @ColumnInfo(name = "sidebar_description") private final String sidebarDescription; @ColumnInfo(name = "subscribers_count") private final int nSubscribers; @ColumnInfo(name = "created_utc") private final long createdUTC; @ColumnInfo(name = "suggested_comment_sort") private final String suggestedCommentSort; @ColumnInfo(name = "over18") private final boolean isNSFW; @Ignore private boolean isSelected; public SubredditData(@NonNull String id, String name, String iconUrl, String bannerUrl, String description, String sidebarDescription, int nSubscribers, long createdUTC, String suggestedCommentSort, boolean isNSFW) { this.id = id; this.name = name; this.iconUrl = iconUrl; this.bannerUrl = bannerUrl; this.description = description; this.sidebarDescription = sidebarDescription; this.nSubscribers = nSubscribers; this.createdUTC = createdUTC; this.suggestedCommentSort = suggestedCommentSort; this.isNSFW = isNSFW; this.isSelected = false; } protected SubredditData(Parcel in) { id = in.readString(); name = in.readString(); iconUrl = in.readString(); bannerUrl = in.readString(); description = in.readString(); sidebarDescription = in.readString(); nSubscribers = in.readInt(); createdUTC = in.readLong(); suggestedCommentSort = in.readString(); isNSFW = in.readByte() != 0; isSelected = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public SubredditData createFromParcel(Parcel in) { return new SubredditData(in); } @Override public SubredditData[] newArray(int size) { return new SubredditData[size]; } }; @NonNull public String getId() { return id; } public String getName() { return name; } public String getIconUrl() { return iconUrl; } public String getBannerUrl() { return bannerUrl; } public String getDescription() { return description; } public String getSidebarDescription() { return sidebarDescription; } public int getNSubscribers() { return nSubscribers; } public long getCreatedUTC() { return createdUTC; } public String getSuggestedCommentSort() { return suggestedCommentSort; } public boolean isNSFW() { return isNSFW; } public boolean isSelected() { return isSelected; } public void setSelected(boolean selected) { isSelected = selected; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(id); dest.writeString(name); dest.writeString(iconUrl); dest.writeString(bannerUrl); dest.writeString(description); dest.writeString(sidebarDescription); dest.writeInt(nSubscribers); dest.writeLong(createdUTC); dest.writeString(suggestedCommentSort); dest.writeByte((byte) (isNSFW ? 1 : 0)); dest.writeByte((byte) (isSelected ? 1 : 0)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditListingDataSource.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.MutableLiveData; import androidx.paging.PageKeyedDataSource; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Retrofit; public class SubredditListingDataSource extends PageKeyedDataSource { private final Executor executor; private final Retrofit retrofit; private final String query; private final SortType sortType; @Nullable private final String accessToken; @NonNull private final String accountName; private final boolean nsfw; private Handler handler; private final MutableLiveData paginationNetworkStateLiveData; private final MutableLiveData initialLoadStateLiveData; private final MutableLiveData hasSubredditLiveData; private LoadParams params; private LoadCallback callback; SubredditListingDataSource(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, @Nullable String accessToken, @NonNull String accountName, boolean nsfw) { this.executor = executor; this.retrofit = retrofit; this.query = query; this.sortType = sortType; this.accessToken = accessToken; this.accountName = accountName; this.nsfw = nsfw; this.handler = handler; paginationNetworkStateLiveData = new MutableLiveData<>(); initialLoadStateLiveData = new MutableLiveData<>(); hasSubredditLiveData = new MutableLiveData<>(); } MutableLiveData getPaginationNetworkStateLiveData() { return paginationNetworkStateLiveData; } MutableLiveData getInitialLoadStateLiveData() { return initialLoadStateLiveData; } MutableLiveData hasSubredditLiveData() { return hasSubredditLiveData; } @Override public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) { initialLoadStateLiveData.postValue(NetworkState.LOADING); FetchSubredditData.fetchSubredditListingData(executor, handler, retrofit, query, null, sortType.getType(), accessToken, accountName, nsfw, new FetchSubredditData.FetchSubredditListingDataListener() { @Override public void onFetchSubredditListingDataSuccess(ArrayList subredditData, String after) { if (subredditData.size() == 0) { hasSubredditLiveData.postValue(false); } else { hasSubredditLiveData.postValue(true); } callback.onResult(subredditData, null, after); initialLoadStateLiveData.postValue(NetworkState.LOADED); } @Override public void onFetchSubredditListingDataFail() { initialLoadStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error retrieving subreddit list")); } }); } @Override public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) { } @Override public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) { this.params = params; this.callback = callback; if (params.key == null || params.key.equals("") || params.key.equals("null")) { return; } FetchSubredditData.fetchSubredditListingData(executor, handler, retrofit, query, params.key, sortType.getType(), accessToken, accountName, nsfw, new FetchSubredditData.FetchSubredditListingDataListener() { @Override public void onFetchSubredditListingDataSuccess(ArrayList subredditData, String after) { callback.onResult(subredditData, after); paginationNetworkStateLiveData.postValue(NetworkState.LOADED); } @Override public void onFetchSubredditListingDataFail() { paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error retrieving subreddit list")); } }); } void retryLoadingMore() { loadAfter(params, callback); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditListingDataSourceFactory.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.MutableLiveData; import androidx.paging.DataSource; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Retrofit; public class SubredditListingDataSourceFactory extends DataSource.Factory { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final String query; private SortType sortType; @Nullable private final String accessToken; @NonNull private final String accountName; private final boolean nsfw; private SubredditListingDataSource subredditListingDataSource; private final MutableLiveData subredditListingDataSourceMutableLiveData; SubredditListingDataSourceFactory(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, @Nullable String accessToken, @NonNull String accountName, boolean nsfw) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.query = query; this.sortType = sortType; this.accessToken = accessToken; this.accountName = accountName; this.nsfw = nsfw; subredditListingDataSourceMutableLiveData = new MutableLiveData<>(); } @NonNull @Override public DataSource create() { subredditListingDataSource = new SubredditListingDataSource(executor, handler, retrofit, query, sortType, accessToken, accountName, nsfw); subredditListingDataSourceMutableLiveData.postValue(subredditListingDataSource); return subredditListingDataSource; } public MutableLiveData getSubredditListingDataSourceMutableLiveData() { return subredditListingDataSourceMutableLiveData; } SubredditListingDataSource getSubredditListingDataSource() { return subredditListingDataSource; } void changeSortType(SortType sortType) { this.sortType = sortType; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditListingViewModel.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Retrofit; public class SubredditListingViewModel extends ViewModel { private final SubredditListingDataSourceFactory subredditListingDataSourceFactory; private final LiveData paginationNetworkState; private final LiveData initialLoadingState; private final LiveData hasSubredditLiveData; private final LiveData> subreddits; private final MutableLiveData sortTypeLiveData; public SubredditListingViewModel(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, @Nullable String accessToken, @NonNull String accountName, boolean nsfw) { subredditListingDataSourceFactory = new SubredditListingDataSourceFactory(executor, handler, retrofit, query, sortType, accessToken, accountName, nsfw); initialLoadingState = Transformations.switchMap(subredditListingDataSourceFactory.getSubredditListingDataSourceMutableLiveData(), SubredditListingDataSource::getInitialLoadStateLiveData); paginationNetworkState = Transformations.switchMap(subredditListingDataSourceFactory.getSubredditListingDataSourceMutableLiveData(), SubredditListingDataSource::getPaginationNetworkStateLiveData); hasSubredditLiveData = Transformations.switchMap(subredditListingDataSourceFactory.getSubredditListingDataSourceMutableLiveData(), SubredditListingDataSource::hasSubredditLiveData); sortTypeLiveData = new MutableLiveData<>(sortType); PagedList.Config pagedListConfig = (new PagedList.Config.Builder()) .setEnablePlaceholders(false) .setPageSize(25) .build(); subreddits = Transformations.switchMap(sortTypeLiveData, sort -> { subredditListingDataSourceFactory.changeSortType(sortTypeLiveData.getValue()); return new LivePagedListBuilder(subredditListingDataSourceFactory, pagedListConfig).build(); }); } public LiveData> getSubreddits() { return subreddits; } public LiveData getPaginationNetworkState() { return paginationNetworkState; } public LiveData getInitialLoadingState() { return initialLoadingState; } public LiveData hasSubredditLiveData() { return hasSubredditLiveData; } public void refresh() { subredditListingDataSourceFactory.getSubredditListingDataSource().invalidate(); } public void retryLoadingMore() { subredditListingDataSourceFactory.getSubredditListingDataSource().retryLoadingMore(); } public void changeSortType(SortType sortType) { sortTypeLiveData.postValue(sortType); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final String query; private final SortType sortType; @Nullable private final String accessToken; @NonNull private final String accountName; private final boolean nsfw; public Factory(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, @Nullable String accessToken, @NonNull String accountName, boolean nsfw) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.query = query; this.sortType = sortType; this.accessToken = accessToken; this.accountName = accountName; this.nsfw = nsfw; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new SubredditListingViewModel(executor, handler, retrofit, query, sortType, accessToken,accountName, nsfw); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditRepository.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import androidx.lifecycle.LiveData; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SubredditRepository { private final SubredditDao mSubredditDao; private final LiveData mSubredditLiveData; SubredditRepository(RedditDataRoomDatabase redditDataRoomDatabase, String subredditName) { mSubredditDao = redditDataRoomDatabase.subredditDao(); mSubredditLiveData = mSubredditDao.getSubredditLiveDataByName(subredditName); } LiveData getSubredditLiveData() { return mSubredditLiveData; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditSettingData.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import androidx.annotation.Nullable; import com.google.gson.annotations.SerializedName; import java.util.Objects; public class SubredditSettingData { public static final String WIKIMODE_ANYONE = "anyone"; public static final String WIKIMODE_DISABLED = "disabled"; public static final String WIKIMODE_MODONLY = "modonly"; // Content visibility || Posts to this profile can appear in r/all and your profile can be discovered in /users @SerializedName("default_set") private boolean defaultSet; @SerializedName("toxicity_threshold_chat_level") private int toxicityThresholdChatLevel; @SerializedName("crowd_control_chat_level") private int crowdControlChatLevel; @SerializedName("restrict_posting") private boolean restrictPosting; @SerializedName("public_description") private String publicDescription; @SerializedName("subreddit_id") private String subredditId; @SerializedName("allow_images") private boolean allowImages; @SerializedName("free_form_reports") private boolean freeFormReports; @SerializedName("domain") @Nullable private String domain; @SerializedName("show_media") private boolean showMedia; @SerializedName("wiki_edit_age") private int wikiEditAge; @SerializedName("submit_text") private String submitText; @SerializedName("allow_polls") private boolean allowPolls; @SerializedName("title") private String title; @SerializedName("collapse_deleted_comments") private boolean collapseDeletedComments; @SerializedName("wikimode") private String wikiMode; @SerializedName("should_archive_posts") private boolean shouldArchivePosts; @SerializedName("allow_videos") private boolean allowVideos; @SerializedName("allow_galleries") private boolean allowGalleries; @SerializedName("crowd_control_level") private int crowdControlLevel; @SerializedName("crowd_control_mode") private boolean crowdControlMode; @SerializedName("welcome_message_enabled") private boolean welcomeMessageEnabled; @SerializedName("welcome_message_text") @Nullable private String welcomeMessageText; @SerializedName("over_18") private boolean over18; @SerializedName("suggested_comment_sort") private String suggestedCommentSort; @SerializedName("disable_contributor_requests") private boolean disableContributorRequests; @SerializedName("original_content_tag_enabled") private boolean originalContentTagEnabled; @SerializedName("description") private String description; @SerializedName("submit_link_label") private String submitLinkLabel; @SerializedName("spoilers_enabled") private boolean spoilersEnabled; @SerializedName("allow_post_crossposts") private boolean allowPostCrossPosts; @SerializedName("spam_comments") private String spamComments; @SerializedName("public_traffic") private boolean publicTraffic; @SerializedName("restrict_commenting") private boolean restrictCommenting; @SerializedName("new_pinned_post_pns_enabled") private boolean newPinnedPostPnsEnabled; @SerializedName("submit_text_label") private String submitTextLabel; @SerializedName("all_original_content") private boolean allOriginalContent; @SerializedName("spam_selfposts") private String spamSelfPosts; @SerializedName("key_color") private String keyColor; @SerializedName("language") private String language; @SerializedName("wiki_edit_karma") private int wikiEditKarma; @SerializedName("hide_ads") private boolean hideAds; @SerializedName("prediction_leaderboard_entry_type") private int predictionLeaderboardEntryType; @SerializedName("header_hover_text") private String headerHoverText; @SerializedName("allow_chat_post_creation") private boolean allowChatPostCreation; @SerializedName("allow_prediction_contributors") private boolean allowPredictionContributors; @SerializedName("allow_discovery") private boolean allowDiscovery; @SerializedName("accept_followers") private boolean acceptFollowers; @SerializedName("exclude_banned_modqueue") private boolean excludeBannedModQueue; @SerializedName("allow_predictions_tournament") private boolean allowPredictionsTournament; @SerializedName("show_media_preview") private boolean showMediaPreview; @SerializedName("comment_score_hide_mins") private int commentScoreHideMins; @SerializedName("subreddit_type") private String subredditType; @SerializedName("spam_links") private String spamLinks; @SerializedName("allow_predictions") private boolean allowPredictions; @SerializedName("user_flair_pns_enabled") private boolean userFlairPnsEnabled; @SerializedName("content_options") private String contentOptions; public boolean isDefaultSet() { return defaultSet; } public void setDefaultSet(boolean defaultSet) { this.defaultSet = defaultSet; } public int getToxicityThresholdChatLevel() { return toxicityThresholdChatLevel; } public void setToxicityThresholdChatLevel(int toxicityThresholdChatLevel) { this.toxicityThresholdChatLevel = toxicityThresholdChatLevel; } public int getCrowdControlChatLevel() { return crowdControlChatLevel; } public void setCrowdControlChatLevel(int crowdControlChatLevel) { this.crowdControlChatLevel = crowdControlChatLevel; } public boolean isRestrictPosting() { return restrictPosting; } public void setRestrictPosting(boolean restrictPosting) { this.restrictPosting = restrictPosting; } public String getPublicDescription() { return publicDescription; } public void setPublicDescription(String publicDescription) { this.publicDescription = publicDescription; } public String getSubredditId() { return subredditId; } public void setSubredditId(String subredditId) { this.subredditId = subredditId; } public boolean isAllowImages() { return allowImages; } public void setAllowImages(boolean allowImages) { this.allowImages = allowImages; } public boolean isFreeFormReports() { return freeFormReports; } public void setFreeFormReports(boolean freeFormReports) { this.freeFormReports = freeFormReports; } @Nullable public String getDomain() { return domain; } public void setDomain(@Nullable String domain) { this.domain = domain; } public boolean isShowMedia() { return showMedia; } public void setShowMedia(boolean showMedia) { this.showMedia = showMedia; } public int getWikiEditAge() { return wikiEditAge; } public void setWikiEditAge(int wikiEditAge) { this.wikiEditAge = wikiEditAge; } public String getSubmitText() { return submitText; } public void setSubmitText(String submitText) { this.submitText = submitText; } public boolean isAllowPolls() { return allowPolls; } public void setAllowPolls(boolean allowPolls) { this.allowPolls = allowPolls; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public boolean isCollapseDeletedComments() { return collapseDeletedComments; } public void setCollapseDeletedComments(boolean collapseDeletedComments) { this.collapseDeletedComments = collapseDeletedComments; } public String getWikiMode() { if (wikiMode == null) { // Default to disabled, since occasionally the API returns null. return WIKIMODE_DISABLED; } return wikiMode; } public void setWikiMode(String wikiMode) { this.wikiMode = wikiMode; } public boolean isShouldArchivePosts() { return shouldArchivePosts; } public void setShouldArchivePosts(boolean shouldArchivePosts) { this.shouldArchivePosts = shouldArchivePosts; } public boolean isAllowVideos() { return allowVideos; } public void setAllowVideos(boolean allowVideos) { this.allowVideos = allowVideos; } public boolean isAllowGalleries() { return allowGalleries; } public void setAllowGalleries(boolean allowGalleries) { this.allowGalleries = allowGalleries; } public int getCrowdControlLevel() { return crowdControlLevel; } public void setCrowdControlLevel(int crowdControlLevel) { this.crowdControlLevel = crowdControlLevel; } public boolean isCrowdControlMode() { return crowdControlMode; } public void setCrowdControlMode(boolean crowdControlMode) { this.crowdControlMode = crowdControlMode; } public boolean isWelcomeMessageEnabled() { return welcomeMessageEnabled; } public void setWelcomeMessageEnabled(boolean welcomeMessageEnabled) { this.welcomeMessageEnabled = welcomeMessageEnabled; } @Nullable public String getWelcomeMessageText() { return welcomeMessageText; } public void setWelcomeMessageText(@Nullable String welcomeMessageText) { this.welcomeMessageText = welcomeMessageText; } public boolean isOver18() { return over18; } public void setOver18(boolean over18) { this.over18 = over18; } public String getSuggestedCommentSort() { return suggestedCommentSort; } public void setSuggestedCommentSort(String suggestedCommentSort) { this.suggestedCommentSort = suggestedCommentSort; } public boolean isDisableContributorRequests() { return disableContributorRequests; } public void setDisableContributorRequests(boolean disableContributorRequests) { this.disableContributorRequests = disableContributorRequests; } public boolean isOriginalContentTagEnabled() { return originalContentTagEnabled; } public void setOriginalContentTagEnabled(boolean originalContentTagEnabled) { this.originalContentTagEnabled = originalContentTagEnabled; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getSubmitLinkLabel() { return submitLinkLabel; } public void setSubmitLinkLabel(String submitLinkLabel) { this.submitLinkLabel = submitLinkLabel; } public boolean isSpoilersEnabled() { return spoilersEnabled; } public void setSpoilersEnabled(boolean spoilersEnabled) { this.spoilersEnabled = spoilersEnabled; } public boolean isAllowPostCrossPosts() { return allowPostCrossPosts; } public void setAllowPostCrossPosts(boolean allowPostCrossPosts) { this.allowPostCrossPosts = allowPostCrossPosts; } public String getSpamComments() { return spamComments; } public void setSpamComments(String spamComments) { this.spamComments = spamComments; } public boolean isPublicTraffic() { return publicTraffic; } public void setPublicTraffic(boolean publicTraffic) { this.publicTraffic = publicTraffic; } public boolean isRestrictCommenting() { return restrictCommenting; } public void setRestrictCommenting(boolean restrictCommenting) { this.restrictCommenting = restrictCommenting; } public boolean isNewPinnedPostPnsEnabled() { return newPinnedPostPnsEnabled; } public void setNewPinnedPostPnsEnabled(boolean newPinnedPostPnsEnabled) { this.newPinnedPostPnsEnabled = newPinnedPostPnsEnabled; } public String getSubmitTextLabel() { return submitTextLabel; } public void setSubmitTextLabel(String submitTextLabel) { this.submitTextLabel = submitTextLabel; } public boolean isAllOriginalContent() { return allOriginalContent; } public void setAllOriginalContent(boolean allOriginalContent) { this.allOriginalContent = allOriginalContent; } public String getSpamSelfPosts() { return spamSelfPosts; } public void setSpamSelfPosts(String spamSelfPosts) { this.spamSelfPosts = spamSelfPosts; } public String getKeyColor() { return keyColor; } public void setKeyColor(String keyColor) { this.keyColor = keyColor; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public int getWikiEditKarma() { return wikiEditKarma; } public void setWikiEditKarma(int wikiEditKarma) { this.wikiEditKarma = wikiEditKarma; } public boolean isHideAds() { return hideAds; } public void setHideAds(boolean hideAds) { this.hideAds = hideAds; } public int getPredictionLeaderboardEntryType() { return predictionLeaderboardEntryType; } public void setPredictionLeaderboardEntryType(int predictionLeaderboardEntryType) { this.predictionLeaderboardEntryType = predictionLeaderboardEntryType; } public String getHeaderHoverText() { return headerHoverText; } public void setHeaderHoverText(String headerHoverText) { this.headerHoverText = headerHoverText; } public boolean isAllowChatPostCreation() { return allowChatPostCreation; } public void setAllowChatPostCreation(boolean allowChatPostCreation) { this.allowChatPostCreation = allowChatPostCreation; } public boolean isAllowPredictionContributors() { return allowPredictionContributors; } public void setAllowPredictionContributors(boolean allowPredictionContributors) { this.allowPredictionContributors = allowPredictionContributors; } public boolean isAllowDiscovery() { return allowDiscovery; } public void setAllowDiscovery(boolean allowDiscovery) { this.allowDiscovery = allowDiscovery; } public boolean isAcceptFollowers() { return acceptFollowers; } public void setAcceptFollowers(boolean acceptFollowers) { this.acceptFollowers = acceptFollowers; } public boolean isExcludeBannedModQueue() { return excludeBannedModQueue; } public void setExcludeBannedModQueue(boolean excludeBannedModQueue) { this.excludeBannedModQueue = excludeBannedModQueue; } public boolean isAllowPredictionsTournament() { return allowPredictionsTournament; } public void setAllowPredictionsTournament(boolean allowPredictionsTournament) { this.allowPredictionsTournament = allowPredictionsTournament; } public boolean isShowMediaPreview() { return showMediaPreview; } public void setShowMediaPreview(boolean showMediaPreview) { this.showMediaPreview = showMediaPreview; } public int getCommentScoreHideMins() { return commentScoreHideMins; } public void setCommentScoreHideMins(int commentScoreHideMins) { this.commentScoreHideMins = commentScoreHideMins; } public String getSubredditType() { return subredditType; } public void setSubredditType(String subredditType) { this.subredditType = subredditType; } public String getSpamLinks() { return spamLinks; } public void setSpamLinks(String spamLinks) { this.spamLinks = spamLinks; } public boolean isAllowPredictions() { return allowPredictions; } public void setAllowPredictions(boolean allowPredictions) { this.allowPredictions = allowPredictions; } public boolean isUserFlairPnsEnabled() { return userFlairPnsEnabled; } public void setUserFlairPnsEnabled(boolean userFlairPnsEnabled) { this.userFlairPnsEnabled = userFlairPnsEnabled; } public String getContentOptions() { return contentOptions; } public void setContentOptions(String contentOptions) { this.contentOptions = contentOptions; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SubredditSettingData that = (SubredditSettingData) o; return defaultSet == that.defaultSet && toxicityThresholdChatLevel == that.toxicityThresholdChatLevel && crowdControlChatLevel == that.crowdControlChatLevel && restrictPosting == that.restrictPosting && allowImages == that.allowImages && freeFormReports == that.freeFormReports && showMedia == that.showMedia && wikiEditAge == that.wikiEditAge && allowPolls == that.allowPolls && collapseDeletedComments == that.collapseDeletedComments && shouldArchivePosts == that.shouldArchivePosts && allowVideos == that.allowVideos && allowGalleries == that.allowGalleries && crowdControlLevel == that.crowdControlLevel && crowdControlMode == that.crowdControlMode && welcomeMessageEnabled == that.welcomeMessageEnabled && over18 == that.over18 && disableContributorRequests == that.disableContributorRequests && originalContentTagEnabled == that.originalContentTagEnabled && spoilersEnabled == that.spoilersEnabled && allowPostCrossPosts == that.allowPostCrossPosts && publicTraffic == that.publicTraffic && restrictCommenting == that.restrictCommenting && newPinnedPostPnsEnabled == that.newPinnedPostPnsEnabled && allOriginalContent == that.allOriginalContent && wikiEditKarma == that.wikiEditKarma && hideAds == that.hideAds && predictionLeaderboardEntryType == that.predictionLeaderboardEntryType && allowChatPostCreation == that.allowChatPostCreation && allowPredictionContributors == that.allowPredictionContributors && allowDiscovery == that.allowDiscovery && acceptFollowers == that.acceptFollowers && excludeBannedModQueue == that.excludeBannedModQueue && allowPredictionsTournament == that.allowPredictionsTournament && showMediaPreview == that.showMediaPreview && commentScoreHideMins == that.commentScoreHideMins && allowPredictions == that.allowPredictions && userFlairPnsEnabled == that.userFlairPnsEnabled && Objects.equals(publicDescription, that.publicDescription) && Objects.equals(subredditId, that.subredditId) && Objects.equals(domain, that.domain) && Objects.equals(submitText, that.submitText) && Objects.equals(title, that.title) && Objects.equals(wikiMode, that.wikiMode) && Objects.equals(welcomeMessageText, that.welcomeMessageText) && Objects.equals(suggestedCommentSort, that.suggestedCommentSort) && Objects.equals(description, that.description) && Objects.equals(submitLinkLabel, that.submitLinkLabel) && Objects.equals(spamComments, that.spamComments) && Objects.equals(submitTextLabel, that.submitTextLabel) && Objects.equals(spamSelfPosts, that.spamSelfPosts) && Objects.equals(keyColor, that.keyColor) && Objects.equals(language, that.language) && Objects.equals(headerHoverText, that.headerHoverText) && Objects.equals(subredditType, that.subredditType) && Objects.equals(spamLinks, that.spamLinks) && Objects.equals(contentOptions, that.contentOptions); } @Override public int hashCode() { return Objects.hash(defaultSet, toxicityThresholdChatLevel, crowdControlChatLevel, restrictPosting, publicDescription, subredditId, allowImages, freeFormReports, domain, showMedia, wikiEditAge, submitText, allowPolls, title, collapseDeletedComments, wikiMode, shouldArchivePosts, allowVideos, allowGalleries, crowdControlLevel, crowdControlMode, welcomeMessageEnabled, welcomeMessageText, over18, suggestedCommentSort, disableContributorRequests, originalContentTagEnabled, description, submitLinkLabel, spoilersEnabled, allowPostCrossPosts, spamComments, publicTraffic, restrictCommenting, newPinnedPostPnsEnabled, submitTextLabel, allOriginalContent, spamSelfPosts, keyColor, language, wikiEditKarma, hideAds, predictionLeaderboardEntryType, headerHoverText, allowChatPostCreation, allowPredictionContributors, allowDiscovery, acceptFollowers, excludeBannedModQueue, allowPredictionsTournament, showMediaPreview, commentScoreHideMins, subredditType, spamLinks, allowPredictions, userFlairPnsEnabled, contentOptions); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditSubscription.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Handler; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Retrofit; public class SubredditSubscription { public static void subscribeToSubreddit(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit retrofit, String accessToken, String subredditName, String accountName, RedditDataRoomDatabase redditDataRoomDatabase, SubredditSubscriptionListener subredditSubscriptionListener) { subredditSubscription(executor, handler, oauthRetrofit, retrofit, accessToken, subredditName, accountName, "sub", redditDataRoomDatabase, subredditSubscriptionListener); } public static void anonymousSubscribeToSubreddit(Executor executor, Handler handler, Retrofit retrofit, RedditDataRoomDatabase redditDataRoomDatabase, String subredditName, SubredditSubscriptionListener subredditSubscriptionListener) { FetchSubredditData.fetchSubredditData(executor, handler, null, retrofit, subredditName, "", new FetchSubredditData.FetchSubredditDataListener() { @Override public void onFetchSubredditDataSuccess(SubredditData subredditData, int nCurrentOnlineSubscribers) { insertSubscription(executor, handler, redditDataRoomDatabase, subredditData, Account.ANONYMOUS_ACCOUNT, subredditSubscriptionListener); } @Override public void onFetchSubredditDataFail(boolean isQuarantined) { subredditSubscriptionListener.onSubredditSubscriptionFail(); } }); } public static void unsubscribeToSubreddit(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, String subredditName, String accountName, RedditDataRoomDatabase redditDataRoomDatabase, SubredditSubscriptionListener subredditSubscriptionListener) { subredditSubscription(executor, handler, oauthRetrofit, null, accessToken, subredditName, accountName, "unsub", redditDataRoomDatabase, subredditSubscriptionListener); } public static void anonymousUnsubscribeToSubreddit(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String subredditName, SubredditSubscriptionListener subredditSubscriptionListener) { removeSubscription(executor, handler, redditDataRoomDatabase, subredditName, Account.ANONYMOUS_ACCOUNT, subredditSubscriptionListener); } private static void subredditSubscription(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit retrofit, String accessToken, String subredditName, String accountName, String action, RedditDataRoomDatabase redditDataRoomDatabase, SubredditSubscriptionListener subredditSubscriptionListener) { RedditAPI api = oauthRetrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.ACTION_KEY, action); params.put(APIUtils.SR_NAME_KEY, subredditName); Call subredditSubscriptionCall = api.subredditSubscription(APIUtils.getOAuthHeader(accessToken), params); subredditSubscriptionCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { if (response.isSuccessful()) { if (action.equals("sub")) { FetchSubredditData.fetchSubredditData(executor, handler, oauthRetrofit, retrofit, subredditName, accessToken, new FetchSubredditData.FetchSubredditDataListener() { @Override public void onFetchSubredditDataSuccess(SubredditData subredditData, int nCurrentOnlineSubscribers) { insertSubscription(executor, handler, redditDataRoomDatabase, subredditData, accountName, subredditSubscriptionListener); } @Override public void onFetchSubredditDataFail(boolean isQuarantined) { } }); } else { removeSubscription(executor, handler, redditDataRoomDatabase, subredditName, accountName, subredditSubscriptionListener); } } else { subredditSubscriptionListener.onSubredditSubscriptionFail(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { subredditSubscriptionListener.onSubredditSubscriptionFail(); } }); } public interface SubredditSubscriptionListener { void onSubredditSubscriptionSuccess(); void onSubredditSubscriptionFail(); } private static void insertSubscription(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, SubredditData subredditData, String accountName, SubredditSubscriptionListener subredditSubscriptionListener) { executor.execute(() -> { SubscribedSubredditData subscribedSubredditData = new SubscribedSubredditData(subredditData.getId(), subredditData.getName(), subredditData.getIconUrl(), accountName, false); if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (!redditDataRoomDatabase.accountDao().isAnonymousAccountInserted()) { redditDataRoomDatabase.accountDao().insert(Account.getAnonymousAccount()); } } redditDataRoomDatabase.subscribedSubredditDao().insert(subscribedSubredditData); handler.post(subredditSubscriptionListener::onSubredditSubscriptionSuccess); }); } private static void removeSubscription(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, String subredditName, String accountName, SubredditSubscriptionListener subredditSubscriptionListener) { executor.execute(() -> { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (!redditDataRoomDatabase.accountDao().isAnonymousAccountInserted()) { redditDataRoomDatabase.accountDao().insert(Account.getAnonymousAccount()); } } redditDataRoomDatabase.subscribedSubredditDao().deleteSubscribedSubreddit(subredditName, accountName); handler.post(subredditSubscriptionListener::onSubredditSubscriptionSuccess); }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditViewModel.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SubredditViewModel extends ViewModel { private final SubredditRepository mSubredditRepository; private final LiveData mSubredditLiveData; public SubredditViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String id) { mSubredditRepository = new SubredditRepository(redditDataRoomDatabase, id); mSubredditLiveData = mSubredditRepository.getSubredditLiveData(); } public LiveData getSubredditLiveData() { return mSubredditLiveData; } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mSubredditName; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String subredditname) { mRedditDataRoomDatabase = redditDataRoomDatabase; mSubredditName = subredditname; } @NonNull @Override public T create(@NonNull Class modelClass) { //noinspection return (T) new SubredditViewModel(mRedditDataRoomDatabase, mSubredditName); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/SubredditWithSelection.java ================================================ package ml.docilealligator.infinityforreddit.subreddit; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; public class SubredditWithSelection implements Parcelable { private final String name; private final String iconUrl; private boolean selected; public SubredditWithSelection(String name, String iconUrl) { this.name = name; this.iconUrl = iconUrl; selected = false; } protected SubredditWithSelection(Parcel in) { name = in.readString(); iconUrl = in.readString(); selected = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public SubredditWithSelection createFromParcel(Parcel in) { return new SubredditWithSelection(in); } @Override public SubredditWithSelection[] newArray(int size) { return new SubredditWithSelection[size]; } }; public String getName() { return name; } public String getIconUrl() { return iconUrl; } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { this.selected = selected; } public static ArrayList convertSubscribedSubreddits( List subscribedSubredditData) { ArrayList subredditWithSelections = new ArrayList<>(); for (SubscribedSubredditData s : subscribedSubredditData) { subredditWithSelections.add(new SubredditWithSelection(s.getName(), s.getIconUrl())); } return subredditWithSelections; } public static SubredditWithSelection convertSubreddit(SubredditData subreddit) { return new SubredditWithSelection(subreddit.getName(), subreddit.getIconUrl()); } public int compareName(SubredditWithSelection subredditWithSelection) { if (subredditWithSelection != null) { return name.compareToIgnoreCase(subredditWithSelection.getName()); } else { return -1; } } @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof SubredditWithSelection)) { return false; } else { return this.getName().compareToIgnoreCase(((SubredditWithSelection) obj).getName()) == 0; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(name); parcel.writeString(iconUrl); parcel.writeByte((byte) (selected ? 1 : 0)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subreddit/shortcut/ShortcutManager.java ================================================ package ml.docilealligator.infinityforreddit.subreddit.shortcut; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivity; public class ShortcutManager { private static ShortcutInfoCompat getInfo(Context context, @NonNull String subreddit, @NonNull Bitmap icon) { final Intent shortcut = new Intent(context, ViewSubredditDetailActivity.class); shortcut.setPackage(context.getPackageName()); shortcut.setAction(Intent.ACTION_MAIN); shortcut.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); shortcut.putExtra(ViewSubredditDetailActivity.EXTRA_SUBREDDIT_NAME_KEY, subreddit); String shortcutId = BuildConfig.APPLICATION_ID + ".shortcut." + subreddit; String subredditName = "r/" + subreddit; return new ShortcutInfoCompat.Builder(context, shortcutId) .setIntent(shortcut) .setShortLabel(subredditName) .setAlwaysBadged() .setIcon(IconCompat.createWithBitmap(icon)) .build(); } public static boolean requestPinShortcut(Context context, @NonNull String subreddit, @NonNull Bitmap icon) { return ShortcutManagerCompat.requestPinShortcut(context, getInfo(context, subreddit, icon), null); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribedsubreddit/SubscribedSubredditDao.java ================================================ package ml.docilealligator.infinityforreddit.subscribedsubreddit; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; @Dao public interface SubscribedSubredditDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(SubscribedSubredditData subscribedSubredditData); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List subscribedSubredditDataList); @Query("DELETE FROM subscribed_subreddits") void deleteAllSubscribedSubreddits(); @Query("SELECT * from subscribed_subreddits WHERE username = :accountName AND name LIKE '%' || :searchQuery || '%' ORDER BY name COLLATE NOCASE ASC") LiveData> getAllSubscribedSubredditsWithSearchQuery(String accountName, String searchQuery); @Query("SELECT * from subscribed_subreddits WHERE username = :accountName COLLATE NOCASE ORDER BY name COLLATE NOCASE ASC") List getAllSubscribedSubredditsList(String accountName); @Query("SELECT * from subscribed_subreddits WHERE username = :accountName AND name LIKE '%' || :searchQuery || '%' COLLATE NOCASE AND is_favorite = 1 ORDER BY name COLLATE NOCASE ASC") LiveData> getAllFavoriteSubscribedSubredditsWithSearchQuery(String accountName, String searchQuery); @Query("SELECT * from subscribed_subreddits WHERE name = :subredditName COLLATE NOCASE AND username = :accountName COLLATE NOCASE LIMIT 1") SubscribedSubredditData getSubscribedSubreddit(String subredditName, String accountName); @Query("DELETE FROM subscribed_subreddits WHERE name = :subredditName COLLATE NOCASE AND username = :accountName COLLATE NOCASE") void deleteSubscribedSubreddit(String subredditName, String accountName); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribedsubreddit/SubscribedSubredditData.java ================================================ package ml.docilealligator.infinityforreddit.subscribedsubreddit; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import ml.docilealligator.infinityforreddit.account.Account; @Entity(tableName = "subscribed_subreddits", primaryKeys = {"id", "username"}, foreignKeys = @ForeignKey(entity = Account.class, parentColumns = "username", childColumns = "username", onDelete = ForeignKey.CASCADE), indices = {@Index(value = "username")}) public class SubscribedSubredditData { @NonNull @ColumnInfo(name = "id") private final String id; @ColumnInfo(name = "name") private final String name; @ColumnInfo(name = "icon") private final String iconUrl; @NonNull @ColumnInfo(name = "username") private String username; @ColumnInfo(name = "is_favorite") private boolean favorite; public SubscribedSubredditData(@NonNull String id, String name, String iconUrl, @NonNull String username, boolean favorite) { this.id = id; this.name = name; this.iconUrl = iconUrl; this.username = username; this.favorite = favorite; } @NonNull public String getId() { return id; } public String getName() { return name; } public String getIconUrl() { return iconUrl; } @NonNull public String getUsername() { return username; } public void setUsername(@NonNull String username) { this.username = username; } public boolean isFavorite() { return favorite; } public void setFavorite(boolean favorite) { this.favorite = favorite; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribedsubreddit/SubscribedSubredditRepository.java ================================================ package ml.docilealligator.infinityforreddit.subscribedsubreddit; import androidx.lifecycle.LiveData; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SubscribedSubredditRepository { private final SubscribedSubredditDao mSubscribedSubredditDao; private final String mAccountName; SubscribedSubredditRepository(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mAccountName = accountName; mSubscribedSubredditDao = redditDataRoomDatabase.subscribedSubredditDao(); } LiveData> getAllSubscribedSubredditsWithSearchQuery(String searchQuery) { return mSubscribedSubredditDao.getAllSubscribedSubredditsWithSearchQuery(mAccountName, searchQuery); } public LiveData> getAllFavoriteSubscribedSubredditsWithSearchQuery(String searchQuery) { return mSubscribedSubredditDao.getAllFavoriteSubscribedSubredditsWithSearchQuery(mAccountName, searchQuery); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribedsubreddit/SubscribedSubredditViewModel.java ================================================ package ml.docilealligator.infinityforreddit.subscribedsubreddit; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SubscribedSubredditViewModel extends ViewModel { private final SubscribedSubredditRepository mSubscribedSubredditRepository; private final LiveData> mAllSubscribedSubreddits; private final LiveData> mAllFavoriteSubscribedSubreddits; private final MutableLiveData searchQueryLiveData; public SubscribedSubredditViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mSubscribedSubredditRepository = new SubscribedSubredditRepository(redditDataRoomDatabase, accountName); searchQueryLiveData = new MutableLiveData<>(""); mAllSubscribedSubreddits = Transformations.switchMap(searchQueryLiveData, mSubscribedSubredditRepository::getAllSubscribedSubredditsWithSearchQuery); mAllFavoriteSubscribedSubreddits = Transformations.switchMap(searchQueryLiveData, mSubscribedSubredditRepository::getAllFavoriteSubscribedSubredditsWithSearchQuery); } public LiveData> getAllSubscribedSubreddits() { return mAllSubscribedSubreddits; } public LiveData> getAllFavoriteSubscribedSubreddits() { return mAllFavoriteSubscribedSubreddits; } public void setSearchQuery(String searchQuery) { searchQueryLiveData.postValue(searchQuery); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mAccountName; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { this.mRedditDataRoomDatabase = redditDataRoomDatabase; this.mAccountName = accountName; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new SubscribedSubredditViewModel(mRedditDataRoomDatabase, mAccountName); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribeduser/SubscribedUserDao.java ================================================ package ml.docilealligator.infinityforreddit.subscribeduser; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; @Dao public interface SubscribedUserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(SubscribedUserData subscribedUserData); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List subscribedUserDataList); @Query("SELECT * FROM subscribed_users WHERE username = :accountName AND name LIKE '%' || :searchQuery || '%' COLLATE NOCASE ORDER BY name COLLATE NOCASE ASC") LiveData> getAllSubscribedUsersWithSearchQuery(String accountName, String searchQuery); @Query("SELECT * FROM subscribed_users WHERE username = :accountName COLLATE NOCASE ORDER BY name COLLATE NOCASE ASC") List getAllSubscribedUsersList(String accountName); @Query("SELECT * FROM subscribed_users WHERE username = :accountName AND name LIKE '%' || :searchQuery || '%' COLLATE NOCASE AND is_favorite = 1 ORDER BY name COLLATE NOCASE ASC") LiveData> getAllFavoriteSubscribedUsersWithSearchQuery(String accountName, String searchQuery); @Query("SELECT * FROM subscribed_users WHERE name = :name COLLATE NOCASE AND username = :accountName COLLATE NOCASE LIMIT 1") SubscribedUserData getSubscribedUser(String name, String accountName); @Query("DELETE FROM subscribed_users WHERE name = :name COLLATE NOCASE AND username = :accountName COLLATE NOCASE") void deleteSubscribedUser(String name, String accountName); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribeduser/SubscribedUserData.java ================================================ package ml.docilealligator.infinityforreddit.subscribeduser; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import ml.docilealligator.infinityforreddit.account.Account; @Entity(tableName = "subscribed_users", primaryKeys = {"name", "username"}, foreignKeys = @ForeignKey(entity = Account.class, parentColumns = "username", childColumns = "username", onDelete = ForeignKey.CASCADE), indices = {@Index(value = "username")}) public class SubscribedUserData { @NonNull @ColumnInfo(name = "name") private final String name; @ColumnInfo(name = "icon") private final String iconUrl; @NonNull @ColumnInfo(name = "username") private String username; @ColumnInfo(name = "is_favorite") private boolean favorite; public SubscribedUserData(@NonNull String name, String iconUrl, @NonNull String username, boolean favorite) { this.name = name; this.iconUrl = iconUrl; this.username = username; this.favorite = favorite; } @NonNull public String getName() { return name; } public String getIconUrl() { return iconUrl; } @NonNull public String getUsername() { return username; } public void setUsername(@NonNull String username) { this.username = username; } public boolean isFavorite() { return favorite; } public void setFavorite(boolean favorite) { this.favorite = favorite; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribeduser/SubscribedUserRepository.java ================================================ package ml.docilealligator.infinityforreddit.subscribeduser; import androidx.lifecycle.LiveData; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SubscribedUserRepository { private final SubscribedUserDao mSubscribedUserDao; private final String mAccountName; SubscribedUserRepository(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mSubscribedUserDao = redditDataRoomDatabase.subscribedUserDao(); mAccountName = accountName; } LiveData> getAllSubscribedUsersWithSearchQuery(String searchQuery) { return mSubscribedUserDao.getAllSubscribedUsersWithSearchQuery(mAccountName, searchQuery); } LiveData> getAllFavoriteSubscribedUsersWithSearchQuery(String searchQuery) { return mSubscribedUserDao.getAllFavoriteSubscribedUsersWithSearchQuery(mAccountName, searchQuery); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/subscribeduser/SubscribedUserViewModel.java ================================================ package ml.docilealligator.infinityforreddit.subscribeduser; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class SubscribedUserViewModel extends ViewModel { private final SubscribedUserRepository mSubscribedUserRepository; private final LiveData> mAllSubscribedUsers; private final LiveData> mAllFavoriteSubscribedUsers; private final MutableLiveData searchQueryLiveData; public SubscribedUserViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mSubscribedUserRepository = new SubscribedUserRepository(redditDataRoomDatabase, accountName); searchQueryLiveData = new MutableLiveData<>(""); mAllSubscribedUsers = Transformations.switchMap(searchQueryLiveData, mSubscribedUserRepository::getAllSubscribedUsersWithSearchQuery); mAllFavoriteSubscribedUsers = Transformations.switchMap(searchQueryLiveData, mSubscribedUserRepository::getAllFavoriteSubscribedUsersWithSearchQuery); } public LiveData> getAllSubscribedUsers() { return mAllSubscribedUsers; } public LiveData> getAllFavoriteSubscribedUsers() { return mAllFavoriteSubscribedUsers; } public void setSearchQuery(String searchQuery) { searchQueryLiveData.postValue(searchQuery); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mAccountName; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String accountName) { mRedditDataRoomDatabase = redditDataRoomDatabase; mAccountName = accountName; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new SubscribedUserViewModel(mRedditDataRoomDatabase, mAccountName); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/DeleteThing.java ================================================ package ml.docilealligator.infinityforreddit.thing; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class DeleteThing { public static void delete(Retrofit oauthRetrofit, String fullname, String accessToken, DeleteThingListener deleteThingListener) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, fullname); oauthRetrofit.create(RedditAPI.class).delete(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { deleteThingListener.deleteSuccess(); } else { deleteThingListener.deleteFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { deleteThingListener.deleteFailed(); } }); } public interface DeleteThingListener { void deleteSuccess(); void deleteFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/FavoriteThing.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.asynctasks.InsertSubscribedThings; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FavoriteThing { public static void favoriteSubreddit(Executor executor, Handler handler, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, SubscribedSubredditData subscribedSubredditData, FavoriteThingListener favoriteThingListener) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedSubredditData, favoriteThingListener::success); } else { Map params = new HashMap<>(); params.put(APIUtils.SR_NAME_KEY, subscribedSubredditData.getName()); params.put(APIUtils.MAKE_FAVORITE_KEY, "true"); oauthRetrofit.create(RedditAPI.class).favoriteThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedSubredditData, favoriteThingListener::success); } else { favoriteThingListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { favoriteThingListener.failed(); } }); } } public static void unfavoriteSubreddit(Executor executor, Handler handler, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, SubscribedSubredditData subscribedSubredditData, FavoriteThingListener favoriteThingListener) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedSubredditData, favoriteThingListener::success); } else { Map params = new HashMap<>(); params.put(APIUtils.SR_NAME_KEY, subscribedSubredditData.getName()); params.put(APIUtils.MAKE_FAVORITE_KEY, "false"); oauthRetrofit.create(RedditAPI.class).favoriteThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedSubredditData, favoriteThingListener::success); } else { favoriteThingListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { favoriteThingListener.failed(); } }); } } public static void favoriteUser(Executor executor, Handler handler, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, SubscribedUserData subscribedUserData, FavoriteThingListener favoriteThingListener) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedUserData, favoriteThingListener::success); } else { Map params = new HashMap<>(); params.put(APIUtils.SR_NAME_KEY, "u_" + subscribedUserData.getName()); params.put(APIUtils.MAKE_FAVORITE_KEY, "true"); oauthRetrofit.create(RedditAPI.class).favoriteThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedUserData, favoriteThingListener::success); } else { favoriteThingListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { favoriteThingListener.failed(); } }); } } public static void unfavoriteUser(Executor executor, Handler handler, Retrofit oauthRetrofit, RedditDataRoomDatabase redditDataRoomDatabase, @Nullable String accessToken, @NonNull String accountName, SubscribedUserData subscribedUserData, FavoriteThingListener favoriteThingListener) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedUserData, favoriteThingListener::success); } else { Map params = new HashMap<>(); params.put(APIUtils.SR_NAME_KEY, "u_" + subscribedUserData.getName()); params.put(APIUtils.MAKE_FAVORITE_KEY, "false"); oauthRetrofit.create(RedditAPI.class).favoriteThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { InsertSubscribedThings.insertSubscribedThings(executor, handler, redditDataRoomDatabase, subscribedUserData, favoriteThingListener::success); } else { favoriteThingListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { favoriteThingListener.failed(); } }); } } public interface FavoriteThingListener { void success(); void failed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/FetchRedgifsVideoLinks.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.content.SharedPreferences; import android.os.Handler; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.FetchVideoLinkListener; import ml.docilealligator.infinityforreddit.apis.RedgifsAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; public class FetchRedgifsVideoLinks { public static void fetchRedgifsVideoLinks(Executor executor, Handler handler, Retrofit redgifsRetrofit, SharedPreferences currentAccountSharedPreferences, String redgifsId, FetchVideoLinkListener fetchVideoLinkListener) { executor.execute(() -> { try { // Get valid token String accessToken = getValidAccessToken(redgifsRetrofit, currentAccountSharedPreferences); if (accessToken.isEmpty()) { handler.post(() -> fetchVideoLinkListener.failed(null)); return; } Response response = redgifsRetrofit .create(RedgifsAPI.class) .getRedgifsData(APIUtils.getRedgifsOAuthHeader(accessToken), redgifsId, APIUtils.USER_AGENT) .execute(); if (response.isSuccessful()) { parseRedgifsVideoLinks(handler, response.body(), fetchVideoLinkListener); } else if (response.code() == 401) { // Token expired, try once more with new token accessToken = refreshAccessToken(redgifsRetrofit, currentAccountSharedPreferences); if (!accessToken.isEmpty()) { response = redgifsRetrofit .create(RedgifsAPI.class) .getRedgifsData( APIUtils.getRedgifsOAuthHeader(accessToken), redgifsId, APIUtils.USER_AGENT) .execute(); if (response.isSuccessful()) { parseRedgifsVideoLinks(handler, response.body(), fetchVideoLinkListener); } else { handler.post(() -> fetchVideoLinkListener.failed(null)); } } else { handler.post(() -> fetchVideoLinkListener.failed(null)); } } else { handler.post(() -> fetchVideoLinkListener.failed(null)); } } catch (IOException e) { e.printStackTrace(); handler.post(() -> fetchVideoLinkListener.failed(null)); } }); } @WorkerThread @Nullable public static String fetchRedgifsVideoLinkSync(Retrofit redgifsRetrofit, SharedPreferences currentAccountSharedPreferences, String redgifsId) { try { // Get valid token String accessToken = getValidAccessToken(redgifsRetrofit, currentAccountSharedPreferences); if (accessToken.isEmpty()) { return null; } Response response = redgifsRetrofit .create(RedgifsAPI.class) .getRedgifsData(APIUtils.getRedgifsOAuthHeader(accessToken), redgifsId, APIUtils.USER_AGENT) .execute(); if (response.isSuccessful()) { return parseRedgifsVideoLinks(response.body()); } else if (response.code() == 401) { // Token expired, try once more with new token accessToken = refreshAccessToken(redgifsRetrofit, currentAccountSharedPreferences); if (!accessToken.isEmpty()) { response = redgifsRetrofit .create(RedgifsAPI.class) .getRedgifsData( APIUtils.getRedgifsOAuthHeader(accessToken), redgifsId, APIUtils.USER_AGENT) .execute(); if (response.isSuccessful()) { return parseRedgifsVideoLinks(response.body()); } } return null; } else { return null; } } catch (IOException e) { e.printStackTrace(); return null; } } public static void fetchRedgifsVideoLinksInRecyclerViewAdapter(Executor executor, Handler handler, Call redgifsCall, FetchVideoLinkListener fetchVideoLinkListener) { executor.execute(() -> { try { Response response = redgifsCall.execute(); if (response.isSuccessful()) { parseRedgifsVideoLinks(handler, response.body(), fetchVideoLinkListener); } else { handler.post(() -> fetchVideoLinkListener.failed(null)); } } catch (IOException e) { e.printStackTrace(); handler.post(() -> fetchVideoLinkListener.failed(null)); } }); } private static void parseRedgifsVideoLinks(Handler handler, String response, FetchVideoLinkListener fetchVideoLinkListener) { /*try { *//*String mp4 = new JSONObject(response).getJSONObject(JSONUtils.GIF_KEY).getJSONObject(JSONUtils.URLS_KEY) .getString(JSONUtils.HD_KEY); if (mp4.contains("-silent")) { mp4 = mp4.substring(0, mp4.indexOf("-silent")) + ".mp4"; } final String mp4Name = mp4; handler.post(() -> fetchVideoLinkListener.onFetchRedgifsVideoLinkSuccess(mp4Name, mp4Name));*//* String mp4 = new JSONObject(response).getString(JSONUtils.VIDEO_DOWNLOAD_URL); handler.post(() -> fetchVideoLinkListener.onFetchRedgifsVideoLinkSuccess(mp4, mp4)); } catch (JSONException e) { e.printStackTrace(); handler.post(() -> fetchVideoLinkListener.failed(null)); }*/ try { JSONObject jsonResponse = new JSONObject(response); JSONObject gif = jsonResponse.getJSONObject(JSONUtils.GIF_KEY); JSONObject urls = gif.getJSONObject(JSONUtils.URLS_KEY); // Try HD first, fall back to SD if not available String mp4; if (urls.has(JSONUtils.HD_KEY)) { mp4 = urls.getString(JSONUtils.HD_KEY); } else if (urls.has("sd")) { mp4 = urls.getString("sd"); } else { handler.post(() -> fetchVideoLinkListener.failed(null)); return; } if (mp4.contains("-silent")) { mp4 = mp4.substring(0, mp4.indexOf("-silent")) + ".mp4"; } final String mp4Name = mp4; handler.post(() -> fetchVideoLinkListener.onFetchRedgifsVideoLinkSuccess(mp4Name, mp4Name)); } catch (JSONException e) { e.printStackTrace(); handler.post(() -> fetchVideoLinkListener.failed(null)); } } @Nullable private static String parseRedgifsVideoLinks(String response) { try { JSONObject jsonResponse = new JSONObject(response); JSONObject gif = jsonResponse.getJSONObject(JSONUtils.GIF_KEY); JSONObject urls = gif.getJSONObject(JSONUtils.URLS_KEY); // Try HD first, fall back to SD if not available if (urls.has(JSONUtils.HD_KEY)) { return urls.getString(JSONUtils.HD_KEY); } else if (urls.has("sd")) { return urls.getString("sd"); } else { return null; } } catch (JSONException e) { e.printStackTrace(); return null; } } private static String getValidAccessToken(Retrofit redgifsRetrofit, SharedPreferences currentAccountSharedPreferences) { // Check if existing token is valid APIUtils.RedgifsAuthToken currentToken = APIUtils.REDGIFS_TOKEN.get(); if (currentToken.isValid()) { return currentToken.token; } // Get new token if current one is invalid return refreshAccessToken(redgifsRetrofit, currentAccountSharedPreferences); } private static String refreshAccessToken(Retrofit redgifsRetrofit, SharedPreferences currentAccountSharedPreferences) { try { RedgifsAPI api = redgifsRetrofit.create(RedgifsAPI.class); retrofit2.Response response = api.getRedgifsTemporaryToken().execute(); if (response.isSuccessful() && response.body() != null) { String newAccessToken = new JSONObject(response.body()).getString("token"); // Update both the atomic reference and shared preferences APIUtils.RedgifsAuthToken newToken = APIUtils.RedgifsAuthToken.expireIn1day(newAccessToken); APIUtils.REDGIFS_TOKEN.set(newToken); currentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, newAccessToken).apply(); return newAccessToken; } } catch (Exception e) { e.printStackTrace(); } return ""; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/FetchSubscribedThing.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.subreddit.SubredditData; import ml.docilealligator.infinityforreddit.subscribedsubreddit.SubscribedSubredditData; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.Utils; import retrofit2.Response; import retrofit2.Retrofit; public class FetchSubscribedThing { public static void fetchSubscribedThing(Executor executor, Handler handler, final Retrofit oauthRetrofit, @Nullable String accessToken, @NonNull String accountName, final String lastItem, final ArrayList subscribedSubredditData, final ArrayList subscribedUserData, final ArrayList subredditData, final FetchSubscribedThingListener fetchSubscribedThingListener) { executor.execute(() -> { try { Response response = oauthRetrofit.create(RedditAPI.class).getSubscribedThing(lastItem, APIUtils.getOAuthHeader(accessToken)).execute(); if (response.isSuccessful()) { JSONObject jsonResponse = new JSONObject(response.body()); JSONArray children = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); List newSubscribedSubredditData = new ArrayList<>(); List newSubscribedUserData = new ArrayList<>(); List newSubredditData = new ArrayList<>(); for (int i = 0; i < children.length(); i++) { try { JSONObject data = children.getJSONObject(i).getJSONObject(JSONUtils.DATA_KEY); String name = data.getString(JSONUtils.DISPLAY_NAME_KEY); String bannerImageUrl = data.getString(JSONUtils.BANNER_BACKGROUND_IMAGE_KEY); if (bannerImageUrl.equals("") || bannerImageUrl.equals("null")) { bannerImageUrl = data.getString(JSONUtils.BANNER_IMG_KEY); if (bannerImageUrl.equals("null")) { bannerImageUrl = ""; } } String iconUrl = data.getString(JSONUtils.COMMUNITY_ICON_KEY); if (iconUrl.equals("") || iconUrl.equals("null")) { iconUrl = data.getString(JSONUtils.ICON_IMG_KEY); if (iconUrl.equals("null")) { iconUrl = ""; } } String id = data.getString(JSONUtils.NAME_KEY); boolean isFavorite = data.getBoolean(JSONUtils.USER_HAS_FAVORITED_KEY); if (data.getString(JSONUtils.SUBREDDIT_TYPE_KEY) .equals(JSONUtils.SUBREDDIT_TYPE_VALUE_USER)) { //It's a user newSubscribedUserData.add(new SubscribedUserData(name.substring(2), iconUrl, accountName, isFavorite)); } else { String subredditFullName = data.getString(JSONUtils.DISPLAY_NAME_KEY); String description = data.getString(JSONUtils.PUBLIC_DESCRIPTION_KEY).trim(); String sidebarDescription = Utils.modifyMarkdown(data.getString(JSONUtils.DESCRIPTION_KEY).trim()); int nSubscribers = data.getInt(JSONUtils.SUBSCRIBERS_KEY); long createdUTC = data.getLong(JSONUtils.CREATED_UTC_KEY) * 1000; String suggestedCommentSort = data.getString(JSONUtils.SUGGESTED_COMMENT_SORT_KEY); boolean isNSFW = data.getBoolean(JSONUtils.OVER18_KEY); newSubscribedSubredditData.add(new SubscribedSubredditData(id, name, iconUrl, accountName, isFavorite)); newSubredditData.add(new SubredditData(id, subredditFullName, iconUrl, bannerImageUrl, description, sidebarDescription, nSubscribers, createdUTC, suggestedCommentSort, isNSFW)); } } catch (JSONException e) { e.printStackTrace(); } } subscribedSubredditData.addAll(newSubscribedSubredditData); subscribedUserData.addAll(newSubscribedUserData); subredditData.addAll(newSubredditData); String newLastItem = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.AFTER_KEY); if (newLastItem.equals("null")) { handler.post(() -> fetchSubscribedThingListener.onFetchSubscribedThingSuccess( subscribedSubredditData, subscribedUserData, subredditData)); } else { handler.post(() -> fetchSubscribedThing(executor, handler, oauthRetrofit, accessToken, accountName, newLastItem, subscribedSubredditData, subscribedUserData, subredditData, fetchSubscribedThingListener)); } } else { handler.post(fetchSubscribedThingListener::onFetchSubscribedThingFail); } } catch (JSONException | IOException e) { e.printStackTrace(); handler.post(fetchSubscribedThingListener::onFetchSubscribedThingFail); } }); } public interface FetchSubscribedThingListener { void onFetchSubscribedThingSuccess(ArrayList subscribedSubredditData, ArrayList subscribedUserData, ArrayList subredditData); void onFetchSubscribedThingFail(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/GiphyGif.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; public class GiphyGif implements Parcelable { public final String id; private GiphyGif(String id) { this.id = id; } public GiphyGif(String id, boolean modifyId) { this(modifyId ? "giphy|" + id + "|downsized" : id); } protected GiphyGif(Parcel in) { id = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public GiphyGif createFromParcel(Parcel in) { return new GiphyGif(in); } @Override public GiphyGif[] newArray(int size) { return new GiphyGif[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(id); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/MediaMetadata.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class MediaMetadata implements Parcelable { public String id; //E.g. Image public String e; public String fileName; public String caption; public boolean isGIF; public MediaItem original; public MediaItem downscaled; public MediaMetadata(String id, String e, MediaItem original, MediaItem downscaled) { this.id = id; this.e = e; isGIF = !e.equalsIgnoreCase("image"); String path = Uri.parse(original.url).getPath(); this.fileName = path == null ? (isGIF ? "Animated.gif" : "Image.jpg") : path.substring(path.lastIndexOf('/') + 1); this.original = original; this.downscaled = downscaled; } protected MediaMetadata(Parcel in) { id = in.readString(); e = in.readString(); fileName = in.readString(); caption = in.readString(); isGIF = in.readByte() != 0; original = in.readParcelable(MediaItem.class.getClassLoader()); downscaled = in.readParcelable(MediaItem.class.getClassLoader()); } public static final Creator CREATOR = new Creator() { @Override public MediaMetadata createFromParcel(Parcel in) { return new MediaMetadata(in); } @Override public MediaMetadata[] newArray(int size) { return new MediaMetadata[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(id); dest.writeString(e); dest.writeString(fileName); dest.writeString(caption); dest.writeByte((byte) (isGIF ? 1 : 0)); dest.writeParcelable(original, flags); dest.writeParcelable(downscaled, flags); } public static class MediaItem implements Parcelable { public int x; public int y; //Image or gif public String url; //Only for gifs @Nullable public String mp4Url; public MediaItem(int x, int y, String url) { this.x = x; this.y = y; this.url = url; } public MediaItem(int x, int y, String url, String mp4Url) { this.x = x; this.y = y; this.url = url; this.mp4Url = mp4Url; } protected MediaItem(Parcel in) { x = in.readInt(); y = in.readInt(); url = in.readString(); mp4Url = in.readString(); } public static final Creator CREATOR = new Creator() { @Override public MediaItem createFromParcel(Parcel in) { return new MediaItem(in); } @Override public MediaItem[] newArray(int size) { return new MediaItem[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(x); dest.writeInt(y); dest.writeString(url); dest.writeString(mp4Url); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/ReplyNotificationsToggle.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.os.Handler; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ReplyNotificationsToggle { public static void toggleEnableNotification(Handler handler, Retrofit oauthRetrofit, String accessToken, Comment comment, SendNotificationListener sendNotificationListener) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, comment.getFullName()); params.put(APIUtils.STATE_KEY, String.valueOf(!comment.isSendReplies())); oauthRetrofit.create(RedditAPI.class).toggleRepliesNotification(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { handler.post(sendNotificationListener::onSuccess); } else { handler.post(sendNotificationListener::onError); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { handler.post(sendNotificationListener::onError); } }); } public interface SendNotificationListener { void onSuccess(); void onError(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/ReportReason.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.subreddit.Rule; public class ReportReason implements Parcelable { public static final String REASON_TYPE_SITE_REASON = "site_reason"; public static final String REASON_TYPE_RULE_REASON = "rule_reason"; public static final String REASON_TYPE_OTHER_REASON = "other_reason"; public static final String REASON_SITE_REASON_SELECTED = "site_reason_selected"; public static final String REASON_RULE_REASON_SELECTED = "rule_reason_selected"; public static final String REASON_OTHER = "other"; private final String reportReason; private final String reasonType; private boolean isSelected; public ReportReason(String reportReason, String reasonType) { this.reportReason = reportReason; this.reasonType = reasonType; this.isSelected = false; } protected ReportReason(Parcel in) { reportReason = in.readString(); reasonType = in.readString(); isSelected = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public ReportReason createFromParcel(Parcel in) { return new ReportReason(in); } @Override public ReportReason[] newArray(int size) { return new ReportReason[size]; } }; public String getReportReason() { return reportReason; } public String getReasonType() { return reasonType; } public boolean isSelected() { return isSelected; } public void setSelected(boolean selected) { isSelected = selected; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(reportReason); parcel.writeString(reasonType); parcel.writeByte((byte) (isSelected ? 1 : 0)); } public static ArrayList getGeneralReasons(Context context) { ArrayList reportReasons = new ArrayList<>(); reportReasons.add(new ReportReason(context.getString(R.string.report_reason_general_spam), REASON_TYPE_SITE_REASON)); reportReasons.add(new ReportReason(context.getString(R.string.report_reason_general_copyright_issue), REASON_TYPE_SITE_REASON)); reportReasons.add(new ReportReason(context.getString(R.string.report_reason_general_child_pornography), REASON_TYPE_SITE_REASON)); reportReasons.add(new ReportReason(context.getString(R.string.report_reason_general_abusive_content), REASON_TYPE_SITE_REASON)); return reportReasons; } public static ArrayList convertRulesToReasons(ArrayList rules) { ArrayList reportReasons = new ArrayList<>(); for (Rule r : rules) { reportReasons.add(new ReportReason(r.getShortName(), REASON_TYPE_RULE_REASON)); } return reportReasons; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/ReportThing.java ================================================ package ml.docilealligator.infinityforreddit.thing; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class ReportThing { public interface ReportThingListener { void success(); void failed(); } public static void reportThing(Retrofit oauthRetrofit, String accessToken, String thingFullname, String subredditName, String reasonType, String reason, ReportThingListener reportThingListener) { Map header = APIUtils.getOAuthHeader(accessToken); Map params = new HashMap<>(); params.put(APIUtils.THING_ID_KEY, thingFullname); params.put(APIUtils.SR_NAME_KEY, subredditName); params.put(reasonType, reason); if (reasonType.equals(ReportReason.REASON_TYPE_SITE_REASON)) { params.put(APIUtils.REASON_KEY, ReportReason.REASON_SITE_REASON_SELECTED); } else if (reasonType.equals(ReportReason.REASON_TYPE_RULE_REASON)) { params.put(APIUtils.REASON_KEY, ReportReason.REASON_RULE_REASON_SELECTED); } else { params.put(APIUtils.REASON_KEY, ReportReason.REASON_OTHER); } params.put(APIUtils.API_TYPE_KEY, APIUtils.API_TYPE_JSON); oauthRetrofit.create(RedditAPI.class).report(header, params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { reportThingListener.success(); } else { reportThingListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { reportThingListener.failed(); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/SaveThing.java ================================================ package ml.docilealligator.infinityforreddit.thing; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class SaveThing { public static void saveThing(Retrofit oauthRetrofit, String accessToken, String fullname, SaveThingListener saveThingListener) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, fullname); oauthRetrofit.create(RedditAPI.class).save(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { saveThingListener.success(); } else { saveThingListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { saveThingListener.failed(); } }); } public static void unsaveThing(Retrofit oauthRetrofit, String accessToken, String fullname, SaveThingListener saveThingListener) { Map params = new HashMap<>(); params.put(APIUtils.ID_KEY, fullname); oauthRetrofit.create(RedditAPI.class).unsave(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { saveThingListener.success(); } else { saveThingListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { saveThingListener.failed(); } }); } public interface SaveThingListener { void success(); void failed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/SelectThingReturnKey.java ================================================ package ml.docilealligator.infinityforreddit.thing; import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class SelectThingReturnKey { public static final String RETURN_EXTRA_SUBREDDIT_OR_USER_NAME = "RESOUN"; public static final String RETURN_EXTRA_SUBREDDIT_OR_USER_ICON = "RESOUI"; public static final String RETRUN_EXTRA_MULTIREDDIT = "REM"; public static final String RETURN_EXTRA_THING_TYPE = "RETT"; @IntDef({THING_TYPE.SUBREDDIT, THING_TYPE.USER, THING_TYPE.MULTIREDDIT}) @Retention(RetentionPolicy.SOURCE) public @interface THING_TYPE { int SUBREDDIT = 0; int USER = 1; int MULTIREDDIT = 2; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/SortType.java ================================================ package ml.docilealligator.infinityforreddit.thing; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class SortType { @NonNull private final Type type; @Nullable private final Time time; public SortType(@NonNull Type type) { this(type, null); } public SortType(@NonNull Type type, @Nullable Time time) { this.type = type; this.time = time; } @NonNull public Type getType() { return type; } @Nullable public Time getTime() { return time; } public enum Type { BEST("best", "Best"), HOT("hot", "Hot"), NEW("new", "New"), RANDOM("random", "Random"), RISING("rising", "Rising"), TOP("top", "Top"), CONTROVERSIAL("controversial", "Controversial"), RELEVANCE("relevance", "Relevance"), COMMENTS("comments", "Comments"), ACTIVITY("activity", "Activity"), CONFIDENCE("confidence", "Best"), OLD("old", "Old"), QA("qa", "QA"), LIVE("live", "Live"); public final String value; public final String fullName; Type(String value, String fullName) { this.value = value; this.fullName = fullName; } } public enum Time { HOUR("hour", "Hour"), DAY("day", "Day"), WEEK("week", "Week"), MONTH("month", "Month"), YEAR("year", "Year"), ALL("all", "All Time"); public final String value; public final String fullName; Time(String value, String fullName) { this.value = value; this.fullName = fullName; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/SortTypeSelectionCallback.java ================================================ package ml.docilealligator.infinityforreddit.thing; public interface SortTypeSelectionCallback { default void sortTypeSelected(SortType sortType){} default void sortTypeSelected(String sortType){} default void searchUserAndSubredditSortTypeSelected(SortType sortType, int fragmentPosition){} } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/StreamableVideo.java ================================================ package ml.docilealligator.infinityforreddit.thing; import androidx.annotation.Nullable; public class StreamableVideo { public String title; @Nullable public Media mp4; @Nullable public Media mp4Mobile; public StreamableVideo(String title, @Nullable Media mp4, @Nullable Media mp4Mobile) { this.title = title; this.mp4 = mp4; this.mp4Mobile = mp4Mobile; } public static class Media { public String url; public int width; public int height; public Media(String url, int width, int height) { this.url = url; this.width = width; this.height = height; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/TrendingSearch.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.os.Parcel; import android.os.Parcelable; import java.util.ArrayList; import ml.docilealligator.infinityforreddit.post.Post; public class TrendingSearch implements Parcelable { public String queryString; public String displayString; public String title; public ArrayList previews; public TrendingSearch(String queryString, String displayString, String title, ArrayList previews) { this.queryString = queryString; this.displayString = displayString; this.title = title; this.previews = previews; } protected TrendingSearch(Parcel in) { queryString = in.readString(); displayString = in.readString(); title = in.readString(); previews = in.createTypedArrayList(Post.Preview.CREATOR); } public static final Creator CREATOR = new Creator() { @Override public TrendingSearch createFromParcel(Parcel in) { return new TrendingSearch(in); } @Override public TrendingSearch[] newArray(int size) { return new TrendingSearch[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(queryString); parcel.writeString(displayString); parcel.writeString(title); parcel.writeTypedList(previews); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/UploadedImage.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.os.Parcel; import android.os.Parcelable; import com.google.gson.Gson; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; import java.util.ArrayList; import java.util.List; public class UploadedImage implements Parcelable { public String imageName; public String imageUrlOrKey; private String caption = ""; public UploadedImage(String imageName, String imageUrlOrKey) { this.imageName = imageName; this.imageUrlOrKey = imageUrlOrKey; } protected UploadedImage(Parcel in) { imageName = in.readString(); imageUrlOrKey = in.readString(); caption = in.readString(); } public String getCaption() { return caption == null ? "" : caption; } public void setCaption(String caption) { this.caption = caption == null ? "" : caption; } public static final Creator CREATOR = new Creator() { @Override public UploadedImage createFromParcel(Parcel in) { return new UploadedImage(in); } @Override public UploadedImage[] newArray(int size) { return new UploadedImage[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(imageName); parcel.writeString(imageUrlOrKey); parcel.writeString(caption); } public static String getArrayListJSONModel(ArrayList uploadedImages) { return new Gson().toJson(uploadedImages, new TypeToken>(){}.getType()); } public static List fromListJson(String json) throws JsonParseException { return new Gson().fromJson(json, new TypeToken>(){}.getType()); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/thing/VoteThing.java ================================================ package ml.docilealligator.infinityforreddit.thing; import android.content.Context; import android.widget.Toast; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Retrofit; /** * Created by alex on 3/14/18. */ public class VoteThing { public static void voteThing(Context context, final Retrofit retrofit, String accessToken, final VoteThingListener voteThingListener, final String fullName, final String point, final int position) { RedditAPI api = retrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.DIR_KEY, point); params.put(APIUtils.ID_KEY, fullName); params.put(APIUtils.RANK_KEY, APIUtils.RANK); Call voteThingCall = api.voteThing(APIUtils.getOAuthHeader(accessToken), params); voteThingCall.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { if (response.isSuccessful()) { voteThingListener.onVoteThingSuccess(position); } else { voteThingListener.onVoteThingFail(position); Toast.makeText(context, "Code " + response.code() + " Body: " + response.body(), Toast.LENGTH_LONG).show(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { voteThingListener.onVoteThingFail(position); Toast.makeText(context, "Network error " + "Body: " + t.getMessage(), Toast.LENGTH_LONG).show(); } }); } public static void voteThing(Context context, final Retrofit retrofit, String accessToken, final VoteThingWithoutPositionListener voteThingWithoutPositionListener, final String fullName, final String point) { RedditAPI api = retrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.DIR_KEY, point); params.put(APIUtils.ID_KEY, fullName); params.put(APIUtils.RANK_KEY, APIUtils.RANK); Call voteThingCall = api.voteThing(APIUtils.getOAuthHeader(accessToken), params); voteThingCall.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { if (response.isSuccessful()) { voteThingWithoutPositionListener.onVoteThingSuccess(); } else { voteThingWithoutPositionListener.onVoteThingFail(); Toast.makeText(context, "Code " + response.code() + " Body: " + response.body(), Toast.LENGTH_LONG).show(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { voteThingWithoutPositionListener.onVoteThingFail(); Toast.makeText(context, "Network error " + "Body: " + t.getMessage(), Toast.LENGTH_LONG).show(); } }); } public interface VoteThingListener { void onVoteThingSuccess(int position); void onVoteThingFail(int position); } public interface VoteThingWithoutPositionListener { void onVoteThingSuccess(); void onVoteThingFail(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/BlockUser.java ================================================ package ml.docilealligator.infinityforreddit.user; import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class BlockUser { public interface BlockUserListener { void success(); void failed(); } public static void blockUser(Retrofit oauthRetrofit, String accessToken, String username, BlockUserListener blockUserListener) { Map params = new HashMap<>(); params.put(APIUtils.NAME_KEY, username); oauthRetrofit.create(RedditAPI.class).blockUser(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { blockUserListener.success(); } else { blockUserListener.failed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { blockUserListener.failed(); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/FetchUserData.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchUserData { public static void fetchUserData(Executor executor, Handler handler, Retrofit retrofit, String userName, FetchUserDataListener fetchUserDataListener) { fetchUserData(executor, handler, null, null, retrofit, null, userName, fetchUserDataListener); } public static void fetchUserData(Executor executor, Handler handler, @Nullable RedditDataRoomDatabase redditDataRoomDatabase, @Nullable Retrofit oauthRetrofit, @Nullable Retrofit retrofit, String accessToken, String username, FetchUserDataListener fetchUserDataListener) { executor.execute(() -> { Call userInfo; boolean isOauth; if (retrofit != null && (redditDataRoomDatabase == null || oauthRetrofit == null)) { userInfo = retrofit.create(RedditAPI.class).getUserData(username); isOauth = false; } else if (oauthRetrofit != null) { userInfo = oauthRetrofit.create(RedditAPI.class).getUserDataOauth(APIUtils.getOAuthHeader(accessToken), username); isOauth = true; } else { // Shouldn't happen, please check why both retrofit are null handler.post(fetchUserDataListener::onFetchUserDataFailed); return; } try { Response response = userInfo.execute(); if (response.isSuccessful()) { processFetchUserDataResponse(response, handler, redditDataRoomDatabase, fetchUserDataListener); } else { if (oauthRetrofit == null || isOauth) { handler.post(fetchUserDataListener::onFetchUserDataFailed); } else { forceOauthFetchUserData(handler, redditDataRoomDatabase, oauthRetrofit, accessToken, username, fetchUserDataListener); } } } catch (IOException | JSONException e) { e.printStackTrace(); if (oauthRetrofit == null || isOauth) { handler.post(fetchUserDataListener::onFetchUserDataFailed); } else { forceOauthFetchUserData(handler, redditDataRoomDatabase, oauthRetrofit, accessToken, username, fetchUserDataListener); } } }); } @WorkerThread private static void processFetchUserDataResponse(Response response, Handler handler, @Nullable RedditDataRoomDatabase redditDataRoomDatabase, FetchUserDataListener fetchUserDataListener) throws JSONException { JSONObject jsonResponse = new JSONObject(response.body()); UserData userData = parseUserDataBase(jsonResponse, true); if (redditDataRoomDatabase != null) { redditDataRoomDatabase.accountDao().updateAccountInfo(userData.getName(), userData.getIconUrl(), userData.getBanner(), userData.getTotalKarma(), userData.isMod()); } if (jsonResponse.getJSONObject(JSONUtils.DATA_KEY).has(JSONUtils.INBOX_COUNT_KEY)) { int inboxCount = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getInt(JSONUtils.INBOX_COUNT_KEY); handler.post(() -> fetchUserDataListener.onFetchUserDataSuccess(userData, inboxCount)); } else { handler.post(() -> fetchUserDataListener.onFetchUserDataSuccess(userData, -1)); } } @WorkerThread private static void forceOauthFetchUserData(Handler handler, @Nullable RedditDataRoomDatabase redditDataRoomDatabase, Retrofit oauthRetrofit, String accessToken, String username, FetchUserDataListener fetchUserDataListener) { try { Response response = oauthRetrofit.create(RedditAPI.class).getUserDataOauth( APIUtils.getOAuthHeader(accessToken), username ).execute(); if (response.isSuccessful()) { processFetchUserDataResponse(response, handler, redditDataRoomDatabase, fetchUserDataListener); } else { handler.post(fetchUserDataListener::onFetchUserDataFailed); } } catch (IOException | JSONException e) { e.printStackTrace(); handler.post(fetchUserDataListener::onFetchUserDataFailed); } } public static void fetchUserListingData(Executor executor, Handler handler, Retrofit retrofit, String query, String after, SortType.Type sortType, boolean nsfw, FetchUserListingDataListener fetchUserListingDataListener) { RedditAPI api = retrofit.create(RedditAPI.class); Call userInfo = api.searchUsers(query, after, sortType, nsfw ? 1 : 0); final String[] responseString = {null}; userInfo.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { try { responseString[0] = response.body(); JSONObject jsonResponse = new JSONObject(responseString[0]); String newAfter = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.AFTER_KEY); JSONArray children = jsonResponse.getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); List userDataList = new ArrayList<>(); for (int i = 0; i < children.length(); i++) { try { UserData userData = parseUserDataBase(children.getJSONObject(i), false); userDataList.add(userData); } catch (JSONException e) { e.printStackTrace(); } } // On the first page, also try a direct user lookup for the exact query // to handle usernames that Reddit search doesn't find (e.g. hyphenated names) if (after == null) { fetchAndPrependExactUser(executor, handler, retrofit, query, userDataList, newAfter, fetchUserListingDataListener); } else { handler.post(() -> fetchUserListingDataListener.onFetchUserListingDataSuccess(userDataList, newAfter)); } } catch (JSONException e) { handler.post(() -> { if (responseString[0] != null && responseString[0].equals("\"{}\"")) { // Still try direct lookup even when search returns empty if (after == null) { fetchAndPrependExactUser(executor, handler, retrofit, query, new ArrayList<>(), null, fetchUserListingDataListener); } else { fetchUserListingDataListener.onFetchUserListingDataSuccess(new ArrayList<>(), null); } } else { fetchUserListingDataListener.onFetchUserListingDataFailed(); } }); } }); } else { fetchUserListingDataListener.onFetchUserListingDataFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { fetchUserListingDataListener.onFetchUserListingDataFailed(); } }); } private static void fetchAndPrependExactUser(Executor executor, Handler handler, Retrofit retrofit, String query, List searchResults, String after, FetchUserListingDataListener fetchUserListingDataListener) { executor.execute(() -> { try { Response directResponse = retrofit.create(RedditAPI.class).getUserData(query).execute(); if (directResponse.isSuccessful() && directResponse.body() != null) { UserData exactUser = parseUserDataBase(new JSONObject(directResponse.body()), false); if (exactUser != null) { // Remove duplicate if the exact user already exists in search results searchResults.removeIf(u -> u.getName().equalsIgnoreCase(exactUser.getName())); searchResults.add(0, exactUser); } } } catch (IOException | JSONException e) { // Direct lookup failed; just use the search results as-is } handler.post(() -> fetchUserListingDataListener.onFetchUserListingDataSuccess(searchResults, after)); }); } @WorkerThread private static UserData parseUserDataBase(JSONObject userDataJson, boolean parseFullKarma) throws JSONException { if (userDataJson == null) { return null; } userDataJson = userDataJson.getJSONObject(JSONUtils.DATA_KEY); String userName = userDataJson.getString(JSONUtils.NAME_KEY); String iconImageUrl = userDataJson.getString(JSONUtils.ICON_IMG_KEY); String bannerImageUrl = ""; boolean canBeFollowed; boolean isNsfw; String description; String title; if (userDataJson.has(JSONUtils.SUBREDDIT_KEY) && !userDataJson.isNull(JSONUtils.SUBREDDIT_KEY)) { bannerImageUrl = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getString(JSONUtils.BANNER_IMG_KEY); isNsfw = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getBoolean(JSONUtils.OVER_18_KEY); description = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getString(JSONUtils.PUBLIC_DESCRIPTION_KEY); title = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getString(JSONUtils.TITLE_KEY); canBeFollowed = true; } else { isNsfw = false; description = ""; title = ""; canBeFollowed = false; } int linkKarma = userDataJson.getInt(JSONUtils.LINK_KARMA_KEY); int commentKarma = userDataJson.getInt(JSONUtils.COMMENT_KARMA_KEY); int awarderKarma = 0; int awardeeKarma = 0; int totalKarma = linkKarma + commentKarma; if (parseFullKarma) { awarderKarma = userDataJson.getInt(JSONUtils.AWARDER_KARMA_KEY); awardeeKarma = userDataJson.getInt(JSONUtils.AWARDEE_KARMA_KEY); totalKarma = userDataJson.getInt(JSONUtils.TOTAL_KARMA_KEY); } long cakeday = userDataJson.getLong(JSONUtils.CREATED_UTC_KEY) * 1000; boolean isGold = userDataJson.getBoolean(JSONUtils.IS_GOLD_KEY); boolean isFriend = userDataJson.getBoolean(JSONUtils.IS_FRIEND_KEY); boolean isMod = userDataJson.getBoolean(JSONUtils.IS_MOD_KEY); return new UserData(userName, iconImageUrl, bannerImageUrl, linkKarma, commentKarma, awarderKarma, awardeeKarma, totalKarma, cakeday, isGold, isFriend, canBeFollowed, isNsfw, description, title, isMod); } public interface FetchUserDataListener { void onFetchUserDataSuccess(UserData userData, int inboxCount); void onFetchUserDataFailed(); } public interface FetchUserListingDataListener { void onFetchUserListingDataSuccess(List userData, String after); void onFetchUserListingDataFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/FetchUserFlairs.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import android.text.Html; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class FetchUserFlairs { public static void fetchUserFlairsInSubreddit(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, String subredditName, FetchUserFlairsInSubredditListener fetchUserFlairsInSubredditListener) { oauthRetrofit.create(RedditAPI.class).getUserFlairs(APIUtils.getOAuthHeader(accessToken), subredditName) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { ArrayList userFlairs = parseUserFlairs(response.body()); if (userFlairs == null) { handler.post(fetchUserFlairsInSubredditListener::fetchFailed); } else { handler.post(() -> fetchUserFlairsInSubredditListener.fetchSuccessful(userFlairs)); } }); } else if (response.code() == 403) { //No flairs fetchUserFlairsInSubredditListener.fetchSuccessful(null); } else { fetchUserFlairsInSubredditListener.fetchFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { fetchUserFlairsInSubredditListener.fetchFailed(); } }); } @WorkerThread private static ArrayList parseUserFlairs(String response) { try { JSONArray jsonArray = new JSONArray(response); ArrayList userFlairs = new ArrayList<>(); for (int i = 0; i < jsonArray.length(); i++) { try { JSONObject userFlairObject = jsonArray.getJSONObject(i); String id = userFlairObject.getString(JSONUtils.ID_KEY); String text = userFlairObject.getString(JSONUtils.TEXT_KEY); boolean editable = userFlairObject.getBoolean(JSONUtils.TEXT_EDITABLE_KEY); int maxEmojis = userFlairObject.getInt(JSONUtils.MAX_EMOJIS_KEY); StringBuilder authorFlairHTMLBuilder = new StringBuilder(); if (userFlairObject.has(JSONUtils.RICHTEXT_KEY)) { JSONArray flairArray = userFlairObject.getJSONArray(JSONUtils.RICHTEXT_KEY); for (int j = 0; j < flairArray.length(); j++) { JSONObject flairObject = flairArray.getJSONObject(j); String e = flairObject.getString(JSONUtils.E_KEY); if (e.equals("text")) { authorFlairHTMLBuilder.append(Html.escapeHtml(flairObject.getString(JSONUtils.T_KEY))); } else if (e.equals("emoji")) { authorFlairHTMLBuilder.append(""); } } } userFlairs.add(new UserFlair(id, text, authorFlairHTMLBuilder.toString(), editable, maxEmojis)); } catch (JSONException e) { e.printStackTrace(); } } return userFlairs; } catch (JSONException e) { e.printStackTrace(); } return null; } public interface FetchUserFlairsInSubredditListener { void fetchSuccessful(@Nullable ArrayList userFlairs); void fetchFailed(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/SelectUserFlair.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class SelectUserFlair { public interface SelectUserFlairListener { void success(); void failed(String errorMessage); } public static void selectUserFlair(Executor executor, Handler handler, Retrofit oauthRetrofit, String accessToken, @Nullable UserFlair userFlair, String subredditName, @NonNull String accountName, SelectUserFlairListener selectUserFlairListener) { Map params = new HashMap<>(); params.put(APIUtils.API_TYPE_KEY, APIUtils.API_TYPE_JSON); if (userFlair != null) { params.put(APIUtils.FLAIR_TEMPLATE_ID_KEY, userFlair.getId()); params.put(APIUtils.TEXT_KEY, userFlair.getText()); } params.put(APIUtils.NAME_KEY, accountName); oauthRetrofit.create(RedditAPI.class).selectUserFlair(APIUtils.getOAuthHeader(accessToken), params, subredditName).enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { executor.execute(() -> { try { JSONObject responseObject = new JSONObject(response.body()).getJSONObject(JSONUtils.JSON_KEY); if (responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() != 0) { JSONArray error = responseObject.getJSONArray(JSONUtils.ERRORS_KEY) .getJSONArray(responseObject.getJSONArray(JSONUtils.ERRORS_KEY).length() - 1); if (error.length() != 0) { String errorString; if (error.length() >= 2) { errorString = error.getString(1); } else { errorString = error.getString(0); } handler.post(() -> selectUserFlairListener.failed(errorString.substring(0, 1).toUpperCase() + errorString.substring(1))); } else { handler.post(selectUserFlairListener::success); } } else { handler.post(selectUserFlairListener::success); } } catch (JSONException e) { e.printStackTrace(); } }); } else { selectUserFlairListener.failed(response.message()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { selectUserFlairListener.failed(throwable.getMessage()); } }); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserDao.java ================================================ package ml.docilealligator.infinityforreddit.user; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; @Dao public interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(UserData userData); @Query("SELECT COUNT(*) FROM users") int getNUsers(); @Query("DELETE FROM users") void deleteAllUsers(); @Query("SELECT * FROM users WHERE name = :userName COLLATE NOCASE LIMIT 1") LiveData getUserLiveData(String userName); @Query("SELECT * FROM users WHERE name = :userName COLLATE NOCASE LIMIT 1") UserData getUserData(String userName); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserData.java ================================================ package ml.docilealligator.infinityforreddit.user; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; @Entity(tableName = "users") public class UserData { @PrimaryKey @NonNull @ColumnInfo(name = "name") private final String name; @ColumnInfo(name = "icon") private final String iconUrl; @ColumnInfo(name = "banner") private final String banner; @ColumnInfo(name = "link_karma") private final int linkKarma; @ColumnInfo(name = "comment_karma") private final int commentKarma; @ColumnInfo(name = "awarder_karma") private final int awarderKarma; @ColumnInfo(name = "awardee_karma") private final int awardeeKarma; @ColumnInfo(name = "total_karma") private final int totalKarma; @ColumnInfo(name = "created_utc") private final long cakeday; @ColumnInfo(name = "is_gold") private final boolean isGold; @ColumnInfo(name = "is_friend") private final boolean isFriend; @ColumnInfo(name = "can_be_followed") private final boolean canBeFollowed; @ColumnInfo(name = "over_18") private final boolean isNSFW; @ColumnInfo(name = "description") private final String description; @ColumnInfo(name = "title") private final String title; @Ignore private boolean isSelected; @Ignore private boolean isMod; public UserData(@NonNull String name, String iconUrl, String banner, int linkKarma, int commentKarma, int awarderKarma, int awardeeKarma, int totalKarma, long cakeday, boolean isGold, boolean isFriend, boolean canBeFollowed, boolean isNSFW, String description, String title, boolean isMod) { this.name = name; this.iconUrl = iconUrl; this.banner = banner; this.commentKarma = commentKarma; this.linkKarma = linkKarma; this.awarderKarma = awarderKarma; this.awardeeKarma = awardeeKarma; this.totalKarma = totalKarma; this.cakeday = cakeday; this.isGold = isGold; this.isFriend = isFriend; this.canBeFollowed = canBeFollowed; this.isNSFW = isNSFW; this.description = description; this.title = title; this.isSelected = false; this.isMod = isMod; } public UserData(@NonNull String name, String iconUrl, String banner, int linkKarma, int commentKarma, int awarderKarma, int awardeeKarma, int totalKarma, long cakeday, boolean isGold, boolean isFriend, boolean canBeFollowed, boolean isNSFW, String description, String title) { this.name = name; this.iconUrl = iconUrl; this.banner = banner; this.commentKarma = commentKarma; this.linkKarma = linkKarma; this.awarderKarma = awarderKarma; this.awardeeKarma = awardeeKarma; this.totalKarma = totalKarma; this.cakeday = cakeday; this.isGold = isGold; this.isFriend = isFriend; this.canBeFollowed = canBeFollowed; this.isNSFW = isNSFW; this.description = description; this.title = title; this.isSelected = false; this.isMod = false; } @NonNull public String getName() { return name; } public String getIconUrl() { return iconUrl; } public String getBanner() { return banner; } public int getLinkKarma() { return linkKarma; } public int getCommentKarma() { return commentKarma; } public int getAwarderKarma() { return awarderKarma; } public int getAwardeeKarma() { return awardeeKarma; } public int getTotalKarma() { return totalKarma; } public long getCakeday() { return cakeday; } public boolean isGold() { return isGold; } public boolean isFriend() { return isFriend; } public boolean isCanBeFollowed() { return canBeFollowed; } public boolean isNSFW() { return isNSFW; } public String getDescription() { return description; } public String getTitle() { return title; } public boolean isSelected() { return isSelected; } public void setSelected(boolean selected) { isSelected = selected; } public boolean isMod() { return isMod; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserFlair.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Parcel; import android.os.Parcelable; public class UserFlair implements Parcelable { private final String id; private String text; private final String htmlText; private final boolean editable; private final int maxEmojis; public UserFlair(String id, String text, String htmlText, boolean editable, int maxEmojis) { this.id = id; this.text = text; this.htmlText = htmlText; this.editable = editable; this.maxEmojis = maxEmojis; } protected UserFlair(Parcel in) { id = in.readString(); text = in.readString(); htmlText = in.readString(); editable = in.readByte() != 0; maxEmojis = in.readInt(); } public static final Creator CREATOR = new Creator() { @Override public UserFlair createFromParcel(Parcel in) { return new UserFlair(in); } @Override public UserFlair[] newArray(int size) { return new UserFlair[size]; } }; public String getId() { return id; } public String getText() { return text; } public void setText(String text) { this.text = text; } public String getHtmlText() { return htmlText; } public boolean isEditable() { return editable; } public int getMaxEmojis() { return maxEmojis; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(id); parcel.writeString(text); parcel.writeString(htmlText); parcel.writeByte((byte) (editable ? 1 : 0)); parcel.writeInt(maxEmojis); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserFollowing.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserDao; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; import ml.docilealligator.infinityforreddit.utils.APIUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Retrofit; public class UserFollowing { public static void followUser(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit retrofit, @Nullable String accessToken, String username, @NonNull String accountName, RedditDataRoomDatabase redditDataRoomDatabase, UserFollowingListener userFollowingListener) { userFollowing(executor, handler, oauthRetrofit, retrofit, accessToken, username, accountName, "sub", redditDataRoomDatabase.subscribedUserDao(), userFollowingListener); } public static void anonymousFollowUser(Executor executor, Handler handler, Retrofit retrofit, String username, RedditDataRoomDatabase redditDataRoomDatabase, UserFollowingListener userFollowingListener) { FetchUserData.fetchUserData(executor, handler, retrofit, username, new FetchUserData.FetchUserDataListener() { @Override public void onFetchUserDataSuccess(UserData userData, int inboxCount) { executor.execute(() -> { if (!redditDataRoomDatabase.accountDao().isAnonymousAccountInserted()) { redditDataRoomDatabase.accountDao().insert(Account.getAnonymousAccount()); } redditDataRoomDatabase.subscribedUserDao().insert(new SubscribedUserData(userData.getName(), userData.getIconUrl(), Account.ANONYMOUS_ACCOUNT, false)); handler.post(userFollowingListener::onUserFollowingSuccess); }); } @Override public void onFetchUserDataFailed() { userFollowingListener.onUserFollowingFail(); } }); } public static void unfollowUser(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit retrofit, @Nullable String accessToken, String username, @NonNull String accountName, RedditDataRoomDatabase redditDataRoomDatabase, UserFollowingListener userFollowingListener) { userFollowing(executor, handler, oauthRetrofit, retrofit, accessToken, username, accountName, "unsub", redditDataRoomDatabase.subscribedUserDao(), userFollowingListener); } public static void anonymousUnfollowUser(Executor executor, Handler handler, String username, RedditDataRoomDatabase redditDataRoomDatabase, UserFollowingListener userFollowingListener) { executor.execute(() -> { redditDataRoomDatabase.subscribedUserDao().deleteSubscribedUser(username, Account.ANONYMOUS_ACCOUNT); handler.post(userFollowingListener::onUserFollowingSuccess); }); } private static void userFollowing(Executor executor, Handler handler, Retrofit oauthRetrofit, Retrofit retrofit, @Nullable String accessToken, String username, @NonNull String accountName, String action, SubscribedUserDao subscribedUserDao, UserFollowingListener userFollowingListener) { RedditAPI api = oauthRetrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.ACTION_KEY, action); params.put(APIUtils.SR_NAME_KEY, "u_" + username); Call subredditSubscriptionCall = api.subredditSubscription(APIUtils.getOAuthHeader(accessToken), params); subredditSubscriptionCall.enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { if (response.isSuccessful()) { if (action.equals("sub")) { FetchUserData.fetchUserData(executor, handler, null, oauthRetrofit, retrofit, accessToken, username, new FetchUserData.FetchUserDataListener() { @Override public void onFetchUserDataSuccess(UserData userData, int inboxCount) { executor.execute(() -> { SubscribedUserData subscribedUserData = new SubscribedUserData(userData.getName(), userData.getIconUrl(), accountName, false); subscribedUserDao.insert(subscribedUserData); }); } @Override public void onFetchUserDataFailed() { } }); userFollowingListener.onUserFollowingSuccess(); } else { executor.execute(() -> { subscribedUserDao.deleteSubscribedUser(username, accountName); handler.post(userFollowingListener::onUserFollowingSuccess); }); } } else { userFollowingListener.onUserFollowingFail(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { userFollowingListener.onUserFollowingFail(); } }); } public interface UserFollowingListener { void onUserFollowingSuccess(); void onUserFollowingFail(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserListingDataSource.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; import androidx.paging.PageKeyedDataSource; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Retrofit; public class UserListingDataSource extends PageKeyedDataSource { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final String query; private final SortType sortType; private final boolean nsfw; private final MutableLiveData paginationNetworkStateLiveData; private final MutableLiveData initialLoadStateLiveData; private final MutableLiveData hasUserLiveData; private PageKeyedDataSource.LoadParams params; private PageKeyedDataSource.LoadCallback callback; UserListingDataSource(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, boolean nsfw) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.query = query; this.sortType = sortType; this.nsfw = nsfw; paginationNetworkStateLiveData = new MutableLiveData<>(); initialLoadStateLiveData = new MutableLiveData<>(); hasUserLiveData = new MutableLiveData<>(); } MutableLiveData getPaginationNetworkStateLiveData() { return paginationNetworkStateLiveData; } MutableLiveData getInitialLoadStateLiveData() { return initialLoadStateLiveData; } MutableLiveData hasUserLiveData() { return hasUserLiveData; } @Override public void loadInitial(@NonNull PageKeyedDataSource.LoadInitialParams params, @NonNull PageKeyedDataSource.LoadInitialCallback callback) { initialLoadStateLiveData.postValue(NetworkState.LOADING); FetchUserData.fetchUserListingData(executor, handler, retrofit, query, null, sortType.getType(), nsfw, new FetchUserData.FetchUserListingDataListener() { @Override public void onFetchUserListingDataSuccess(List UserData, String after) { hasUserLiveData.postValue(!UserData.isEmpty()); callback.onResult(UserData, null, after); initialLoadStateLiveData.postValue(NetworkState.LOADED); } @Override public void onFetchUserListingDataFailed() { initialLoadStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error retrieving ml.docilealligator.infinityforreddit.User list")); } }); } @Override public void loadBefore(@NonNull PageKeyedDataSource.LoadParams params, @NonNull PageKeyedDataSource.LoadCallback callback) { } @Override public void loadAfter(@NonNull PageKeyedDataSource.LoadParams params, @NonNull PageKeyedDataSource.LoadCallback callback) { this.params = params; this.callback = callback; if (params.key.equals("null") || params.key.isEmpty()) { return; } FetchUserData.fetchUserListingData(executor, handler, retrofit, query, params.key, sortType.getType(), nsfw, new FetchUserData.FetchUserListingDataListener() { @Override public void onFetchUserListingDataSuccess(List UserData, String after) { callback.onResult(UserData, after); paginationNetworkStateLiveData.postValue(NetworkState.LOADED); } @Override public void onFetchUserListingDataFailed() { paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error retrieving ml.docilealligator.infinityforreddit.User list")); } }); } void retryLoadingMore() { loadAfter(params, callback); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserListingDataSourceFactory.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; import androidx.paging.DataSource; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Retrofit; public class UserListingDataSourceFactory extends DataSource.Factory { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final String query; private SortType sortType; private final boolean nsfw; private UserListingDataSource userListingDataSource; private final MutableLiveData userListingDataSourceMutableLiveData; UserListingDataSourceFactory(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, boolean nsfw) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.query = query; this.sortType = sortType; this.nsfw = nsfw; userListingDataSourceMutableLiveData = new MutableLiveData<>(); } @NonNull @Override public DataSource create() { userListingDataSource = new UserListingDataSource(executor, handler, retrofit, query, sortType, nsfw); userListingDataSourceMutableLiveData.postValue(userListingDataSource); return userListingDataSource; } public MutableLiveData getUserListingDataSourceMutableLiveData() { return userListingDataSourceMutableLiveData; } UserListingDataSource getUserListingDataSource() { return userListingDataSource; } void changeSortType(SortType sortType) { this.sortType = sortType; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserListingViewModel.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.NetworkState; import ml.docilealligator.infinityforreddit.thing.SortType; import retrofit2.Retrofit; public class UserListingViewModel extends ViewModel { private final UserListingDataSourceFactory userListingDataSourceFactory; private final LiveData paginationNetworkState; private final LiveData initialLoadingState; private final LiveData hasUserLiveData; private final LiveData> users; private final MutableLiveData sortTypeLiveData; public UserListingViewModel(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, boolean nsfw) { userListingDataSourceFactory = new UserListingDataSourceFactory(executor, handler, retrofit, query, sortType, nsfw); initialLoadingState = Transformations.switchMap(userListingDataSourceFactory.getUserListingDataSourceMutableLiveData(), UserListingDataSource::getInitialLoadStateLiveData); paginationNetworkState = Transformations.switchMap(userListingDataSourceFactory.getUserListingDataSourceMutableLiveData(), UserListingDataSource::getPaginationNetworkStateLiveData); hasUserLiveData = Transformations.switchMap(userListingDataSourceFactory.getUserListingDataSourceMutableLiveData(), UserListingDataSource::hasUserLiveData); sortTypeLiveData = new MutableLiveData<>(sortType); PagedList.Config pagedListConfig = (new PagedList.Config.Builder()) .setEnablePlaceholders(false) .setPageSize(25) .build(); users = Transformations.switchMap(sortTypeLiveData, sort -> { userListingDataSourceFactory.changeSortType(sortTypeLiveData.getValue()); return (new LivePagedListBuilder(userListingDataSourceFactory, pagedListConfig)).build(); }); } public LiveData> getUsers() { return users; } public LiveData getPaginationNetworkState() { return paginationNetworkState; } public LiveData getInitialLoadingState() { return initialLoadingState; } public LiveData hasUser() { return hasUserLiveData; } public void refresh() { userListingDataSourceFactory.getUserListingDataSource().invalidate(); } public void retryLoadingMore() { userListingDataSourceFactory.getUserListingDataSource().retryLoadingMore(); } public void changeSortType(SortType sortType) { sortTypeLiveData.postValue(sortType); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Executor executor; private final Handler handler; private final Retrofit retrofit; private final String query; private final SortType sortType; private final boolean nsfw; public Factory(Executor executor, Handler handler, Retrofit retrofit, String query, SortType sortType, boolean nsfw) { this.executor = executor; this.handler = handler; this.retrofit = retrofit; this.query = query; this.sortType = sortType; this.nsfw = nsfw; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new UserListingViewModel(executor, handler, retrofit, query, sortType, nsfw); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserProfileImagesBatchLoader.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.viewmodels.ViewPostDetailActivityViewModel; import retrofit2.Response; import retrofit2.Retrofit; public class UserProfileImagesBatchLoader { public static final int BATCH_SIZE = 100; private final Executor mExecutor; private final Handler mHandler; private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final Retrofit mRetrofit; private final Map mAuthorFullNameToImageMap; private final Queue mCommentQueue; private final Map mAuthorFullNameToListenerMap; private final List mCallingComments; private final Set mLoadingAuthorFullNames; private final Object mImageMapLock = new Object(); private final Object mCommentQueueLock = new Object(); private final Object mListenerMapLock = new Object(); private final Object mCallingCommentsLock = new Object(); private final Object mLoadingSetLock = new Object(); private boolean mIsLoadingBatch = false; public UserProfileImagesBatchLoader(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, Retrofit retrofit) { mExecutor = executor; mHandler = handler; mRedditDataRoomDatabase = redditDataRoomDatabase; mRetrofit = retrofit; mAuthorFullNameToImageMap = new HashMap<>(); mCommentQueue = new LinkedList<>(); mAuthorFullNameToListenerMap = new HashMap<>(); mCallingComments = new LinkedList<>(); mLoadingAuthorFullNames = new HashSet<>(); } public void loadAuthorImages(List comments, @NonNull ViewPostDetailActivityViewModel.LoadIconListener loadIconListener) { String authorFullName = comments.get(0).getAuthorFullName(); synchronized (mImageMapLock) { if (mAuthorFullNameToImageMap.containsKey(authorFullName)) { loadIconListener.loadIconSuccess(authorFullName, mAuthorFullNameToImageMap.get(authorFullName)); return; } } synchronized (mListenerMapLock) { mAuthorFullNameToListenerMap.put(authorFullName, loadIconListener); } synchronized (mCommentQueueLock) { mCommentQueue.addAll(comments); } synchronized (mCallingCommentsLock) { mCallingComments.add(comments.get(0)); } if (!mIsLoadingBatch) { loadNextBatch(); } } private void loadNextBatch() { synchronized (mCommentQueueLock) { if (mCommentQueue.isEmpty()) { return; } } mIsLoadingBatch = true; mExecutor.execute(() -> { synchronized (mCallingCommentsLock) { Iterator iterator = mCallingComments.iterator(); while (iterator.hasNext()) { Comment c = iterator.next(); String authorFullName = c.getAuthorFullName(); ViewPostDetailActivityViewModel.LoadIconListener loadIconListener; synchronized (mListenerMapLock) { loadIconListener = mAuthorFullNameToListenerMap.get(authorFullName); } if (loadIconListener != null) { synchronized (mImageMapLock) { if (mAuthorFullNameToImageMap.containsKey(authorFullName)) { String url = mAuthorFullNameToImageMap.get(authorFullName); mHandler.post(() -> loadIconListener.loadIconSuccess(authorFullName, url)); iterator.remove(); continue; } } UserData userData = mRedditDataRoomDatabase.userDao().getUserData(c.getAuthor()); if (userData != null) { String iconImageUrl = userData.getIconUrl(); synchronized (mImageMapLock) { mAuthorFullNameToImageMap.put(authorFullName, iconImageUrl); } mHandler.post(() -> loadIconListener.loadIconSuccess(authorFullName, iconImageUrl)); synchronized (mListenerMapLock) { mAuthorFullNameToListenerMap.remove(authorFullName); } iterator.remove(); } } else { iterator.remove(); } } } StringBuilder stringBuilder = new StringBuilder(); synchronized (mCommentQueueLock) { for (int i = 0; i < BATCH_SIZE && !mCommentQueue.isEmpty(); i++) { Comment comment = mCommentQueue.poll(); if (comment == null) { continue; } String authorFullName = comment.getAuthorFullName(); boolean alreadyCached; synchronized (mImageMapLock) { alreadyCached = mAuthorFullNameToImageMap.containsKey(authorFullName); } if (!alreadyCached) { stringBuilder.append(authorFullName).append(","); synchronized (mLoadingSetLock) { mLoadingAuthorFullNames.add(authorFullName); } } else if (i == 0) { ViewPostDetailActivityViewModel.LoadIconListener loadIconListener; synchronized (mListenerMapLock) { loadIconListener = mAuthorFullNameToListenerMap.get(authorFullName); } if (loadIconListener != null) { String url; synchronized (mImageMapLock) { url = mAuthorFullNameToImageMap.get(authorFullName); } mHandler.post(() -> { loadIconListener.loadIconSuccess(authorFullName, url); }); synchronized (mListenerMapLock) { mAuthorFullNameToListenerMap.remove(authorFullName); } } for (int j = 0; j < BATCH_SIZE - 1 && !mCommentQueue.isEmpty(); j++) { mCommentQueue.poll(); } break; } } } if (stringBuilder.length() > 0) { stringBuilder.deleteCharAt(stringBuilder.length() - 1); try { Response response = mRetrofit.create(RedditAPI.class).loadPartialUserData(stringBuilder.toString()).execute(); if (response.isSuccessful()) { parseUserProfileImages(response.body()); callListenerAndLoadNextBatch(true); } else { callListenerAndLoadNextBatch(false); } } catch (IOException e) { e.printStackTrace(); callListenerAndLoadNextBatch(false); } } else { mIsLoadingBatch = false; loadNextBatch(); } }); } @WorkerThread private void parseUserProfileImages(String response) { try { JSONObject jsonResponse = new JSONObject(response); synchronized (mLoadingSetLock) { for (String s : mLoadingAuthorFullNames) { try { String imageUrl = jsonResponse.getJSONObject(s).getString(JSONUtils.PROFILE_IMG_KEY).replaceAll("&","&"); synchronized (mImageMapLock) { mAuthorFullNameToImageMap.put(s, imageUrl); } } catch (JSONException e) { e.printStackTrace(); } } } } catch (JSONException e) { e.printStackTrace(); } } private void callListenerAndLoadNextBatch(boolean loadSuccessful) { synchronized (mLoadingSetLock) { for (String s : mLoadingAuthorFullNames) { ViewPostDetailActivityViewModel.LoadIconListener loadIconListener; synchronized (mListenerMapLock) { loadIconListener = mAuthorFullNameToListenerMap.get(s); } if (loadIconListener != null) { String imageUrl; synchronized (mImageMapLock) { imageUrl = mAuthorFullNameToImageMap.get(s); } mHandler.post(() -> { loadIconListener.loadIconSuccess(s, imageUrl); }); synchronized (mListenerMapLock) { mAuthorFullNameToListenerMap.remove(s); } } if (!loadSuccessful) { synchronized (mImageMapLock) { if (!mAuthorFullNameToImageMap.containsKey(s)) { mAuthorFullNameToImageMap.put(s, null); } } } } mLoadingAuthorFullNames.clear(); } mIsLoadingBatch = false; loadNextBatch(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserRepository.java ================================================ package ml.docilealligator.infinityforreddit.user; import androidx.lifecycle.LiveData; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class UserRepository { private final LiveData mUserLiveData; UserRepository(RedditDataRoomDatabase redditDataRoomDatabase, String userName) { mUserLiveData = redditDataRoomDatabase.userDao().getUserLiveData(userName); } LiveData getUserLiveData() { return mUserLiveData; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserViewModel.java ================================================ package ml.docilealligator.infinityforreddit.user; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; public class UserViewModel extends ViewModel { private final UserRepository mSubredditRepository; private final LiveData mUserLiveData; public UserViewModel(RedditDataRoomDatabase redditDataRoomDatabase, String id) { mSubredditRepository = new UserRepository(redditDataRoomDatabase, id); mUserLiveData = mSubredditRepository.getUserLiveData(); } public LiveData getUserLiveData() { return mUserLiveData; } public static class Factory extends ViewModelProvider.NewInstanceFactory { private final RedditDataRoomDatabase mRedditDataRoomDatabase; private final String mUsername; public Factory(RedditDataRoomDatabase redditDataRoomDatabase, String username) { mRedditDataRoomDatabase = redditDataRoomDatabase; mUsername = username; } @Override public T create(Class modelClass) { //noinspection unchecked return (T) new UserViewModel(mRedditDataRoomDatabase, mUsername); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/user/UserWithSelection.java ================================================ package ml.docilealligator.infinityforreddit.user; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; import ml.docilealligator.infinityforreddit.subscribeduser.SubscribedUserData; public class UserWithSelection implements Parcelable { private final String name; private final String iconUrl; private boolean selected; public UserWithSelection(String name, String iconUrl) { this.name = name; this.iconUrl = iconUrl; selected = false; } protected UserWithSelection(Parcel in) { name = in.readString(); iconUrl = in.readString(); selected = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public UserWithSelection createFromParcel(Parcel in) { return new UserWithSelection(in); } @Override public UserWithSelection[] newArray(int size) { return new UserWithSelection[size]; } }; public String getName() { return name; } public String getIconUrl() { return iconUrl; } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { this.selected = selected; } public static ArrayList convertSubscribedUsers( List subscribedUserData) { ArrayList userWithSelections = new ArrayList<>(); for (SubscribedUserData s : subscribedUserData) { userWithSelections.add(new UserWithSelection(s.getName(), s.getIconUrl())); } return userWithSelections; } public static UserWithSelection convertUser(UserData user) { return new UserWithSelection(user.getName(), user.getIconUrl()); } public int compareName(UserWithSelection userWithSelection) { if (userWithSelection != null) { return name.compareToIgnoreCase(userWithSelection.getName()); } else { return -1; } } @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof UserWithSelection)) { return false; } else { return this.getName().compareToIgnoreCase(((UserWithSelection) obj).getName()) == 0; } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(name); parcel.writeString(iconUrl); parcel.writeByte((byte) (selected ? 1 : 0)); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/APIUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.content.Context; import android.content.SharedPreferences; import android.os.SystemClock; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.account.Account; import okhttp3.MediaType; import okhttp3.RequestBody; /** * Created by alex on 2/23/18. */ public class APIUtils { public static final String OAUTH_URL = "https://www.reddit.com/api/v1/authorize.compact"; public static final String OAUTH_API_BASE_URI = "https://oauth.reddit.com"; public static final String API_BASE_URI = "https://www.reddit.com"; public static final String API_UPLOAD_MEDIA_URI = "https://reddit-uploaded-media.s3-accelerate.amazonaws.com"; public static final String API_UPLOAD_VIDEO_URI = "https://reddit-uploaded-video.s3-accelerate.amazonaws.com"; public static final String REDGIFS_API_BASE_URI = "https://api.redgifs.com"; public static final String OH_MY_DL_BASE_URI = "https://ohmydl.com"; public static final String IMGUR_API_BASE_URI = "https://api.imgur.com/3/"; public static final String STREAMABLE_API_BASE_URI = "https://api.streamable.com"; public static final String SERVER_API_BASE_URI = "http://127.0.0.1"; public static final String CLIENT_ID_KEY = "client_id"; public static final String CLIENT_SECRET_KEY = "client_secret"; public static final String IMGUR_CLIENT_ID = "Client-ID cc671794e0ab397"; public static final String REDGIFS_CLIENT_ID = "1828d0bcc93-15ac-bde6-0005-d2ecbe8daab3"; public static final String REDGIFS_CLIENT_SECRET = "TJBlw7jRXW65NAGgFBtgZHu97WlzRXHYybK81sZ9dLM="; public static final String RESPONSE_TYPE_KEY = "response_type"; public static final String RESPONSE_TYPE = "code"; public static final String STATE_KEY = "state"; public static final String STATE = "23ro8xlxvzp4asqd"; public static final String REDIRECT_URI_KEY = "redirect_uri"; public static final String DEFAULT_REDIRECT_URI = "continuum://localhost"; public static String REDIRECT_URI = DEFAULT_REDIRECT_URI; public static final String DURATION_KEY = "duration"; public static final String DURATION = "permanent"; public static final String SCOPE_KEY = "scope"; public static final String[] SCOPE_LIST = { "account", "creddits", "edit", "flair", "history", "identity", "livemanage", "modconfig", "modcontributors", "modflair", "modlog", "modmail", "modothers", "modposts", "modwiki", "modself", "mysubreddits", "privatemessages", "read", "report", "save", "submit", "subscribe", "vote", "wikiedit", "wikiread" }; public static final String SCOPE = String.join(" ", SCOPE_LIST); public static final String ACCESS_TOKEN_KEY = "access_token"; public static final String AUTHORIZATION_KEY = "Authorization"; public static final String AUTHORIZATION_BASE = "bearer "; public static final String USER_AGENT_KEY = "User-Agent"; public static final String DEFAULT_USER_AGENT = "android:org.cygnusx1.continuum:" + BuildConfig.VERSION_NAME + " (by /u/edgan)"; public static String USER_AGENT = DEFAULT_USER_AGENT; public static String ANONYMOUS_USER_AGENT = DEFAULT_USER_AGENT; public static final String USERNAME_KEY = "username"; public static final String GRANT_TYPE_KEY = "grant_type"; public static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"; public static final String REFRESH_TOKEN_KEY = "refresh_token"; public static final String DIR_KEY = "dir"; public static final String ID_KEY = "id"; public static final String RANK_KEY = "rank"; public static final String DIR_UPVOTE = "1"; public static final String DIR_UNVOTE = "0"; public static final String DIR_DOWNVOTE = "-1"; public static final String RANK = "10"; public static final String ACTION_KEY = "action"; public static final String SR_NAME_KEY = "sr_name"; public static final String API_TYPE_KEY = "api_type"; public static final String API_TYPE_JSON = "json"; public static final String RETURN_RTJSON_KEY = "return_rtjson"; public static final String TEXT_KEY = "text"; public static final String URL_KEY = "url"; public static final String VIDEO_POSTER_URL_KEY = "video_poster_url"; public static final String THING_ID_KEY = "thing_id"; public static final String SR_KEY = "sr"; public static final String TITLE_KEY = "title"; public static final String FLAIR_TEXT_KEY = "flair_text"; public static final String SPOILER_KEY = "spoiler"; public static final String NSFW_KEY = "nsfw"; public static final String CROSSPOST_FULLNAME_KEY = "crosspost_fullname"; public static final String SEND_REPLIES_KEY = "sendreplies"; public static final String KIND_KEY = "kind"; public static final String KIND_SELF = "self"; public static final String KIND_LINK = "link"; public static final String KIND_IMAGE = "image"; public static final String KIND_VIDEO = "video"; public static final String KIND_VIDEOGIF = "videogif"; public static final String KIND_CROSSPOST = "crosspost"; public static final String RICHTEXT_JSON_KEY = "richtext_json"; public static final String FILEPATH_KEY = "filepath"; public static final String MIMETYPE_KEY = "mimetype"; public static final String LINK_KEY = "link"; public static final String FLAIR_TEMPLATE_ID_KEY = "flair_template_id"; public static final String FLAIR_ID_KEY = "flair_id"; public static final String MAKE_FAVORITE_KEY = "make_favorite"; public static final String MULTIPATH_KEY = "multipath"; public static final String MODEL_KEY = "model"; public static final String FROM_KEY = "from"; public static final String DISPLAY_NAME_KEY = "display_name"; public static final String DESCRIPTION_MD_KEY = "description_md"; public static final String REASON_KEY = "reason"; public static final String SUBJECT_KEY = "subject"; public static final String TO_KEY = "to"; public static final String NAME_KEY = "name"; public static final String ORIGIN_KEY = "Origin"; public static final String REVEDDIT_ORIGIN = "https://www.reveddit.com"; public static final String REFERER_KEY = "Referer"; public static final String REVEDDIT_REFERER = "https://www.reveddit.com/"; // Method to retrieve Client ID from SharedPreferences public static String getClientId(Context context) { // Explicitly get SharedPreferences by file name to ensure consistency with the PreferenceFragment SharedPreferences sharedPreferences = context.getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); return sharedPreferences.getString(SharedPreferencesUtils.CLIENT_ID_PREF_KEY, context.getString(R.string.default_client_id)); } // Method to retrieve User Agent from SharedPreferences public static String getUserAgent(Context context) { SharedPreferences sharedPreferences = context.getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); return sharedPreferences.getString(SharedPreferencesUtils.USER_AGENT_PREF_KEY, DEFAULT_USER_AGENT); } // Method to retrieve Redirect URI from SharedPreferences public static String getRedirectUri(Context context) { SharedPreferences sharedPreferences = context.getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); return sharedPreferences.getString(SharedPreferencesUtils.REDIRECT_URI_PREF_KEY, context.getString(R.string.default_redirect_uri)); } // Initialize mutable configurable fields from SharedPreferences at app startup public static void initConfigurableFields(Context context) { USER_AGENT = getUserAgent(context); ANONYMOUS_USER_AGENT = USER_AGENT; REDIRECT_URI = getRedirectUri(context); } // Method to retrieve Giphy API Key from SharedPreferences public static String getGiphyApiKey(Context context) { // Explicitly get SharedPreferences by file name to ensure consistency SharedPreferences sharedPreferences = context.getSharedPreferences(SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE, Context.MODE_PRIVATE); String customKey = sharedPreferences.getString(SharedPreferencesUtils.GIPHY_API_KEY_PREF_KEY, null); // Return custom key if set and not empty, otherwise return the default from strings.xml if (customKey != null && !customKey.isEmpty()) { return customKey; } else { return context.getString(R.string.default_giphy_api_key); } } public static final String SPAM_KEY = "spam"; public static final String HOW_KEY = "how"; public static final String HOW_YES = "yes"; public static final String HOW_NO = "no"; public static final String PLATFORM_KEY = "platform"; public static Map getHttpBasicAuthHeader(Context context) { // Ensure we use the application context to avoid potential lifecycle issues with the passed context Context appContext = context.getApplicationContext(); Map params = new HashMap<>(); String clientId = getClientId(appContext); String credentials = String.format("%s:%s", clientId, ""); String auth = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP); params.put(APIUtils.AUTHORIZATION_KEY, auth); return params; } public static Map getOAuthHeader(String accessToken) { Map params = new HashMap<>(); params.put(APIUtils.AUTHORIZATION_KEY, APIUtils.AUTHORIZATION_BASE + accessToken); params.put(APIUtils.USER_AGENT_KEY, APIUtils.USER_AGENT); return params; } public static Map getServerHeader(String serverAccessToken, String accountName, boolean anonymous) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT) || anonymous) { return new HashMap<>(); } Map params = new HashMap<>(); params.put(APIUtils.AUTHORIZATION_KEY, APIUtils.AUTHORIZATION_BASE + serverAccessToken); params.put(APIUtils.USERNAME_KEY, accountName); return params; } public static Map getRedgifsOAuthHeader(String redgifsAccessToken) { Map params = new HashMap<>(); params.put(APIUtils.AUTHORIZATION_KEY, APIUtils.AUTHORIZATION_BASE + redgifsAccessToken); return params; } public static RequestBody getRequestBody(String s) { return RequestBody.create(s, MediaType.parse("text/plain")); } public static Map getRevedditHeader() { Map params = new HashMap<>(); params.put(APIUtils.ORIGIN_KEY, APIUtils.REVEDDIT_ORIGIN); params.put(APIUtils.REFERER_KEY, APIUtils.REVEDDIT_REFERER); params.put(APIUtils.USER_AGENT_KEY, APIUtils.USER_AGENT); return params; } // Concatenated subreddit name works too public static int subredditAPICallLimit(@Nullable String subredditName) { return 100; } // RedGifs token management public static final AtomicReference REDGIFS_TOKEN = new AtomicReference<>(new RedgifsAuthToken("", 0)); public static class RedgifsAuthToken { @NonNull public final String token; private final long expireAt; private RedgifsAuthToken(@NonNull String token, final long expireAt) { this.token = token; this.expireAt = expireAt; } public static RedgifsAuthToken expireIn1day(@NonNull String token) { // 23 not 24 to give an hour leeway long expireTime = 1000 * 60 * 60 * 23; return new RedgifsAuthToken(token, SystemClock.uptimeMillis() + expireTime); } public boolean isValid() { return !token.isEmpty() && expireAt > SystemClock.uptimeMillis(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/AppRestartHelper.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.Toast; public class AppRestartHelper { private static final String TAG = "AppRestartHelper"; /** * Restarts the application by clearing the task stack and launching the main activity. * Also kills the current process to ensure a clean restart. * Shows a toast message as a fallback if restarting via Intent fails. * * @param context Context used to get application context, package manager, and show toasts. */ public static void triggerAppRestart(Context context) { try { Context appContext = context.getApplicationContext(); // Use application context for getPackageManager and getPackageName Intent intent = appContext.getPackageManager().getLaunchIntentForPackage(appContext.getPackageName()); if (intent != null) { // Clear the activity stack and start the launch activity as a new task. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); appContext.startActivity(intent); Log.i(TAG, "Triggering app restart via Intent."); // Request the current process be killed. System will eventually restart it. // This ensures that services and other components are also restarted. android.os.Process.killProcess(android.os.Process.myPid()); // System.exit(0); // Alternative to killProcess, might be slightly cleaner in some cases } else { Log.e(TAG, "Could not get launch intent for package to trigger restart."); // Use application context for Toast as well Toast.makeText(appContext, "Please restart the app manually.", Toast.LENGTH_LONG).show(); } } catch (Exception e) { Log.e(TAG, "Error triggering app restart", e); // Use application context for Toast Toast.makeText(context.getApplicationContext(), "Please restart the app manually.", Toast.LENGTH_LONG).show(); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/ColorUtils.kt ================================================ package ml.docilealligator.infinityforreddit.utils import android.graphics.Color import androidx.core.graphics.ColorUtils fun deriveContrastingColor(originalColor: Int): Int { val blendedColor = if (ColorUtils.calculateLuminance(originalColor) < 0.5) Color.WHITE else Color.BLACK val originalAlpha = Color.alpha(originalColor) val opaqueThumbColor = ColorUtils.setAlphaComponent(originalColor, 255) val opaqueNewColor = ColorUtils.blendARGB(opaqueThumbColor, blendedColor, 0.6f) return ColorUtils.setAlphaComponent(opaqueNewColor, originalAlpha) } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/CommentScrollPositionCache.java ================================================ package ml.docilealligator.infinityforreddit.utils; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.comment.Comment; /** * In-memory cache for storing comment data and scroll positions per post. * Data is not persisted to disk and is cleared when the app is restarted. */ public class CommentScrollPositionCache { private static final CommentScrollPositionCache INSTANCE = new CommentScrollPositionCache(); private final Map cache = new HashMap<>(); private CommentScrollPositionCache() { // Private constructor for singleton } public static CommentScrollPositionCache getInstance() { return INSTANCE; } /** * Saves the comment data and scroll position for a post. * @param postId The post ID * @param comments The list of comments * @param children The list of child IDs for loading more * @param hasMoreChildren Whether there are more children to load * @param scrollPosition The scroll position (first visible item position) */ public void save(String postId, ArrayList comments, ArrayList children, boolean hasMoreChildren, int scrollPosition) { if (postId != null && comments != null) { cache.put(postId, new CachedPostComments(comments, children, hasMoreChildren, scrollPosition)); } } /** * Gets the cached data for a post. * @param postId The post ID * @return The cached data, or null if not found */ public CachedPostComments get(String postId) { if (postId != null) { return cache.get(postId); } return null; } /** * Checks if there is cached data for a post. * @param postId The post ID * @return true if cached data exists */ public boolean has(String postId) { return postId != null && cache.containsKey(postId); } /** * Removes the cached data for a post. * @param postId The post ID */ public void remove(String postId) { if (postId != null) { cache.remove(postId); } } /** * Clears all cached data. */ public void clearAll() { cache.clear(); } /** * Holder class for cached post comment data. */ public static class CachedPostComments { public final ArrayList comments; public final ArrayList children; public final boolean hasMoreChildren; public final int scrollPosition; public CachedPostComments(ArrayList comments, ArrayList children, boolean hasMoreChildren, int scrollPosition) { this.comments = comments; this.children = children; this.hasMoreChildren = hasMoreChildren; this.scrollPosition = scrollPosition; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/CustomThemeSharedPreferencesUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.content.SharedPreferences; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; public class CustomThemeSharedPreferencesUtils { public static final int LIGHT = 0; public static final int DARK = 1; public static final int AMOLED = 2; public static final String LIGHT_THEME_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.light_theme"; public static final String DARK_THEME_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.dark_theme"; public static final String AMOLED_THEME_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.amoled_theme"; public static final String LIGHT_STATUS_BAR = "lightStatusBar"; public static final String LIGHT_NAV_BAR = "lightNavBar"; public static final String CHANGE_STATUS_BAR_ICON_COLOR_AFTER_TOOLBAR_COLLAPSED_IN_IMMERSIVE_INTERFACE = "changeStatusBarIconColorImmersive"; public static final String COLOR_PRIMARY = "colorPrimary"; public static final String COLOR_PRIMARY_DARK = "colorPrimaryDark"; public static final String COLOR_ACCENT = "colorAccent"; public static final String COLOR_PRIMARY_LIGHT_THEME = "colorPrimaryLightTheme"; public static final String POST_TITLE_COLOR = "postTitleColor"; public static final String POST_CONTENT_COLOR = "postContentColor"; public static final String READ_POST_TITLE_COLOR = "readPostTitleColor"; public static final String READ_POST_CONTENT_COLOR = "readPostContentColor"; public static final String COMMENT_COLOR = "commentColor"; public static final String PRIMARY_TEXT_COLOR = "primaryTextColor"; public static final String SECONDARY_TEXT_COLOR = "secondaryTextColor"; public static final String BUTTON_TEXT_COLOR = "buttonTextColor"; public static final String BACKGROUND_COLOR = "backgroundColor"; public static final String CARD_VIEW_BACKGROUND_COLOR = "cardViewBackgroundColor"; public static final String READ_POST_CARD_VIEW_BACKGROUND_COLOR = "readPostCardViewBackgroundColor"; public static final String FILLED_CARD_VIEW_BACKGROUND_COLOR = "filledCardViewBackgroundColor"; public static final String READ_POST_FILLED_CARD_VIEW_BACKGROUND_COLOR = "readPostFilledCardViewBackgroundColor"; public static final String COMMENT_BACKGROUND_COLOR = "commentBackgroundColor"; public static final String BOTTOM_APP_BAR_BACKGROUND_COLOR = "bottomAppBarBackgroundColor"; public static final String PRIMARY_ICON_COLOR = "primaryIconColor"; public static final String BOTTOM_APP_BAR_ICON_COLOR = "bottomAppBarIconColor"; public static final String POST_ICON_AND_INFO_COLOR = "postIconAndInfoColor"; public static final String COMMENT_ICON_AND_INFO_COLOR = "commentIconAndInfoColor"; public static final String TOOLBAR_PRIMARY_TEXT_AND_ICON_COLOR = "toolbarPrimaryTextAndIconColor"; public static final String TOOLBAR_SECONDARY_TEXT_COLOR = "toolbarSecondaryTextColor"; public static final String CIRCULAR_PROGRESS_BAR_BACKGROUND = "circularProgressBarBackground"; public static final String MEDIA_INDICATOR_ICON_COLOR = "mediaIndicatorIconColor"; public static final String MEDIA_INDICATOR_BACKGROUND_COLOR = "mediaIndicatorBackgroundColor"; public static final String TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TAB_BACKGROUND = "tabLayoutWithExpandedCollapsingToolbarTabBackground"; public static final String TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TEXT_COLOR = "tabLayoutWithExpandedCollapsingToolbarTextColor"; public static final String TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TAB_INDICATOR = "tabLayoutWithExpandedCollapsingToolbarTabIndicator"; public static final String TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TAB_BACKGROUND = "tabLayoutWithCollapsedCollapsingToolbarTabBackground"; public static final String TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TEXT_COLOR = "tabLayoutWithCollapsedCollapsingToolbarTextColor"; public static final String TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TAB_INDICATOR = "tabLayoutWithCollapsedCollapsingToolbarTabIndicator"; public static final String NAV_BAR_COLOR = "navBarColor"; public static final String UPVOTED = "upvoted"; public static final String DOWNVOTED = "downvoted"; public static final String POST_TYPE_BACKGROUND_COLOR = "postTypeBackgroundColor"; public static final String POST_TYPE_TEXT_COLOR = "postTypeTextColor"; public static final String SPOILER_BACKGROUND_COLOR = "spoilerBackgroundColor"; public static final String SPOILER_TEXT_COLOR = "spoilerTextColor"; public static final String NSFW_BACKGROUND_COLOR = "nsfwBackgroundColor"; public static final String NSFW_TEXT_COLOR = "nsfwTextColor"; public static final String FLAIR_BACKGROUND_COLOR = "flairBackgroundColor"; public static final String FLAIR_TEXT_COLOR = "flairTextColor"; private static final String AWARDS_BACKGROUND_COLOR = "awardsBackgroundColor"; private static final String AWARDS_TEXT_COLOR = "awardsTextColor"; public static final String ARCHIVED_ICON_TINT = "archivedIconTint"; public static final String LOCKED_ICON_TINT = "lockedIconTint"; public static final String CROSSPOST_ICON_TINT = "crosspostIconTint"; public static final String UPVOTE_RATIO_ICON_TINT = "upvoteRatioIconTint"; public static final String STICKIED_POST_ICON_TINT = "stickiedPost"; public static final String NO_PREVIEW_POST_TYPE_ICON_TINT = "noPreviewPostTypeIconTint"; public static final String SUBSCRIBED = "subscribed"; public static final String UNSUBSCRIBED = "unsubscribed"; public static final String USERNAME = "username"; public static final String SUBREDDIT = "subreddit"; public static final String AUTHOR_FLAIR_TEXT_COLOR = "authorFlairTextColor"; public static final String SUBMITTER = "submitter"; public static final String MODERATOR = "moderator"; public static final String CURRENT_USER = "currentUser"; public static final String SINGLE_COMMENT_THREAD_BACKGROUND_COLOR = "singleCommentThreadBackgroundColor"; public static final String UNREAD_MESSAGE_BACKGROUND_COLOR = "unreadMessageBackgroundColor"; public static final String DIVIDER_COLOR = "dividerColor"; public static final String NO_PREVIEW_POST_TYPE_BACKGROUND_COLOR = "noPreviewLinkBackgroundColor"; public static final String VOTE_AND_REPLY_UNAVAILABLE_BUTTON_COLOR = "voteAndReplyUnavailableButtonColor"; public static final String COMMENT_VERTICAL_BAR_COLOR_1 = "commentVerticalBarColor1"; public static final String COMMENT_VERTICAL_BAR_COLOR_2 = "commentVerticalBarColor2"; public static final String COMMENT_VERTICAL_BAR_COLOR_3 = "commentVerticalBarColor3"; public static final String COMMENT_VERTICAL_BAR_COLOR_4 = "commentVerticalBarColor4"; public static final String COMMENT_VERTICAL_BAR_COLOR_5 = "commentVerticalBarColor5"; public static final String COMMENT_VERTICAL_BAR_COLOR_6 = "commentVerticalBarColor6"; public static final String COMMENT_VERTICAL_BAR_COLOR_7 = "commentVerticalBarColor7"; public static final String FAB_ICON_COLOR = "fabIconColor"; public static final String CHIP_TEXT_COLOR = "chipTextColor"; public static final String LINK_COLOR = "linkColor"; public static final String RECEIVED_MESSAGE_TEXT_COLOR = "receivedMessageTextColor"; public static final String SENT_MESSAGE_TEXT_COLOR = "sentMessageTextColor"; public static final String RECEIVED_MESSAGE_BACKROUND_COLOR = "receivedMessageBackgroundColor"; public static final String SENT_MESSAGE_BACKGROUND_COLOR = "sentMessageBackgroundColor"; public static final String SEND_MESSAGE_ICON_COLOR = "sentMessageIconColor"; public static final String FULLY_COLLAPSED_COMMENT_BACKGROUND_COLOR = "fullyCollapsedCommentBackgroundColor"; private static final String AWARDED_COMMENT_BACKGROUND_COLOR = "awardedCommentBackgroundColor"; public static void insertThemeToSharedPreferences(CustomTheme customTheme, SharedPreferences themeSharedPreferences) { SharedPreferences.Editor editor = themeSharedPreferences.edit(); editor.putInt(COLOR_PRIMARY, customTheme.colorPrimary); editor.putInt(COLOR_PRIMARY_DARK, customTheme.colorPrimaryDark); editor.putInt(COLOR_ACCENT, customTheme.colorAccent); editor.putInt(COLOR_PRIMARY_LIGHT_THEME, customTheme.colorPrimaryLightTheme); editor.putInt(PRIMARY_TEXT_COLOR, customTheme.primaryTextColor); editor.putInt(SECONDARY_TEXT_COLOR, customTheme.secondaryTextColor); editor.putInt(POST_TITLE_COLOR, customTheme.postTitleColor); editor.putInt(POST_CONTENT_COLOR, customTheme.postContentColor); editor.putInt(READ_POST_TITLE_COLOR, customTheme.readPostTitleColor); editor.putInt(READ_POST_CONTENT_COLOR, customTheme.readPostContentColor); editor.putInt(COMMENT_COLOR, customTheme.commentColor); editor.putInt(BUTTON_TEXT_COLOR, customTheme.buttonTextColor); editor.putInt(BACKGROUND_COLOR, customTheme.backgroundColor); editor.putInt(CARD_VIEW_BACKGROUND_COLOR, customTheme.cardViewBackgroundColor); editor.putInt(READ_POST_CARD_VIEW_BACKGROUND_COLOR, customTheme.readPostCardViewBackgroundColor); editor.putInt(FILLED_CARD_VIEW_BACKGROUND_COLOR, customTheme.filledCardViewBackgroundColor); editor.putInt(READ_POST_FILLED_CARD_VIEW_BACKGROUND_COLOR, customTheme.readPostFilledCardViewBackgroundColor); editor.putInt(COMMENT_BACKGROUND_COLOR, customTheme.commentBackgroundColor); editor.putInt(BOTTOM_APP_BAR_BACKGROUND_COLOR, customTheme.bottomAppBarBackgroundColor); editor.putInt(PRIMARY_ICON_COLOR, customTheme.primaryIconColor); editor.putInt(BOTTOM_APP_BAR_ICON_COLOR, customTheme.bottomAppBarIconColor); editor.putInt(POST_ICON_AND_INFO_COLOR, customTheme.postIconAndInfoColor); editor.putInt(COMMENT_ICON_AND_INFO_COLOR, customTheme.commentIconAndInfoColor); editor.putInt(TOOLBAR_PRIMARY_TEXT_AND_ICON_COLOR, customTheme.toolbarPrimaryTextAndIconColor); editor.putInt(TOOLBAR_SECONDARY_TEXT_COLOR, customTheme.toolbarSecondaryTextColor); editor.putInt(CIRCULAR_PROGRESS_BAR_BACKGROUND, customTheme.circularProgressBarBackground); editor.putInt(MEDIA_INDICATOR_ICON_COLOR, customTheme.mediaIndicatorIconColor); editor.putInt(MEDIA_INDICATOR_BACKGROUND_COLOR, customTheme.mediaIndicatorBackgroundColor); editor.putInt(TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TAB_BACKGROUND, customTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground); editor.putInt(TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TEXT_COLOR, customTheme.tabLayoutWithExpandedCollapsingToolbarTextColor); editor.putInt(TAB_LAYOUT_WITH_EXPANDED_COLLAPSING_TOOLBAR_TAB_INDICATOR, customTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator); editor.putInt(TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TAB_BACKGROUND, customTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground); editor.putInt(TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TEXT_COLOR, customTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor); editor.putInt(TAB_LAYOUT_WITH_COLLAPSED_COLLAPSING_TOOLBAR_TAB_INDICATOR, customTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator); editor.putInt(NAV_BAR_COLOR, customTheme.navBarColor); editor.putInt(UPVOTED, customTheme.upvoted); editor.putInt(DOWNVOTED, customTheme.downvoted); editor.putInt(POST_TYPE_BACKGROUND_COLOR, customTheme.postTypeBackgroundColor); editor.putInt(POST_TYPE_TEXT_COLOR, customTheme.postTypeTextColor); editor.putInt(SPOILER_BACKGROUND_COLOR, customTheme.spoilerBackgroundColor); editor.putInt(SPOILER_TEXT_COLOR, customTheme.spoilerTextColor); editor.putInt(NSFW_BACKGROUND_COLOR, customTheme.nsfwBackgroundColor); editor.putInt(NSFW_TEXT_COLOR, customTheme.nsfwTextColor); editor.putInt(FLAIR_BACKGROUND_COLOR, customTheme.flairBackgroundColor); editor.putInt(FLAIR_TEXT_COLOR, customTheme.flairTextColor); editor.putInt(AWARDS_BACKGROUND_COLOR, customTheme.awardsBackgroundColor); editor.putInt(AWARDS_TEXT_COLOR, customTheme.awardsTextColor); editor.putInt(ARCHIVED_ICON_TINT, customTheme.archivedTint); editor.putInt(LOCKED_ICON_TINT, customTheme.lockedIconTint); editor.putInt(CROSSPOST_ICON_TINT, customTheme.crosspostIconTint); editor.putInt(UPVOTE_RATIO_ICON_TINT, customTheme.upvoteRatioIconTint); editor.putInt(STICKIED_POST_ICON_TINT, customTheme.stickiedPostIconTint); editor.putInt(NO_PREVIEW_POST_TYPE_ICON_TINT, customTheme.noPreviewPostTypeIconTint); editor.putInt(SUBSCRIBED, customTheme.subscribed); editor.putInt(UNSUBSCRIBED, customTheme.unsubscribed); editor.putInt(USERNAME, customTheme.username); editor.putInt(SUBREDDIT, customTheme.subreddit); editor.putInt(AUTHOR_FLAIR_TEXT_COLOR, customTheme.authorFlairTextColor); editor.putInt(SUBMITTER, customTheme.submitter); editor.putInt(MODERATOR, customTheme.moderator); editor.putInt(CURRENT_USER, customTheme.currentUser); editor.putInt(SINGLE_COMMENT_THREAD_BACKGROUND_COLOR, customTheme.singleCommentThreadBackgroundColor); editor.putInt(UNREAD_MESSAGE_BACKGROUND_COLOR, customTheme.unreadMessageBackgroundColor); editor.putInt(DIVIDER_COLOR, customTheme.dividerColor); editor.putInt(NO_PREVIEW_POST_TYPE_BACKGROUND_COLOR, customTheme.noPreviewPostTypeBackgroundColor); editor.putInt(VOTE_AND_REPLY_UNAVAILABLE_BUTTON_COLOR, customTheme.voteAndReplyUnavailableButtonColor); editor.putInt(COMMENT_VERTICAL_BAR_COLOR_1, customTheme.commentVerticalBarColor1); editor.putInt(COMMENT_VERTICAL_BAR_COLOR_2, customTheme.commentVerticalBarColor2); editor.putInt(COMMENT_VERTICAL_BAR_COLOR_3, customTheme.commentVerticalBarColor3); editor.putInt(COMMENT_VERTICAL_BAR_COLOR_4, customTheme.commentVerticalBarColor4); editor.putInt(COMMENT_VERTICAL_BAR_COLOR_5, customTheme.commentVerticalBarColor5); editor.putInt(COMMENT_VERTICAL_BAR_COLOR_6, customTheme.commentVerticalBarColor6); editor.putInt(COMMENT_VERTICAL_BAR_COLOR_7, customTheme.commentVerticalBarColor7); editor.putInt(FAB_ICON_COLOR, customTheme.fabIconColor); editor.putInt(CHIP_TEXT_COLOR, customTheme.chipTextColor); editor.putInt(LINK_COLOR, customTheme.linkColor); editor.putInt(RECEIVED_MESSAGE_TEXT_COLOR, customTheme.receivedMessageTextColor); editor.putInt(SENT_MESSAGE_TEXT_COLOR, customTheme.sentMessageTextColor); editor.putInt(RECEIVED_MESSAGE_BACKROUND_COLOR, customTheme.receivedMessageBackgroundColor); editor.putInt(SENT_MESSAGE_BACKGROUND_COLOR, customTheme.sentMessageBackgroundColor); editor.putInt(SEND_MESSAGE_ICON_COLOR, customTheme.sendMessageIconColor); editor.putInt(FULLY_COLLAPSED_COMMENT_BACKGROUND_COLOR, customTheme.fullyCollapsedCommentBackgroundColor); editor.putInt(AWARDED_COMMENT_BACKGROUND_COLOR, customTheme.awardedCommentBackgroundColor); editor.putBoolean(LIGHT_STATUS_BAR, customTheme.isLightStatusBar); editor.putBoolean(LIGHT_NAV_BAR, customTheme.isLightNavBar); editor.putBoolean(CHANGE_STATUS_BAR_ICON_COLOR_AFTER_TOOLBAR_COLLAPSED_IN_IMMERSIVE_INTERFACE, customTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface); editor.apply(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/EditProfileUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.google.gson.Gson; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.subreddit.SubredditSettingData; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public final class EditProfileUtils { /** * @return the error String. Null indicates no error. */ @WorkerThread public static String updateProfileSync(Retrofit oauthRetrofit, @Nullable String accessToken, @NonNull String accountName, String displayName, String publicDesc) { final Map oauthHeader = APIUtils.getOAuthHeader(accessToken); final RedditAPI api = oauthRetrofit.create(RedditAPI.class); final String name = "u_" + accountName; try { Response response = api.getSubredditSetting(oauthHeader, name).execute(); if (response.isSuccessful()) { final String json = response.body(); if (json == null) { return "Something happened."; } final JSONObject resBody = new JSONObject(json); final SubredditSettingData data = new Gson().fromJson(resBody.getString("data"), SubredditSettingData.class); if (data.getPublicDescription().equals(publicDesc) && data.getTitle().equals(displayName)) { // no-op return null; } final Map params = new HashMap<>(); params.put("api_type", "json"); params.put("sr", data.getSubredditId()); params.put("name", name); params.put("type", data.getSubredditType()); // Only this 2 param params.put("public_description", publicDesc); params.put("title", displayName); // Official Reddit app have this 2 params // 1 = disable; 0 = enable || Active in communities visibility || Show which communities I am active in on my profile. params.put("toxicity_threshold_chat_level", String.valueOf(data.getToxicityThresholdChatLevel())); // Content visibility || Posts to this profile can appear in r/all and your profile can be discovered in /users params.put("default_set", String.valueOf(data.isDefaultSet())); // Allow people to follow you || Followers will be notified about posts you make to your profile and see them in their home feed. params.put("accept_followers", String.valueOf(data.isAcceptFollowers())); params.put("allow_top", String.valueOf(data.isPublicTraffic())); // params.put("link_type", String.valueOf(data.getContentOptions())); // // params.put("original_content_tag_enabled", String.valueOf(data.isOriginalContentTagEnabled())); params.put("new_pinned_post_pns_enabled", String.valueOf(data.isNewPinnedPostPnsEnabled())); params.put("prediction_leaderboard_entry_type", String.valueOf(data.getPredictionLeaderboardEntryType())); params.put("restrict_commenting", String.valueOf(data.isRestrictCommenting())); params.put("restrict_posting", String.valueOf(data.isRestrictPosting())); params.put("should_archive_posts", String.valueOf(data.isShouldArchivePosts())); params.put("show_media", String.valueOf(data.isShowMedia())); params.put("show_media_preview", String.valueOf(data.isShowMediaPreview())); params.put("spam_comments", data.getSpamComments()); params.put("spam_links", data.getSpamLinks()); params.put("spam_selfposts", data.getSpamSelfPosts()); params.put("spoilers_enabled", String.valueOf(data.isSpoilersEnabled())); params.put("submit_link_label", data.getSubmitLinkLabel()); params.put("submit_text", data.getSubmitText()); params.put("submit_text_label", data.getSubmitTextLabel()); params.put("user_flair_pns_enabled", String.valueOf(data.isUserFlairPnsEnabled())); params.put("all_original_content", String.valueOf(data.isAllOriginalContent())); params.put("allow_chat_post_creation", String.valueOf(data.isAllowChatPostCreation())); params.put("allow_discovery", String.valueOf(data.isAllowDiscovery())); params.put("allow_galleries", String.valueOf(data.isAllowGalleries())); params.put("allow_images", String.valueOf(data.isAllowImages())); params.put("allow_polls", String.valueOf(data.isAllowPolls())); params.put("allow_post_crossposts", String.valueOf(data.isAllowPostCrossPosts())); params.put("allow_prediction_contributors", String.valueOf(data.isAllowPredictionContributors())); params.put("allow_predictions", String.valueOf(data.isAllowPredictions())); params.put("allow_predictions_tournament", String.valueOf(data.isAllowPredictionsTournament())); params.put("allow_videos", String.valueOf(data.isAllowVideos())); params.put("collapse_deleted_comments", String.valueOf(data.isCollapseDeletedComments())); params.put("comment_score_hide_mins", String.valueOf(data.getCommentScoreHideMins())); params.put("crowd_control_chat_level", String.valueOf(data.getCrowdControlChatLevel())); params.put("crowd_control_filter", String.valueOf(data.getCrowdControlChatLevel())); params.put("crowd_control_level", String.valueOf(data.getCrowdControlLevel())); params.put("crowd_control_mode", String.valueOf(data.isCrowdControlMode())); params.put("description", data.getDescription()); params.put("disable_contributor_requests", String.valueOf(data.isDisableContributorRequests())); params.put("exclude_banned_modqueue", String.valueOf(data.isExcludeBannedModQueue())); params.put("free_form_reports", String.valueOf(data.isFreeFormReports())); params.put("header-title", data.getHeaderHoverText()); params.put("hide_ads", String.valueOf(data.isHideAds())); params.put("key_color", data.getKeyColor()); params.put("lang", data.getLanguage()); params.put("over_18", String.valueOf(data.isOver18())); params.put("suggested_comment_sort", data.getSuggestedCommentSort()); params.put("welcome_message_enabled", String.valueOf(data.isWelcomeMessageEnabled())); params.put("welcome_message_text", String.valueOf(data.getWelcomeMessageText())); params.put("wiki_edit_age", String.valueOf(data.getWikiEditAge())); params.put("wiki_edit_karma", String.valueOf(data.getWikiEditKarma())); params.put("wikimode", data.getWikiMode()); Response postSiteAdminResponse = api.postSiteAdmin(oauthHeader, params).execute(); if (postSiteAdminResponse.isSuccessful()) { return null; } else { return postSiteAdminResponse.message(); } } else { return response.message(); } } catch (IOException | JSONException e) { e.printStackTrace(); return e.getLocalizedMessage(); } } /** * * @return the error String. Null indicates no error. */ @WorkerThread public static String uploadAvatarSync(Retrofit oauthRetrofit, @Nullable String accessToken, @NonNull String accountName, Bitmap image) { try { Response response = oauthRetrofit.create(RedditAPI.class) .uploadSrImg( APIUtils.getOAuthHeader(accessToken), "u_" + accountName, requestBodyUploadSr("icon"), fileToUpload(image, accountName + "-icon")) .execute(); if (response.isSuccessful()) { return null; } else { return response.message(); } } catch (IOException e) { e.printStackTrace(); return e.getLocalizedMessage(); } } /** @return the error String. Null indicates no error. */ @WorkerThread public static String uploadBannerSync(Retrofit oauthRetrofit, @Nullable String accessToken, @NonNull String accountName, Bitmap image) { try { Response response = oauthRetrofit.create(RedditAPI.class) .uploadSrImg( APIUtils.getOAuthHeader(accessToken), "u_" + accountName, requestBodyUploadSr("banner"), fileToUpload(image, accountName + "-banner")) .execute(); if (response.isSuccessful()) { return null; } else { return response.message(); } } catch (IOException e) { e.printStackTrace(); return e.getLocalizedMessage(); } } public static void deleteAvatar(Retrofit oauthRetrofit, @Nullable String accessToken, @NonNull String accountName, EditProfileUtilsListener listener) { oauthRetrofit.create(RedditAPI.class) .deleteSrIcon(APIUtils.getOAuthHeader(accessToken), "u_" + accountName) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) listener.success(); else listener.failed(response.message()); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { t.printStackTrace(); listener.failed(t.getLocalizedMessage()); } }); } public static void deleteBanner(Retrofit oauthRetrofit, @Nullable String accessToken, @NonNull String accountName, EditProfileUtilsListener listener) { oauthRetrofit.create(RedditAPI.class) .deleteSrBanner(APIUtils.getOAuthHeader(accessToken), "u_" + accountName) .enqueue(new Callback<>() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) listener.success(); else listener.failed(response.message()); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { t.printStackTrace(); listener.failed(t.getLocalizedMessage()); } }); } private static Map requestBodyUploadSr(String type) { Map param = new HashMap<>(); param.put("upload_type", APIUtils.getRequestBody(type)); param.put("img_type", APIUtils.getRequestBody("jpg")); return param; } private static MultipartBody.Part fileToUpload(Bitmap image, String fileName) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, stream); byte[] byteArray = stream.toByteArray(); RequestBody fileBody = RequestBody.create(byteArray, MediaType.parse("image/*")); return MultipartBody.Part.createFormData("file", fileName + ".jpg", fileBody); } public interface EditProfileUtilsListener { void success(); void failed(String message); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/GlideImageGetter.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.Html; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Glide; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import java.lang.ref.WeakReference; public class GlideImageGetter implements Html.ImageGetter { private final WeakReference container; private boolean enlargeImage; private final HtmlImagesHandler imagesHandler; private float density = 1.0f; private final float textSize; public GlideImageGetter(TextView textView, boolean enlargeImage) { this(textView, false, null); this.enlargeImage = enlargeImage; } public GlideImageGetter(TextView textView, boolean densityAware, @Nullable HtmlImagesHandler imagesHandler) { this.container = new WeakReference<>(textView); this.imagesHandler = imagesHandler; if (densityAware) { density = container.get().getResources().getDisplayMetrics().density; } textSize = container.get().getTextSize(); } @Override public Drawable getDrawable(String source) { if (imagesHandler != null) { imagesHandler.addImage(source); } BitmapDrawablePlaceholder drawable = new BitmapDrawablePlaceholder(textSize); container.get().post(() -> { TextView textView = container.get(); if (textView != null) { Context context = textView.getContext(); if (!(context instanceof Activity && (((Activity) context).isFinishing() || ((Activity) context).isDestroyed()))) { Glide.with(context) .asBitmap() .load(source) .into(drawable); } } }); return drawable; } private class BitmapDrawablePlaceholder extends BitmapDrawable implements Target { protected Drawable drawable; BitmapDrawablePlaceholder(float textSize) { super(container.get().getResources(), Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)); } @Override public void draw(final Canvas canvas) { if (drawable != null) { drawable.draw(canvas); } } private void setDrawable(Drawable drawable) { this.drawable = drawable; int drawableWidth = (int) (drawable.getIntrinsicWidth() * density); int drawableHeight = (int) (drawable.getIntrinsicHeight() * density); float ratio = (float) drawableWidth / (float) drawableHeight; drawableHeight = enlargeImage ? (int) (textSize * 1.5) : (int) textSize; drawableWidth = (int) (drawableHeight * ratio); drawable.setBounds(0, 0, drawableWidth, drawableHeight); setBounds(0, 0, drawableWidth, drawableHeight); container.get().setText(container.get().getText()); } @Override public void onLoadStarted(@Nullable Drawable placeholderDrawable) { if (placeholderDrawable != null) { setDrawable(placeholderDrawable); } } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { if (errorDrawable != null) { setDrawable(errorDrawable); } } @Override public void onResourceReady(@NonNull Bitmap bitmap, @Nullable Transition transition) { if (container != null) { TextView textView = container.get(); if (textView != null) { Resources resources = textView.getResources(); if (resources != null) { setDrawable(new BitmapDrawable(resources, bitmap)); } } } } @Override public void onLoadCleared(@Nullable Drawable placeholderDrawable) { if (placeholderDrawable != null) { setDrawable(placeholderDrawable); } } @Override public void getSize(@NonNull SizeReadyCallback cb) { cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); } @Override public void removeCallback(@NonNull SizeReadyCallback cb) {} @Override public void setRequest(@Nullable Request request) {} @Nullable @Override public Request getRequest() { return null; } @Override public void onStart() {} @Override public void onStop() {} @Override public void onDestroy() {} } public interface HtmlImagesHandler { void addImage(String uri); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/JSONUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; /** * Created by alex on 2/25/18. */ public class JSONUtils { public static final String KIND_KEY = "kind"; public static final String KIND_VALUE_MORE = "more"; public static final String DATA_KEY = "data"; public static final String AFTER_KEY = "after"; public static final String CHILDREN_KEY = "children"; public static final String COUNT_KEY = "count"; public static final String TITLE_KEY = "title"; public static final String NAME_KEY = "name"; public static final String SUBREDDIT_NAME_PREFIX_KEY = "subreddit_name_prefixed"; public static final String SELFTEXT_KEY = "selftext"; public static final String SELFTEXT_HTML_KEY = "selftext_html"; public static final String AUTHOR_KEY = "author"; public static final String AUTHOR_FLAIR_RICHTEXT_KEY = "author_flair_richtext"; public static final String AUTHOR_FLAIR_TEXT_KEY = "author_flair_text"; public static final String E_KEY = "e"; public static final String T_KEY = "t"; public static final String U_KEY = "u"; public static final String LINK_KEY = "link"; public static final String LINK_AUTHOR_KEY = "link_author"; public static final String LINK_FLAIR_TEXT_KEY = "link_flair_text"; public static final String LINK_FLAIR_RICHTEXT_KEY = "link_flair_richtext"; public static final String SCORE_KEY = "score"; public static final String LIKES_KEY = "likes"; public static final String NSFW_KEY = "over_18"; public static final String PERMALINK_KEY = "permalink"; public static final String CREATED_UTC_KEY = "created_utc"; public static final String PREVIEW_KEY = "preview"; public static final String IMAGES_KEY = "images"; public static final String WIDTH_KEY = "width"; public static final String HEIGHT_KEY = "height"; public static final String SOURCE_KEY = "source"; public static final String URL_KEY = "url"; public static final String MEDIA_KEY = "media"; public static final String REDDIT_VIDEO_KEY = "reddit_video"; public static final String HLS_URL_KEY = "hls_url"; public static final String FALLBACK_URL_KEY = "fallback_url"; public static final String IS_VIDEO_KEY = "is_video"; public static final String CROSSPOST_PARENT_LIST = "crosspost_parent_list"; public static final String REDDIT_VIDEO_PREVIEW_KEY = "reddit_video_preview"; public static final String STICKIED_KEY = "stickied"; public static final String BODY_KEY = "body"; public static final String BODY_HTML_KEY = "body_html"; public static final String COLLAPSED_KEY = "collapsed"; public static final String IS_SUBMITTER_KEY = "is_submitter"; public static final String REPLIES_KEY = "replies"; public static final String DEPTH_KEY = "depth"; public static final String ID_KEY = "id"; public static final String SCORE_HIDDEN_KEY = "score_hidden"; public static final String SUBREDDIT_KEY = "subreddit"; public static final String BANNER_IMG_KEY = "banner_img"; public static final String BANNER_BACKGROUND_IMAGE_KEY = "banner_background_image"; public static final String ICON_IMG_KEY = "icon_img"; public static final String ICON_URL_KEY = "icon_url"; public static final String COMMUNITY_ICON_KEY = "community_icon"; public static final String LINK_KARMA_KEY = "link_karma"; public static final String COMMENT_KARMA_KEY = "comment_karma"; public static final String DISPLAY_NAME_KEY = "display_name"; public static final String SUBREDDIT_TYPE_KEY = "subreddit_type"; public static final String SUBREDDIT_TYPE_VALUE_USER = "user"; public static final String SUBSCRIBERS_KEY = "subscribers"; public static final String PUBLIC_DESCRIPTION_KEY = "public_description"; public static final String ACTIVE_USER_COUNT_KEY = "active_user_count"; public static final String IS_GOLD_KEY = "is_gold"; public static final String IS_FRIEND_KEY = "is_friend"; public static final String JSON_KEY = "json"; public static final String PARENT_ID_KEY = "parent_id"; public static final String LINK_ID_KEY = "link_id"; public static final String LINK_TITLE_KEY = "link_title"; public static final String ERRORS_KEY = "errors"; public static final String ARGS_KEY = "args"; public static final String FIELDS_KEY = "fields"; public static final String VALUE_KEY = "value"; public static final String TEXT_KEY = "text"; public static final String SPOILER_KEY = "spoiler"; public static final String RULES_KEY = "rules"; public static final String SHORT_NAME_KEY = "short_name"; public static final String DESCRIPTION_KEY = "description"; public static final String DESCRIPTION_HTML_KEY = "description_html"; public static final String DESCRIPTION_MD_KEY = "description_md"; public static final String ARCHIVED_KEY = "archived"; public static final String LOCKED_KEY = "locked"; public static final String SAVED_KEY = "saved"; public static final String REMOVED_KEY = "removed"; public static final String REMOVED_BY_CATEGORY_KEY = "removed_by_category"; public static final String TEXT_EDITABLE_KEY = "text_editable"; public static final String SUBJECT_KEY = "subject"; public static final String CONTEXT_KEY = "context"; public static final String EDITED_KEY = "edited"; public static final String DISTINGUISHED_KEY = "distinguished"; public static final String WAS_COMMENT_KEY = "was_comment"; public static final String NEW_KEY = "new"; public static final String NUM_COMMENTS_KEY = "num_comments"; public static final String HIDDEN_KEY = "hidden"; public static final String USER_HAS_FAVORITED_KEY = "user_has_favorited"; public static final String RESOLUTIONS_KEY = "resolutions"; public static final String NUM_SUBSCRIBERS_KEY = "num_subscribers"; public static final String COPIED_FROM_KEY = "copied_from"; public static final String VISIBILITY_KEY = "visibility"; public static final String OVER_18_KEY = "over_18"; public static final String OWNER_KEY = "owner"; public static final String IS_SUBSCRIBER_KEY = "is_subscriber"; public static final String IS_FAVORITED_KEY = "is_favorited"; public static final String SUBREDDITS_KEY = "subreddits"; public static final String PATH_KEY = "path"; public static final String RESIZED_ICONS_KEY = "resized_icons"; public static final String GFY_ITEM_KEY = "gfyItem"; public static final String MP4_URL_KEY = "mp4Url"; public static final String TYPE_KEY = "type"; public static final String MP4_KEY = "mp4"; public static final String THINGS_KEY = "things"; public static final String MEDIA_METADATA_KEY = "media_metadata"; public static final String GALLERY_DATA_KEY = "gallery_data"; public static final String ITEMS_KEY = "items"; public static final String M_KEY = "m"; public static final String MEDIA_ID_KEY = "media_id"; public static final String S_KEY = "s"; public static final String X_KEY = "x"; public static final String Y_KEY = "y"; public static final String DEST_KEY = "dest"; public static final String GIF_KEY = "gif"; public static final String MAX_EMOJIS_KEY = "max_emojis"; public static final String RICHTEXT_KEY = "richtext"; public static final String SUGGESTED_COMMENT_SORT_KEY = "suggested_comment_sort"; public static final String OVER18_KEY = "over18"; public static final String TOTAL_KARMA_KEY = "total_karma"; public static final String AWARDER_KARMA_KEY = "awarder_karma"; public static final String AWARDEE_KARMA_KEY = "awardee_karma"; public static final String CONTENT_URLS_KEY = "content_urls"; public static final String WEBM_KEY = "webm"; public static final String WEBM_URL_KEY = "webmUrl"; public static final String UPVOTE_RATIO_KEY = "upvote_ratio"; public static final String INBOX_COUNT_KEY = "inbox_count"; public static final String NEXT_CURSOR_KEY = "next_cursor"; public static final String POST_KEY = "post"; public static final String STYLES_KEY = "styles"; public static final String AUTHOR_INFO_KEY= "authorInfo"; public static final String VOTE_STATE_KEY = "voteState"; public static final String UPVOTE_RATIO_CAMEL_CASE_KEY = "upvoteRatio"; public static final String OUTBOUND_LINK_KEY = "outboundLink"; public static final String IS_NSFW_KEY = "isNsfw"; public static final String IS_LOCKED_KEY = "isLocked"; public static final String IS_ARCHIVED_KEY = "isArchived"; public static final String IS_SPOILER = "isSpoiler"; public static final String SUGGESTED_COMMENT_SORT_CAMEL_CASE_KEY = "suggestedCommentSort"; public static final String LIVE_COMMENTS_WEBSOCKET_KEY = "liveCommentsWebsocket"; public static final String ICON_KEY = "icon"; public static final String STREAM_KEY = "stream"; public static final String STREAM_ID_KEY = "stream_id"; public static final String THUMBNAIL_KEY = "thumbnail"; public static final String PUBLISH_AT_KEY = "publish_at"; public static final String STATE_KEY = "state"; public static final String UPVOTES_KEY = "upvotes"; public static final String DOWNVOTES_KEY = "downvotes"; public static final String UNIQUE_WATCHERS_KEY = "unique_watchers"; public static final String CONTINUOUS_WATCHERS_KEY = "continuous_watchers"; public static final String TOTAL_CONTINUOUS_WATCHERS_KEY = "total_continuous_watchers"; public static final String CHAT_DISABLED_KEY = "chat_disabled"; public static final String BROADCAST_TIME_KEY = "broadcast_time"; public static final String ESTIMATED_REMAINING_TIME_KEY = "estimated_remaining_time"; public static final String PAYLOAD_KEY = "payload"; public static final String AUTHOR_ICON_IMAGE = "author_icon_img"; public static final String ASSET_KEY = "asset"; public static final String ASSET_ID_KEY = "asset_id"; public static final String TRENDING_SEARCHES_KEY = "trending_searches"; public static final String QUERY_STRING_KEY = "query_string"; public static final String DISPLAY_STRING_KEY = "display_string"; public static final String RESULTS_KEY = "results"; public static final String CONTENT_MD_KEY = "content_md"; public static final String CAPTION_KEY = "caption"; public static final String CAPTION_URL_KEY = "outbound_url"; public static final String FILES_KEY = "files"; public static final String MP4_MOBILE_KEY = "mp4-mobile"; public static final String STATUS_KEY = "status"; public static final String URLS_KEY = "urls"; public static final String HD_KEY = "hd"; public static final String SUGGESTED_SORT_KEY = "suggested_sort"; public static final String P_KEY = "p"; public static final String VARIANTS_KEY = "variants"; public static final String PAGE_KEY = "page"; public static final String SEND_REPLIES_KEY = "send_replies"; public static final String PROFILE_IMG_KEY = "profile_img"; public static final String AUTHOR_FULLNAME_KEY = "author_fullname"; public static final String IS_MOD_KEY = "is_mod"; public static final String CAN_MOD_POST_KEY = "can_mod_post"; public static final String APPROVED_KEY = "approved"; public static final String APPROVED_AT_UTC_KEY = "approved_at_utc"; public static final String APPROVED_BY_KEY = "approved_by"; public static final String SPAM_KEY = "spam"; public static final String O_EMBED_KEY = "oembed"; public static final String THUMBNAIL_URL_KEY = "thumbnail_url"; public static final String VIDEO_DOWNLOAD_URL = "videoDownloadUrl"; public static final String EXPLANATION_KEY = "explanation"; @Nullable public static Map parseMediaMetadata(JSONObject data) { try { if (data.has(JSONUtils.MEDIA_METADATA_KEY)) { Map mediaMetadataMap = new HashMap<>(); JSONObject mediaMetadataJSON = data.getJSONObject(JSONUtils.MEDIA_METADATA_KEY); for (Iterator it = mediaMetadataJSON.keys(); it.hasNext();) { try { String k = it.next(); JSONObject media = mediaMetadataJSON.getJSONObject(k); // Handle giphy entries with "invalid" status by constructing direct Giphy URLs if (media.has(STATUS_KEY) && "invalid".equals(media.getString(STATUS_KEY))) { if (k.startsWith("giphy|")) { MediaMetadata giphyMetadata = createGiphyFallbackMetadata(k); if (giphyMetadata != null) { mediaMetadataMap.put(k, giphyMetadata); } } continue; } String e = media.getString(JSONUtils.E_KEY); JSONObject originalItemJSON = media.getJSONObject(JSONUtils.S_KEY); MediaMetadata.MediaItem originalItem; if (e.equalsIgnoreCase("Image")) { originalItem = new MediaMetadata.MediaItem(originalItemJSON.getInt(JSONUtils.X_KEY), originalItemJSON.getInt(JSONUtils.Y_KEY), originalItemJSON.getString(JSONUtils.U_KEY)); } else { if (originalItemJSON.has(JSONUtils.MP4_KEY)) { originalItem = new MediaMetadata.MediaItem(originalItemJSON.getInt(JSONUtils.X_KEY), originalItemJSON.getInt(JSONUtils.Y_KEY), originalItemJSON.getString(JSONUtils.GIF_KEY), originalItemJSON.getString(JSONUtils.MP4_KEY)); } else { originalItem = new MediaMetadata.MediaItem(originalItemJSON.getInt(JSONUtils.X_KEY), originalItemJSON.getInt(JSONUtils.Y_KEY), originalItemJSON.getString(JSONUtils.GIF_KEY)); } } MediaMetadata.MediaItem downscaledItem; if (media.has(JSONUtils.P_KEY)) { JSONArray downscales = media.getJSONArray(JSONUtils.P_KEY); JSONObject downscaledItemJSON; if (downscales.length() <= 0) { downscaledItem = originalItem; } else { if (downscales.length() <= 3) { downscaledItemJSON = downscales.getJSONObject(downscales.length() - 1); } else { downscaledItemJSON = downscales.getJSONObject(3); } downscaledItem = new MediaMetadata.MediaItem(downscaledItemJSON.getInt(JSONUtils.X_KEY), downscaledItemJSON.getInt(JSONUtils.Y_KEY), downscaledItemJSON.getString(JSONUtils.U_KEY)); } } else { downscaledItem = originalItem; } String id = media.getString(JSONUtils.ID_KEY); mediaMetadataMap.put(id, new MediaMetadata(id, e, originalItem, downscaledItem)); } catch (JSONException e) { /* https://www.reddit.com/r/Leathercraft/comments/1qo3jrv/one_year_of_patina/.json?raw_json=1 "media_metadata": { "1a9oi91fitfg1": { "status": "failed" }, "2ik58hyditfg1": { "status": "failed" } } */ e.printStackTrace(); } } return mediaMetadataMap; } } catch (JSONException e) { e.printStackTrace(); } return null; } /** * Creates a fallback MediaMetadata for giphy entries with "invalid" status. * Extracts the giphy ID from the key (format: "giphy|{id}" or "giphy|{id}|downsized") * and constructs direct Giphy URLs. * * @param key The media_metadata key (e.g., "giphy|abc123|downsized") * @return A MediaMetadata with direct Giphy URLs, or null if the key format is invalid */ @Nullable private static MediaMetadata createGiphyFallbackMetadata(String key) { // Key format: "giphy|{id}" or "giphy|{id}|downsized" String[] parts = key.split("\\|"); if (parts.length < 2) { return null; } String giphyId = parts[1]; String gifUrl = "https://media.giphy.com/media/" + giphyId + "/giphy.gif"; String mp4Url = "https://media.giphy.com/media/" + giphyId + "/giphy.mp4"; // Use reasonable default dimensions (will be adjusted when loaded) MediaMetadata.MediaItem originalItem = new MediaMetadata.MediaItem(480, 480, gifUrl, mp4Url); return new MediaMetadata(key, "AnimatedImage", originalItem, originalItem); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/MaterialYouUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.app.WallpaperColors; import android.app.WallpaperManager; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; import android.os.Build; import android.os.Handler; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import org.greenrobot.eventbus.EventBus; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.customtheme.CustomTheme; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; public class MaterialYouUtils { public interface CheckThemeNameListener { void themeNotExists(); void themeExists(); } public static void checkThemeName(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, CheckThemeNameListener checkThemeNameListener) { executor.execute(() -> { if (redditDataRoomDatabase.customThemeDao().getCustomTheme("Material You") != null || redditDataRoomDatabase.customThemeDao().getCustomTheme("Material You Dark") != null || redditDataRoomDatabase.customThemeDao().getCustomTheme("Material You Amoled") != null) { handler.post(checkThemeNameListener::themeExists); } else { handler.post(checkThemeNameListener::themeNotExists); } }); } public static void changeThemeSync(Context context, RedditDataRoomDatabase redditDataRoomDatabase, CustomThemeWrapper customThemeWrapper, SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences, SharedPreferences internalSharedPreferences) { try { Thread.sleep(2000); } catch (InterruptedException ignored) { } if (changeTheme(context, redditDataRoomDatabase, customThemeWrapper, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, internalSharedPreferences)) { EventBus.getDefault().post(new RecreateActivityEvent()); } } public static void changeThemeASync(Context context, Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, CustomThemeWrapper customThemeWrapper, SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences, SharedPreferences internalSharedPreferences, @Nullable MaterialYouListener materialYouListener) { executor.execute(() -> { try { Thread.sleep(2000); } catch (InterruptedException ignored) { } if (changeTheme(context, redditDataRoomDatabase, customThemeWrapper, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, internalSharedPreferences)) { handler.post(() -> { if (materialYouListener != null) { materialYouListener.applied(); } EventBus.getDefault().post(new RecreateActivityEvent()); }); } }); } private static boolean changeTheme(Context context, RedditDataRoomDatabase redditDataRoomDatabase, CustomThemeWrapper customThemeWrapper, SharedPreferences lightThemeSharedPreferences, SharedPreferences darkThemeSharedPreferences, SharedPreferences amoledThemeSharedPreferences, SharedPreferences internalSharedPreferences) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { CustomTheme lightTheme = CustomThemeWrapper.getIndigo(context); CustomTheme darkTheme = CustomThemeWrapper.getIndigoDark(context); CustomTheme amoledTheme = CustomThemeWrapper.getIndigoAmoled(context); lightTheme.colorPrimary = context.getColor(android.R.color.system_accent1_100); lightTheme.colorPrimaryDark = lightTheme.colorPrimary; lightTheme.colorAccent = context.getColor(android.R.color.system_accent3_300); lightTheme.colorPrimaryLightTheme = lightTheme.colorPrimary; lightTheme.backgroundColor = context.getColor(android.R.color.system_neutral1_50); lightTheme.cardViewBackgroundColor = context.getColor(android.R.color.system_accent3_10); lightTheme.filledCardViewBackgroundColor = lightTheme.cardViewBackgroundColor; lightTheme.commentBackgroundColor = context.getColor(android.R.color.system_neutral2_10); lightTheme.awardedCommentBackgroundColor = context.getColor(android.R.color.system_neutral2_10); lightTheme.bottomAppBarBackgroundColor = lightTheme.colorPrimary; lightTheme.navBarColor = lightTheme.colorPrimary; lightTheme.primaryTextColor = context.getColor(android.R.color.system_neutral1_900); lightTheme.secondaryTextColor = context.getColor(android.R.color.system_neutral1_700); lightTheme.buttonTextColor = lightTheme.primaryTextColor; lightTheme.bottomAppBarIconColor = lightTheme.buttonTextColor; lightTheme.primaryIconColor = context.getColor(android.R.color.system_accent1_400); lightTheme.fabIconColor = lightTheme.buttonTextColor; lightTheme.toolbarPrimaryTextAndIconColor = lightTheme.buttonTextColor; lightTheme.toolbarSecondaryTextColor = lightTheme.buttonTextColor; lightTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = lightTheme.buttonTextColor; lightTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = lightTheme.buttonTextColor; lightTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = lightTheme.colorPrimary; lightTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = lightTheme.backgroundColor; lightTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = lightTheme.buttonTextColor; lightTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = lightTheme.buttonTextColor; lightTheme.circularProgressBarBackground = context.getColor(android.R.color.system_accent1_10); lightTheme.dividerColor = context.getColor(android.R.color.system_neutral1_400); lightTheme.isLightStatusBar = true; lightTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = true; lightTheme.name = "Material You"; darkTheme.colorPrimary = context.getColor(android.R.color.system_accent2_800); darkTheme.colorPrimaryDark = darkTheme.colorPrimary; darkTheme.colorAccent = context.getColor(android.R.color.system_accent3_100); darkTheme.colorPrimaryLightTheme = lightTheme.colorPrimary; darkTheme.backgroundColor = context.getColor(android.R.color.system_neutral1_900); darkTheme.cardViewBackgroundColor = context.getColor(android.R.color.system_neutral2_800); darkTheme.filledCardViewBackgroundColor = darkTheme.cardViewBackgroundColor; darkTheme.commentBackgroundColor = darkTheme.cardViewBackgroundColor; darkTheme.awardedCommentBackgroundColor = darkTheme.cardViewBackgroundColor; darkTheme.bottomAppBarBackgroundColor = darkTheme.colorPrimary; darkTheme.navBarColor = darkTheme.colorPrimary; darkTheme.primaryTextColor = context.getColor(android.R.color.system_neutral1_10); darkTheme.secondaryTextColor = context.getColor(android.R.color.system_neutral1_10); darkTheme.buttonTextColor = context.getColor(android.R.color.system_neutral1_900); darkTheme.bottomAppBarIconColor = context.getColor(android.R.color.system_accent1_100); darkTheme.primaryIconColor = context.getColor(android.R.color.system_accent1_100); darkTheme.fabIconColor = context.getColor(android.R.color.system_neutral1_900); darkTheme.toolbarPrimaryTextAndIconColor = context.getColor(android.R.color.system_accent2_100); darkTheme.toolbarSecondaryTextColor = darkTheme.toolbarPrimaryTextAndIconColor; darkTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = darkTheme.toolbarPrimaryTextAndIconColor; darkTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = darkTheme.toolbarPrimaryTextAndIconColor; darkTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = darkTheme.colorPrimary; darkTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = darkTheme.backgroundColor; darkTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = darkTheme.bottomAppBarIconColor; darkTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = darkTheme.bottomAppBarIconColor; darkTheme.circularProgressBarBackground = context.getColor(android.R.color.system_accent1_900); darkTheme.dividerColor = context.getColor(android.R.color.system_neutral1_600); darkTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = true; darkTheme.name = "Material You Dark"; amoledTheme.colorAccent = context.getColor(android.R.color.system_accent1_100); amoledTheme.colorPrimaryLightTheme = lightTheme.colorPrimary; amoledTheme.fabIconColor = context.getColor(android.R.color.system_neutral1_900); amoledTheme.name = "Material You Amoled"; redditDataRoomDatabase.customThemeDao().unsetLightTheme(); redditDataRoomDatabase.customThemeDao().unsetDarkTheme(); redditDataRoomDatabase.customThemeDao().unsetAmoledTheme(); redditDataRoomDatabase.customThemeDao().insert(lightTheme); redditDataRoomDatabase.customThemeDao().insert(darkTheme); redditDataRoomDatabase.customThemeDao().insert(amoledTheme); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(lightTheme, lightThemeSharedPreferences); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(darkTheme, darkThemeSharedPreferences); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(amoledTheme, amoledThemeSharedPreferences); internalSharedPreferences.edit().putInt(SharedPreferencesUtils.MATERIAL_YOU_SENTRY_COLOR, context.getColor(android.R.color.system_accent1_100)).apply(); return true; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); WallpaperColors wallpaperColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM); if (wallpaperColors != null) { int colorPrimaryInt = lightenColor(wallpaperColors.getPrimaryColor().toArgb(), 0.4); int colorPrimaryDarkInt = darkenColor(colorPrimaryInt, 0.3); int backgroundColor = lightenColor(colorPrimaryInt, 0.2); int cardViewBackgroundColor = lightenColor(colorPrimaryInt, 0.6); Color colorAccent = wallpaperColors.getSecondaryColor(); int colorAccentInt = lightenColor(colorAccent == null ? customThemeWrapper.getColorAccent() : colorAccent.toArgb(), 0.4); int colorPrimaryAppropriateTextColor = getAppropriateTextColor(colorPrimaryInt); int backgroundColorAppropriateTextColor = getAppropriateTextColor(backgroundColor); CustomTheme lightTheme = CustomThemeWrapper.getIndigo(context); CustomTheme darkTheme = CustomThemeWrapper.getIndigoDark(context); CustomTheme amoledTheme = CustomThemeWrapper.getIndigoAmoled(context); lightTheme.colorPrimary = colorPrimaryInt; lightTheme.colorPrimaryDark = colorPrimaryDarkInt; lightTheme.colorAccent = colorAccentInt; lightTheme.colorPrimaryLightTheme = colorPrimaryInt; lightTheme.backgroundColor = backgroundColor; lightTheme.cardViewBackgroundColor = cardViewBackgroundColor; lightTheme.filledCardViewBackgroundColor = cardViewBackgroundColor; lightTheme.commentBackgroundColor = cardViewBackgroundColor; lightTheme.awardedCommentBackgroundColor = cardViewBackgroundColor; lightTheme.bottomAppBarBackgroundColor = colorPrimaryInt; lightTheme.navBarColor = colorPrimaryInt; lightTheme.primaryTextColor = backgroundColorAppropriateTextColor; lightTheme.secondaryTextColor = backgroundColorAppropriateTextColor == Color.BLACK ? Color.parseColor("#8A000000") : Color.parseColor("#B3FFFFFF"); lightTheme.bottomAppBarIconColor = colorPrimaryAppropriateTextColor; lightTheme.primaryIconColor = backgroundColorAppropriateTextColor; lightTheme.fabIconColor = colorPrimaryAppropriateTextColor; lightTheme.toolbarPrimaryTextAndIconColor = colorPrimaryAppropriateTextColor; lightTheme.toolbarSecondaryTextColor = colorPrimaryAppropriateTextColor; lightTheme.tabLayoutWithCollapsedCollapsingToolbarTabIndicator = colorPrimaryAppropriateTextColor; lightTheme.tabLayoutWithCollapsedCollapsingToolbarTextColor = colorPrimaryAppropriateTextColor; lightTheme.tabLayoutWithCollapsedCollapsingToolbarTabBackground = colorPrimaryInt; lightTheme.tabLayoutWithExpandedCollapsingToolbarTabBackground = backgroundColor; lightTheme.tabLayoutWithExpandedCollapsingToolbarTabIndicator = colorPrimaryAppropriateTextColor; lightTheme.tabLayoutWithExpandedCollapsingToolbarTextColor = colorPrimaryAppropriateTextColor; lightTheme.circularProgressBarBackground = colorPrimaryInt; lightTheme.dividerColor = backgroundColorAppropriateTextColor == Color.BLACK ? Color.parseColor("#E0E0E0") : Color.parseColor("69666C"); lightTheme.isLightStatusBar = colorPrimaryAppropriateTextColor == Color.BLACK; lightTheme.isChangeStatusBarIconColorAfterToolbarCollapsedInImmersiveInterface = (lightTheme.isLightStatusBar && getAppropriateTextColor(cardViewBackgroundColor) == Color.WHITE) || (!lightTheme.isLightStatusBar && getAppropriateTextColor(cardViewBackgroundColor) == Color.BLACK); lightTheme.name = "Material You"; darkTheme.colorAccent = colorPrimaryInt; darkTheme.colorPrimaryLightTheme = colorPrimaryInt; darkTheme.name = "Material You Dark"; amoledTheme.colorAccent = colorPrimaryInt; amoledTheme.colorPrimaryLightTheme = colorPrimaryInt; amoledTheme.name = "Material You Amoled"; redditDataRoomDatabase.customThemeDao().unsetLightTheme(); redditDataRoomDatabase.customThemeDao().unsetDarkTheme(); redditDataRoomDatabase.customThemeDao().unsetAmoledTheme(); redditDataRoomDatabase.customThemeDao().insert(lightTheme); redditDataRoomDatabase.customThemeDao().insert(darkTheme); redditDataRoomDatabase.customThemeDao().insert(amoledTheme); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(lightTheme, lightThemeSharedPreferences); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(darkTheme, darkThemeSharedPreferences); CustomThemeSharedPreferencesUtils.insertThemeToSharedPreferences(amoledTheme, amoledThemeSharedPreferences); internalSharedPreferences.edit().putInt(SharedPreferencesUtils.MATERIAL_YOU_SENTRY_COLOR, wallpaperColors.getPrimaryColor().toArgb()).apply(); return true; } } return false; } private static int lightenColor(int color, double ratio) { return Color.argb(Color.alpha(color), (int) (Color.red(color) + (255 - Color.red(color)) * ratio), (int) (Color.green(color) + (255 - Color.green(color)) * ratio), (int) (Color.blue(color) + (255 - Color.blue(color)) * ratio)); } private static int darkenColor(int color, double ratio) { return Color.argb(Color.alpha(color), (int) (Color.red(color) * (1 - ratio)), (int) (Color.green(color) * (1 - ratio)), (int) (Color.blue(color) * (1 - ratio))); } @ColorInt public static int getAppropriateTextColor(@ColorInt int color) { // Counting the perceptive luminance - human eye favors green color... double luminance = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255; return luminance < 0.5 ? Color.BLACK : Color.WHITE; } public interface MaterialYouListener { void applied(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/NotificationUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.content.Context; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import ml.docilealligator.infinityforreddit.R; public class NotificationUtils { public static final String CHANNEL_SUBMIT_POST = "Submit Post"; public static final String CHANNEL_ID_NEW_MESSAGES = "new_messages"; public static final String CHANNEL_NEW_MESSAGES = "New Messages"; public static final String CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO = "download_reddit_video"; public static final String CHANNEL_DOWNLOAD_REDDIT_VIDEO = "Download Reddit Video"; public static final String CHANNEL_ID_DOWNLOAD_VIDEO = "download_video"; public static final String CHANNEL_DOWNLOAD_VIDEO = "Download Video"; public static final String CHANNEL_ID_DOWNLOAD_IMAGE = "download_image"; public static final String CHANNEL_DOWNLOAD_IMAGE = "Download Image"; public static final String CHANNEL_ID_DOWNLOAD_GIF = "download_gif"; public static final String CHANNEL_DOWNLOAD_GIF = "Download Gif"; public static final String CHANNEL_ID_MATERIAL_YOU = "material_you"; public static final String CHANNEL_MATERIAL_YOU = "Material You"; public static final int SUBMIT_POST_SERVICE_NOTIFICATION_ID = 10000; public static final int DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID = 20000; public static final int DOWNLOAD_VIDEO_NOTIFICATION_ID = 30000; public static final int DOWNLOAD_IMAGE_NOTIFICATION_ID = 40000; public static final int DOWNLOAD_GIF_NOTIFICATION_ID = 50000; public static final int MATERIAL_YOU_NOTIFICATION_ID = 60000; public static final int EDIT_PROFILE_SERVICE_NOTIFICATION_ID = 70000; private static final int SUMMARY_BASE_ID_UNREAD_MESSAGE = 0; private static final int NOTIFICATION_BASE_ID_UNREAD_MESSAGE = 1; private static final String GROUP_USER_BASE = "ml.docilealligator.infinityforreddit."; public static NotificationCompat.Builder buildNotification(NotificationManagerCompat notificationManager, Context context, String title, String content, String summary, String channelId, String channelName, String group, int color) { NotificationChannelCompat channel = new NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_DEFAULT) .setName(channelName) .build(); notificationManager.createNotificationChannel(channel); return new NotificationCompat.Builder(context.getApplicationContext(), channelId) .setContentTitle(title) .setContentText(content) .setSmallIcon(R.drawable.ic_notification) .setColor(color) .setStyle(new NotificationCompat.BigTextStyle() .setSummaryText(summary) .bigText(content)) .setGroup(group) .setAutoCancel(true); } public static NotificationCompat.Builder buildSummaryNotification(Context context, NotificationManagerCompat notificationManager, String title, String content, String channelId, String channelName, String group, int color) { NotificationChannelCompat channel = new NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_DEFAULT) .setName(channelName) .build(); notificationManager.createNotificationChannel(channel); return new NotificationCompat.Builder(context, channelId) .setContentTitle(title) //set content text to support devices running API level < 24 .setContentText(content) .setSmallIcon(R.drawable.ic_notification) .setColor(color) .setGroup(group) .setGroupSummary(true) .setAutoCancel(true); } public static NotificationManagerCompat getNotificationManager(Context context) { return NotificationManagerCompat.from(context); } public static String getAccountGroupName(String accountName) { return GROUP_USER_BASE + accountName; } public static int getSummaryIdUnreadMessage(int accountIndex) { return SUMMARY_BASE_ID_UNREAD_MESSAGE + accountIndex * 1000; } public static int getNotificationIdUnreadMessage(int accountIndex, int messageIndex) { return NOTIFICATION_BASE_ID_UNREAD_MESSAGE + accountIndex * 1000 + messageIndex; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/ShareScreenshotUtils.kt ================================================ package ml.docilealligator.infinityforreddit.utils import android.content.ClipData import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.PorterDuff import android.graphics.drawable.Drawable import android.os.Handler import android.os.Looper import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.widget.ImageView import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.MultiTransformation import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.Target import com.github.alexzhirkevich.customqrgenerator.QrData import com.github.alexzhirkevich.customqrgenerator.vector.QrCodeDrawable import com.github.alexzhirkevich.customqrgenerator.vector.QrVectorOptions import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorBallShape import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorColor import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorColors import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorFrameShape import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogo import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoPadding import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoShape import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorPixelShape import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorShapes import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.RoundedCornersTransformation import ml.docilealligator.infinityforreddit.R import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy import ml.docilealligator.infinityforreddit.activities.BaseActivity import ml.docilealligator.infinityforreddit.comment.Comment import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper import ml.docilealligator.infinityforreddit.databinding.ItemSharedCommentRowBinding import ml.docilealligator.infinityforreddit.databinding.SharedCommentBinding import ml.docilealligator.infinityforreddit.databinding.SharedPostBinding import ml.docilealligator.infinityforreddit.databinding.SharedPostWithCommentsBinding import ml.docilealligator.infinityforreddit.post.Post import java.io.File import java.io.FileOutputStream import java.io.IOException import java.util.Locale fun sharePostAsScreenshot( baseActivity: BaseActivity, post: Post, customThemeWrapper: CustomThemeWrapper, locale: Locale, timeFormatPattern: String, saveMemoryCenterInsideDownsampleStrategy: SaveMemoryCenterInisdeDownsampleStrategy ) { //val binding: SharedPostBinding = SharedPostBinding.inflate(LayoutInflater.from(ContextThemeWrapper(baseActivity, R.style.AppTheme))) val binding: SharedPostBinding = SharedPostBinding.inflate(LayoutInflater.from(baseActivity)) binding.titleTextViewSharedPost.text = post.title binding.subredditNameTextViewSharedPost.text = post.subredditNamePrefixed binding.userTextViewSharedPost.text = post.authorNamePrefixed binding.postTimeTextViewSharedPost.text = Utils.getFormattedTime( locale, post.postTimeMillis, timeFormatPattern ) binding.scoreTextViewSharedPost.text = post.score.toString() binding.commentsCountTextViewSharedPost.text = post.nComments.toString() binding.root.setBackgroundTintList(ColorStateList.valueOf(customThemeWrapper.filledCardViewBackgroundColor)) binding.titleTextViewSharedPost.setTextColor(customThemeWrapper.postTitleColor) binding.contentTextViewSharedPost.setTextColor(customThemeWrapper.postContentColor) binding.subredditNameTextViewSharedPost.setTextColor(customThemeWrapper.subreddit) binding.userTextViewSharedPost.setTextColor(customThemeWrapper.username) binding.postTimeTextViewSharedPost.setTextColor(customThemeWrapper.secondaryTextColor) binding.scoreTextViewSharedPost.setTextColor(customThemeWrapper.upvoted) binding.commentsCountTextViewSharedPost.setTextColor(customThemeWrapper.postIconAndInfoColor) binding.upvoteImageViewSharedPost.setColorFilter( customThemeWrapper.upvoted, PorterDuff.Mode.SRC_IN ) binding.commentImageViewSharedPost.setColorFilter( customThemeWrapper.postIconAndInfoColor, PorterDuff.Mode.SRC_IN ) binding.titleTextViewSharedPost.setTypeface(baseActivity.titleTypeface) binding.contentTextViewSharedPost.setTypeface(baseActivity.contentTypeface) binding.subredditNameTextViewSharedPost.setTypeface(baseActivity.titleTypeface) binding.userTextViewSharedPost.setTypeface(baseActivity.titleTypeface) binding.postTimeTextViewSharedPost.setTypeface(baseActivity.titleTypeface) binding.scoreTextViewSharedPost.setTypeface(baseActivity.titleTypeface) binding.commentsCountTextViewSharedPost.setTypeface(baseActivity.titleTypeface) binding.qrCodeImageViewSharedPost.setImageDrawable(generateQRCode(baseActivity, customThemeWrapper, post.permalink)) when (post.postType) { Post.VIDEO_TYPE, Post.GIF_TYPE, Post.IMAGE_TYPE, Post.GALLERY_TYPE, Post.LINK_TYPE -> { binding.contentTextViewSharedPost.visibility = View.GONE val preview = if (post.previews.isNotEmpty()) post.previews[0] else null if (preview != null) { val height = (400 * baseActivity.resources.displayMetrics.density).toInt() binding.imageViewSharedPost.setScaleType(ImageView.ScaleType.CENTER_CROP) binding.imageViewSharedPost.layoutParams.height = height measureView(binding.getRoot()) val blurImage = post.isNSFW || post.isSpoiler val url = preview.previewUrl val imageRequestBuilder = Glide.with(baseActivity).load(url) .listener(object : RequestListener { override fun onLoadFailed( e: GlideException?, model: Any?, target: Target, isFirstResource: Boolean ): Boolean { binding.imageViewSharedPost.visibility = View.GONE measureView(binding.getRoot()) shareScreenshot(baseActivity, getBitmapFromView(binding.getRoot())) return false } override fun onResourceReady( resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean ): Boolean { Handler(Looper.getMainLooper()).post { shareScreenshot(baseActivity, getBitmapFromView(binding.getRoot())) } return false } }) if (blurImage) { imageRequestBuilder.apply( RequestOptions.bitmapTransform( MultiTransformation( CenterCrop(), BlurTransformation(50, 10), RoundedCornersTransformation(4, 0) ) ) ).into(binding.imageViewSharedPost) } else { imageRequestBuilder.apply(RequestOptions.bitmapTransform( MultiTransformation( CenterCrop(), RoundedCornersTransformation(50, 0) ) )).downsample( saveMemoryCenterInsideDownsampleStrategy ).into(binding.imageViewSharedPost) } return } else { binding.imageViewSharedPost.visibility = View.GONE } } Post.NO_PREVIEW_LINK_TYPE -> { binding.contentTextViewSharedPost.text = post.url binding.imageViewSharedPost.visibility = View.GONE } else -> { if (post.selfTextPlainTrimmed != null && post.selfTextPlainTrimmed.isNotEmpty()) { binding.contentTextViewSharedPost.text = post.selfTextPlainTrimmed } binding.imageViewSharedPost.visibility = View.GONE } } measureView(binding.getRoot()) shareScreenshot(baseActivity, getBitmapFromView(binding.getRoot())) } fun sharePostWithCommentsAsScreenshot( baseActivity: BaseActivity, post: Post, comments: List, customThemeWrapper: CustomThemeWrapper, locale: Locale, timeFormatPattern: String, saveMemoryCenterInsideDownsampleStrategy: SaveMemoryCenterInisdeDownsampleStrategy ) { val binding: SharedPostWithCommentsBinding = SharedPostWithCommentsBinding.inflate(LayoutInflater.from(baseActivity)) binding.titleTextViewSharedPostWithComments.text = post.title binding.subredditNameTextViewSharedPostWithComments.text = post.subredditNamePrefixed binding.userTextViewSharedPostWithComments.text = post.authorNamePrefixed binding.postTimeTextViewSharedPostWithComments.text = Utils.getFormattedTime(locale, post.postTimeMillis, timeFormatPattern) binding.scoreTextViewSharedPostWithComments.text = post.score.toString() binding.commentsCountTextViewSharedPostWithComments.text = post.nComments.toString() binding.root.setBackgroundTintList(ColorStateList.valueOf(customThemeWrapper.filledCardViewBackgroundColor)) binding.titleTextViewSharedPostWithComments.setTextColor(customThemeWrapper.postTitleColor) binding.contentTextViewSharedPostWithComments.setTextColor(customThemeWrapper.postContentColor) binding.subredditNameTextViewSharedPostWithComments.setTextColor(customThemeWrapper.subreddit) binding.userTextViewSharedPostWithComments.setTextColor(customThemeWrapper.username) binding.postTimeTextViewSharedPostWithComments.setTextColor(customThemeWrapper.secondaryTextColor) binding.scoreTextViewSharedPostWithComments.setTextColor(customThemeWrapper.upvoted) binding.commentsCountTextViewSharedPostWithComments.setTextColor(customThemeWrapper.postIconAndInfoColor) binding.upvoteImageViewSharedPostWithComments.setColorFilter(customThemeWrapper.upvoted, PorterDuff.Mode.SRC_IN) binding.commentImageViewSharedPostWithComments.setColorFilter(customThemeWrapper.postIconAndInfoColor, PorterDuff.Mode.SRC_IN) binding.titleTextViewSharedPostWithComments.setTypeface(baseActivity.titleTypeface) binding.contentTextViewSharedPostWithComments.setTypeface(baseActivity.contentTypeface) binding.subredditNameTextViewSharedPostWithComments.setTypeface(baseActivity.titleTypeface) binding.userTextViewSharedPostWithComments.setTypeface(baseActivity.titleTypeface) binding.postTimeTextViewSharedPostWithComments.setTypeface(baseActivity.titleTypeface) binding.scoreTextViewSharedPostWithComments.setTypeface(baseActivity.titleTypeface) binding.commentsCountTextViewSharedPostWithComments.setTypeface(baseActivity.titleTypeface) binding.qrCodeImageViewSharedPostWithComments.setImageDrawable(generateQRCode(baseActivity, customThemeWrapper, post.permalink)) val depthColors = intArrayOf( customThemeWrapper.commentVerticalBarColor1, customThemeWrapper.commentVerticalBarColor2, customThemeWrapper.commentVerticalBarColor3, customThemeWrapper.commentVerticalBarColor4, customThemeWrapper.commentVerticalBarColor5, customThemeWrapper.commentVerticalBarColor6, customThemeWrapper.commentVerticalBarColor7, ) for (comment in comments.take(10)) { val rowBinding = ItemSharedCommentRowBinding.inflate(LayoutInflater.from(baseActivity)) rowBinding.authorTextViewItemSharedCommentRow.text = comment.author rowBinding.scoreTextViewItemSharedCommentRow.text = comment.score.toString() rowBinding.timeTextViewItemSharedCommentRow.text = Utils.getFormattedTime(locale, comment.commentTimeMillis, timeFormatPattern) rowBinding.contentTextViewItemSharedCommentRow.text = comment.commentRawText val depthColor = depthColors[comment.depth % depthColors.size] rowBinding.depthIndicatorItemSharedCommentRow.setBackgroundColor(depthColor) rowBinding.authorTextViewItemSharedCommentRow.setTextColor(customThemeWrapper.username) rowBinding.scoreTextViewItemSharedCommentRow.setTextColor(customThemeWrapper.postIconAndInfoColor) rowBinding.timeTextViewItemSharedCommentRow.setTextColor(customThemeWrapper.secondaryTextColor) rowBinding.contentTextViewItemSharedCommentRow.setTextColor(customThemeWrapper.commentColor) val depthPaddingPx = (comment.depth * 16 * baseActivity.resources.displayMetrics.density).toInt() rowBinding.root.setPaddingRelative(depthPaddingPx, rowBinding.root.paddingTop, rowBinding.root.paddingEnd, rowBinding.root.paddingBottom) if (baseActivity.typeface != null) { rowBinding.authorTextViewItemSharedCommentRow.typeface = baseActivity.typeface rowBinding.scoreTextViewItemSharedCommentRow.typeface = baseActivity.typeface rowBinding.timeTextViewItemSharedCommentRow.typeface = baseActivity.typeface rowBinding.contentTextViewItemSharedCommentRow.typeface = baseActivity.contentTypeface } binding.commentsContainerSharedPostWithComments.addView(rowBinding.root) } when (post.postType) { Post.VIDEO_TYPE, Post.GIF_TYPE, Post.IMAGE_TYPE, Post.GALLERY_TYPE, Post.LINK_TYPE -> { binding.contentTextViewSharedPostWithComments.visibility = View.GONE val preview = if (post.previews.isNotEmpty()) post.previews[0] else null if (preview != null) { val height = (400 * baseActivity.resources.displayMetrics.density).toInt() binding.imageViewSharedPostWithComments.setScaleType(ImageView.ScaleType.CENTER_CROP) binding.imageViewSharedPostWithComments.layoutParams.height = height measureView(binding.root) val blurImage = post.isNSFW || post.isSpoiler val url = preview.previewUrl val imageRequestBuilder = Glide.with(baseActivity).load(url) .listener(object : RequestListener { override fun onLoadFailed(e: GlideException?, model: Any?, target: Target, isFirstResource: Boolean): Boolean { binding.imageViewSharedPostWithComments.visibility = View.GONE measureView(binding.root) shareScreenshot(baseActivity, getBitmapFromView(binding.root)) return false } override fun onResourceReady(resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean { Handler(Looper.getMainLooper()).post { shareScreenshot(baseActivity, getBitmapFromView(binding.root)) } return false } }) if (blurImage) { imageRequestBuilder.apply(RequestOptions.bitmapTransform(MultiTransformation(CenterCrop(), BlurTransformation(50, 10), RoundedCornersTransformation(4, 0)))).into(binding.imageViewSharedPostWithComments) } else { imageRequestBuilder.apply(RequestOptions.bitmapTransform(MultiTransformation(CenterCrop(), RoundedCornersTransformation(50, 0)))).downsample(saveMemoryCenterInsideDownsampleStrategy).into(binding.imageViewSharedPostWithComments) } return } else { binding.imageViewSharedPostWithComments.visibility = View.GONE } } Post.NO_PREVIEW_LINK_TYPE -> { binding.contentTextViewSharedPostWithComments.text = post.url binding.imageViewSharedPostWithComments.visibility = View.GONE } else -> { if (post.selfTextPlainTrimmed != null && post.selfTextPlainTrimmed.isNotEmpty()) { binding.contentTextViewSharedPostWithComments.text = post.selfTextPlainTrimmed } binding.imageViewSharedPostWithComments.visibility = View.GONE } } measureView(binding.root) shareScreenshot(baseActivity, getBitmapFromView(binding.root)) } fun shareCommentAsScreenshot( baseActivity: BaseActivity, comment: Comment ) { val binding: SharedCommentBinding = SharedCommentBinding.inflate(LayoutInflater.from(ContextThemeWrapper(baseActivity, R.style.AppTheme))) val customThemeWrapper = baseActivity.customThemeWrapper binding.userTextViewSharedComment.text = "— u/" + comment.author binding.contentTextViewSharedComment.text = comment.commentRawText binding.root.setBackgroundTintList(ColorStateList.valueOf(customThemeWrapper.filledCardViewBackgroundColor)) binding.userTextViewSharedComment.setTextColor(customThemeWrapper.username) binding.contentTextViewSharedComment.setTextColor(customThemeWrapper.commentColor) binding.quoteImageViewSharedComment.setColorFilter( customThemeWrapper.colorPrimary, PorterDuff.Mode.SRC_IN ) binding.userTextViewSharedComment.setTypeface(baseActivity.typeface) binding.contentTextViewSharedComment.setTypeface(baseActivity.contentTypeface) binding.qrCodeImageViewSharedComment.setImageDrawable(generateQRCode(baseActivity, customThemeWrapper, comment.permalink)) measureView(binding.getRoot()) shareScreenshot(baseActivity, getBitmapFromView(binding.getRoot())) } private fun measureView(rootView: View) { val specWidth = View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) val specHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) rootView.measure(specWidth, specHeight) rootView.layout(0, 0, rootView.measuredWidth, rootView.measuredHeight) } private fun getBitmapFromView(rootView: View): Bitmap { val bitmap = Bitmap.createBitmap(rootView.width, rootView.height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) val bgDrawable = rootView.background if (bgDrawable != null) bgDrawable.draw(canvas) else canvas.drawColor(Color.WHITE) rootView.draw(canvas) return bitmap } private fun generateQRCode(baseActivity: BaseActivity, customThemeWrapper: CustomThemeWrapper, url: String): Drawable { val data: QrData.Url = QrData.Url(url) return QrCodeDrawable( data, QrVectorOptions.Builder() .setLogo( QrVectorLogo( drawable = ContextCompat.getDrawable(baseActivity, R.mipmap.ic_launcher_round), size = .3f, padding = QrVectorLogoPadding.Natural(.1f), shape = QrVectorLogoShape.Circle ) ) .setColors( QrVectorColors( dark = QrVectorColor.Solid(Color.BLACK), light = QrVectorColor.Solid(Color.WHITE), ball = QrVectorColor.Solid(Color.BLACK), frame = QrVectorColor.Solid(Color.BLACK) ) ) .setShapes( QrVectorShapes( darkPixel = QrVectorPixelShape.RoundCorners(0.5f), ball = QrVectorBallShape.RoundCorners(0.5f), frame = QrVectorFrameShape.RoundCorners(0.25f), ) ).build() ) } private fun shareScreenshot(context: Context, bitmap: Bitmap) { try { val cachePath = File(context.externalCacheDir, "images") if (!cachePath.exists()) { cachePath.mkdirs() } val file = File(cachePath, "shared_view.png") val stream = FileOutputStream(file) bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) stream.close() val uri = FileProvider.getUriForFile( context, context.packageName + ".provider", file ) val intent = Intent(Intent.ACTION_SEND) intent.setType("image/png") intent.putExtra(Intent.EXTRA_STREAM, uri) intent.clipData = ClipData.newRawUri("", uri) intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) context.startActivity(Intent.createChooser(intent, "Share")) } catch (e: IOException) { e.printStackTrace() } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesLiveData.kt ================================================ package ml.docilealligator.infinityforreddit.utils 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 { sharedPreferences, 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)!! } 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)!! } 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/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.view.Display; import androidx.annotation.Nullable; /** * Created by alex on 2/23/18. */ public class SharedPreferencesUtils { public static final String ENABLE_NOTIFICATION_KEY = "enable_notification"; public static final String NOTIFICATION_INTERVAL_KEY = "notificaiton_interval"; public static final String LAZY_MODE_INTERVAL_KEY = "lazy_mode_interval"; public static final String THEME_KEY = "theme"; public static final String ICON_FOREGROUND_KEY = "icon_foreground"; public static final String ICON_BACKGROUND_KEY = "icon_background"; public static final String ERROR_IMAGE_KEY = "error_image"; public static final String CROSSPOST_ICON_KEY = "crosspost_icon"; public static final String THUMBTACK_ICON_KEY = "thumbtack_icon"; public static final String BEST_ROCKET_ICON_KEY = "best_rocket_icon"; public static final String MATERIAL_ICONS_KEY = "material_icons"; public static final String OPEN_SOURCE_KEY = "open_source"; public static final String RATE_KEY = "rate"; public static final String EMAIL_KEY = "email"; public static final String REDDIT_ACCOUNT_KEY = "reddit_account"; public static final String SUBREDDIT_KEY = "subreddit"; public static final String SHARE_KEY = "share"; public static final String PRIVACY_POLICY_KEY = "privacy_policy"; public static final String VERSION_KEY = "version"; public static final String SCREEN_WIDTH_DP_KEY = "screen_width_dp"; public static final String SMALLEST_SCREEN_WIDTH_DP_KEY = "smallest_screen_width_dp"; public static final String IS_TABLET_KEY = "is_tablet"; public static final String FONT_SIZE_KEY = "font_size"; public static final String TITLE_FONT_SIZE_KEY = "title_font_size"; public static final String CONTENT_FONT_SIZE_KEY = "content_font_size"; public static final String FONT_FAMILY_KEY = "font_family"; public static final String TITLE_FONT_FAMILY_KEY = "title_font_family"; public static final String CONTENT_FONT_FAMILY_KEY = "content_font_family"; public static final String AMOLED_DARK_KEY = "amoled_dark"; public static final String IMMERSIVE_INTERFACE_ENTRY_KEY = "immersive_interface_entry"; public static final String IMMERSIVE_INTERFACE_KEY = "immersive_interface"; public static final String DISABLE_IMMERSIVE_INTERFACE_IN_LANDSCAPE_MODE = "disable_immersive_interface_in_landscape_mode"; public static final String BOTTOM_APP_BAR_KEY = "bottom_app_bar"; public static final String VOTE_BUTTONS_ON_THE_RIGHT_KEY = "vote_buttons_on_the_right"; public static final String SHOW_AVATAR_ON_THE_RIGHT = "show_avatar_on_the_right"; public static final String DEFAULT_SEARCH_RESULT_TAB = "default_search_result_tab"; public static final String CUSTOM_FONT_FAMILY_KEY = "custom_font_family"; public static final String CUSTOM_TITLE_FONT_FAMILY_KEY = "custom_title_font_family"; public static final String CUSTOM_CONTENT_FONT_FAMILY_KEY = "custom_content_font_family"; public static final String HIDE_FAB_IN_POST_FEED = "hide_fab_in_post_feed"; public static final String SORT_TYPE_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.sort_type"; public static final String SORT_TYPE_BEST_POST = "sort_type_best_post"; public static final String SORT_TIME_BEST_POST = "sort_time_best_post"; public static final String SORT_TYPE_SEARCH_POST = "sort_type_search_post"; public static final String SORT_TIME_SEARCH_POST = "sort_time_search_post"; public static final String SORT_TYPE_SUBREDDIT_POST_BASE = "sort_type_subreddit_post_"; public static final String SORT_TIME_SUBREDDIT_POST_BASE = "sort_time_subreddit_post_"; public static final String SORT_TYPE_MULTI_REDDIT_POST_BASE = "sort_type_multi_reddit_post_"; public static final String SORT_TIME_MULTI_REDDIT_POST_BASE = "sort_time_multi_reddit_post_"; public static final String SORT_TYPE_USER_POST_BASE = "sort_type_user_post_"; public static final String SORT_TIME_USER_POST_BASE = "sort_time_user_post_"; public static final String SORT_TYPE_USER_COMMENT = "sort_type_user_comment"; public static final String SORT_TIME_USER_COMMENT = "sort_time_user_comment"; public static final String SORT_TYPE_SEARCH_SUBREDDIT = "sort_type_search_subreddit"; public static final String SORT_TYPE_SEARCH_USER = "sort_type_search_user"; public static final String SORT_TYPE_POST_COMMENT = "sort_type_post_comment"; public static final String POST_LAYOUT_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.post_layout"; public static final String POST_LAYOUT_FRONT_PAGE_POST = "post_layout_best_post"; public static final String POST_LAYOUT_SUBREDDIT_POST_BASE = "post_layout_subreddit_post_"; public static final String POST_LAYOUT_MULTI_REDDIT_POST_BASE = "post_layout_multi_reddit_post_"; public static final String POST_LAYOUT_USER_POST_BASE = "post_layout_user_post_"; public static final String POST_LAYOUT_SEARCH_POST = "post_layout_search_post"; public static final String HISTORY_POST_LAYOUT_READ_POST = "history_post_layout_read_post"; public static final int POST_LAYOUT_CARD = 0; public static final int POST_LAYOUT_COMPACT = 1; public static final int POST_LAYOUT_GALLERY = 2; public static final int POST_LAYOUT_CARD_2 = 3; public static final int POST_LAYOUT_CARD_3 = 4; public static final int POST_LAYOUT_COMPACT_2 = 5; public static final String FRONT_PAGE_SCROLLED_POSITION_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.front_page_scrolled_position"; public static final String FRONT_PAGE_SCROLLED_POSITION_FRONT_PAGE_BASE = "_front_page"; public static final String FRONT_PAGE_SCROLLED_POSITION_ANONYMOUS = ".anonymous"; public static final String PULL_NOTIFICATION_TIME = "pull_notification_time"; public static final String SHOW_ELAPSED_TIME_KEY = "show_elapsed_time"; public static final String TIME_FORMAT_KEY = "time_format"; public static final String TIME_FORMAT_DEFAULT_VALUE = "MMM d, yyyy, HH:mm"; public static final String DEFAULT_POST_LAYOUT_KEY = "default_post_layout"; public static final String SHOW_DIVIDER_IN_COMPACT_LAYOUT = "show_divider_in_compact_layout"; public static final String SHOW_THUMBNAIL_ON_THE_LEFT_IN_COMPACT_LAYOUT = "show_thumbnail_on_the_left_in_compact_layout"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT = "number_of_columns_in_post_feed_portrait"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE = "number_of_columns_in_post_feed_landscape"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_UNFOLDED = "number_of_columns_in_post_feed_portrait_unfolded"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_UNFOLDED = "number_of_columns_in_post_feed_landscape_unfolded"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_COMPACT_LAYOUT = "number_of_columns_in_post_feed_portrait_compact_layout"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_COMPACT_LAYOUT = "number_of_columns_in_post_feed_landscape_compact_layout"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_GALLERY_LAYOUT = "number_of_columns_in_post_feed_portrait_gallery_layout"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_GALLERY_LAYOUT = "number_of_columns_in_post_feed_landscape_gallery_layout"; public static final String SWIPE_RIGHT_TO_GO_BACK = "swipe_to_go_back_from_post_detail"; public static final String SWIPE_VERTICALLY_TO_GO_BACK_FROM_MEDIA = "swipe_vertically_to_go_back_from_media"; public static final String VOLUME_KEYS_NAVIGATE_COMMENTS = "volume_keys_navigate_comments"; public static final String VOLUME_KEYS_NAVIGATE_POSTS = "volume_keys_navigate_posts"; public static final String MUTE_VIDEO = "mute_video"; public static final String LINK_HANDLER = "link_handler"; public static final String VIDEO_AUTOPLAY = "video_autoplay"; public static final String VIDEO_AUTOPLAY_VALUE_ALWAYS_ON = "2"; public static final String VIDEO_AUTOPLAY_VALUE_ON_WIFI = "1"; public static final String VIDEO_AUTOPLAY_VALUE_NEVER = "0"; public static final String SIMULTANEOUS_AUTOPLAY_LIMIT = "simultaneous_autoplay_limit"; public static final String MUTE_AUTOPLAYING_VIDEOS = "mute_autoplaying_videos"; public static final String AUTOPLAY_NSFW_VIDEOS = "autoplay_nsfw_videos"; public static final String LOCK_JUMP_TO_NEXT_TOP_LEVEL_COMMENT_BUTTON = "lock_jump_to_next_top_level_comment_button"; public static final String SWAP_TAP_AND_LONG_COMMENTS = "swap_tap_and_long_in_comments"; public static final String SWIPE_UP_TO_HIDE_JUMP_TO_NEXT_TOP_LEVEL_COMMENT_BUTTON = "swipe_up_to_hide_jump_to_next_top_level_comments_button"; public static final String SHOW_TOP_LEVEL_COMMENTS_FIRST = "show_top_level_comments_first"; public static final String MAIN_PAGE_BACK_BUTTON_ACTION = "main_page_back_button_action"; public static final int MAIN_PAGE_BACK_BUTTON_ACTION_CONFIRM_EXIT = 1; public static final int MAIN_PAGE_BACK_BUTTON_ACTION_OPEN_NAVIGATION_DRAWER = 2; public static final String LOCK_TOOLBAR = "lock_toolbar"; public static final String LOCK_BOTTOM_APP_BAR = "lock_bottom_app_bar"; public static final String COMMENT_TOOLBAR_HIDDEN = "comment_toolbar_hidden"; public static final String COMMENT_TOOLBAR_HIDE_ON_CLICK = "comment_toolbar_hide_on_click"; public static final String FULLY_COLLAPSE_COMMENT = "fully_collapse_comment"; public static final String SHOW_COMMENT_DIVIDER = "show_comment_divider"; public static final String SHOW_ABSOLUTE_NUMBER_OF_VOTES = "show_absolute_number_of_votes"; public static final String CUSTOMIZE_LIGHT_THEME = "customize_light_theme"; public static final String CUSTOMIZE_DARK_THEME = "customize_dark_theme"; public static final String CUSTOMIZE_AMOLED_THEME = "customize_amoled_theme"; public static final String MANAGE_THEMES = "manage_themes"; public static final String DELETE_ALL_SUBREDDITS_DATA_IN_DATABASE = "delete_all_subreddits_data_in_database"; public static final String DELETE_ALL_USERS_DATA_IN_DATABASE = "delete_all_users_data_in_database"; public static final String DELETE_ALL_SORT_TYPE_DATA_IN_DATABASE = "delete_all_sort_type_data_in_database"; public static final String DELETE_ALL_POST_LAYOUT_DATA_IN_DATABASE = "delete_all_post_layout_data_in_database"; public static final String DELETE_ALL_THEMES_IN_DATABASE = "delete_all_themes_in_database"; public static final String DELETE_FRONT_PAGE_SCROLLED_POSITIONS_IN_DATABASE = "delete_front_page_scrolled_positions_in_database"; public static final String DELETE_READ_POSTS_IN_DATABASE = "delete_read_posts_in_database"; public static final String DELETE_ALL_LEGACY_SETTINGS = "delete_all_legacy_settings"; public static final String RESET_ALL_SETTINGS = "reset_all_settings"; public static final String IMAGE_DOWNLOAD_LOCATION = "image_download_location"; public static final String GIF_DOWNLOAD_LOCATION = "gif_download_location"; public static final String VIDEO_DOWNLOAD_LOCATION = "video_download_location"; public static final String SEPARATE_FOLDER_FOR_EACH_SUBREDDIT = "separate_folder_for_each_subreddit"; public static final String SAVE_NSFW_MEDIA_IN_DIFFERENT_FOLDER = "save_nsfw_media_in_different_folder"; public static final String NSFW_DOWNLOAD_LOCATION = "nsfw_download_location"; public static final String VIBRATE_WHEN_ACTION_TRIGGERED = "vibrate_when_action_triggered"; public static final String DISABLE_SWIPING_BETWEEN_TABS = "disable_swiping_between_tabs"; public static final String ENABLE_SWIPE_ACTION = "enable_swipe_action"; public static final String SWIPE_ACTION_THRESHOLD = "swipe_action_threshold"; public static final String PULL_TO_REFRESH = "pull_to_refresh"; public static final String LONG_PRESS_TO_HIDE_TOOLBAR_IN_COMPACT_LAYOUT = "long_press_to_hide_toolbar_in_compact_layout"; public static final String POST_COMPACT_LAYOUT_TOOLBAR_HIDDEN_BY_DEFAULT = "post_compact_layout_toolbar_hidden_by_default"; public static final String SECURITY = "security"; public static final String START_AUTOPLAY_VISIBLE_AREA_OFFSET_PORTRAIT = "start_autoplay_visible_area_offset_portrait"; public static final String START_AUTOPLAY_VISIBLE_AREA_OFFSET_LANDSCAPE = "start_autoplay_visible_area_offset_landscape"; public static final String MUTE_NSFW_VIDEO = "mute_nsfw_video"; public static final String VIDEO_PLAYER_IGNORE_NAV_BAR = "video_player_ignore_nav_bar"; public static final String SAVE_FRONT_PAGE_SCROLLED_POSITION = "save_front_page_scrolled_position"; public static final String DATA_SAVING_MODE_PREFERENCE = "data_saving_mode_preference"; public static final String DATA_SAVING_MODE = "data_saving_mode"; public static final String DATA_SAVING_MODE_OFF = "0"; public static final String DATA_SAVING_MODE_ONLY_ON_CELLULAR_DATA = "1"; public static final String DATA_SAVING_MODE_ALWAYS = "2"; public static final String NATIONAL_FLAGS = "national_flags"; public static final String RESPECT_SUBREDDIT_RECOMMENDED_COMMENT_SORT_TYPE = "respect_subreddit_recommended_comment_sort_type"; public static final String UFO_CAPTURING_ANIMATION = "ufo_capturing_animation"; public static final String HIDE_SUBREDDIT_DESCRIPTION = "hide_subreddit_description"; public static final String DISABLE_IMAGE_PREVIEW = "disable_image_preview"; public static final String SWIPE_LEFT_ACTION = "swipe_left_action"; public static final String SWIPE_RIGHT_ACTION = "swipe_right_action"; public static final int SWIPE_ACITON_UPVOTE = 0; public static final int SWIPE_ACITON_DOWNVOTE = 1; public static final String LANGUAGE = "language"; public static final String LANGUAGE_DEFAULT_VALUE = "auto"; public static final String ENABLE_SEARCH_HISTORY = "enable_search_history"; public static final String POST_FILTER = "post_filter"; public static final String ONLY_DISABLE_PREVIEW_IN_VIDEO_AND_GIF_POSTS = "only_disable_preview_in_video_and_gif_posts"; public static final String SAVE_SORT_TYPE = "save_sort_type"; public static final String SUBREDDIT_DEFAULT_SORT_TYPE = "subreddit_default_sort_type"; public static final String SUBREDDIT_DEFAULT_SORT_TIME = "subreddit_default_sort_time"; public static final String USER_DEFAULT_SORT_TYPE = "user_default_sort_type"; public static final String USER_DEFAULT_SORT_TIME = "user_default_sort_time"; public static final String CLICK_TO_SHOW_MEDIA_IN_GALLERY_LAYOUT = "click_to_show_media_in_gallery_layout"; public static final String HIDE_POST_TYPE = "hide_post_type"; public static final String HIDE_POST_FLAIR = "hide_post_flair"; public static final String HIDE_SUBREDDIT_AND_USER_PREFIX = "hide_subreddit_and_user_prefix"; public static final String HIDE_THE_NUMBER_OF_VOTES = "hide_the_number_of_votes"; public static final String HIDE_THE_NUMBER_OF_COMMENTS = "hide_the_number_of_comments"; public static final String BACKUP_SETTINGS = "backup_settings"; public static final String RESTORE_SETTINGS = "restore_settings"; public static final String SHOW_SUICIDE_PREVENTION_ACTIVITY = "show_suicide_prevention_activity"; public static final String LOVE_ANIMATION = "love_animation"; public static final String SWIPE_BETWEEN_POSTS = "swipe_between_posts"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_PORTRAIT_CARD_LAYOUT_2 = "number_of_columns_in_post_feed_portrait_card_layout_2"; public static final String NUMBER_OF_COLUMNS_IN_POST_FEED_LANDSCAPE_CARD_LAYOUT_2 = "number_of_columns_in_post_feed_landscape_card_layout_2"; public static final String DISABLE_NSFW_FOREVER = "disable_nsfw_forever"; public static final String SHOW_ONLY_ONE_COMMENT_LEVEL_INDICATOR = "show_only_one_comment_level_indicator"; public static final String ENABLE_MATERIAL_YOU = "enable_material_you"; public static final String APPLY_MATERIAL_YOU = "apply_material_you"; public static final String VIDEO_PLAYER_AUTOMATIC_LANDSCAPE_ORIENTATION = "video_player_automatic_landscape_orientation"; public static final String REMEMBER_MUTING_OPTION_IN_POST_FEED = "remember_muting_option_in_post_feed"; public static final String DEFAULT_LINK_POST_LAYOUT_KEY = "default_link_post_layout"; public static final String USE_BOTTOM_TOOLBAR_IN_MEDIA_VIEWER = "use_bottom_toolbar_in_media_viewer"; public static final String HIDE_ACCOUNT_KARMA_NAV_BAR = "hide_account_karma"; public static final String LOCK_SCREEN_ANIMATION = "lock_screen_animation"; public static final String ENABLE_FOLD_SUPPORT = "enable_fold_support"; public static final String DEFAULT_POST_LAYOUT_UNFOLDED_KEY = "default_post_layout_unfolded"; public static final String LOOP_VIDEO = "loop_video"; public static final String DEFAULT_PLAYBACK_SPEED = "default_playback_speed"; public static final String LEGACY_AUTOPLAY_VIDEO_CONTROLLER_UI = "legacy_autoplay_video_controller_ui"; public static final String PINCH_TO_ZOOM_VIDEO = "pinch_to_zoom_video"; public static final String FIXED_HEIGHT_PREVIEW_IN_CARD = "fixed_height_preview_in_card"; public static final String HIDE_TEXT_POST_CONTENT = "hide_text_post_content"; public static final String SHOW_FEWER_TOOLBAR_OPTIONS_THRESHOLD = "show_fewer_toolbar_options_threshold"; public static final String SHOW_AUTHOR_AVATAR = "show_author_avatar"; public static final String DISABLE_PROFILE_AVATAR_ANIMATION = "disable_profile_avatar_animation"; public static final String SHOW_USER_PREFIX = "show_user_prefix"; public static final String HIDE_UPVOTE_RATIO = "hide_upvote_ratio"; public static final String POST_FEED_MAX_RESOLUTION = "post_feed_max_resolution"; public static final String REDDIT_VIDEO_DEFAULT_RESOLUTION = "reddit_video_default_resolution"; public static final String EASIER_TO_WATCH_IN_FULL_SCREEN = "easier_to_watch_in_full_screen"; public static final String HIDE_THE_NUMBER_OF_VOTES_IN_COMMENTS = "hide_the_number_of_votes_in_comments"; public static final String COMMENT_DIVIDER_TYPE = "comment_divider_type"; public static final String REMEMBER_COMMENT_SCROLL_POSITION = "remember_comment_scroll_position"; public static final String SUBSCRIBED_THINGS_SYNC_TIME = "subscribed_things_sync_time"; public static final String COMMENT_FILTER = "comment_filter"; private static final String POST_DETAIL_FAB_PORTRAIT_X_BASE = "fab_portrait_x_"; private static final String POST_DETAIL_FAB_PORTRAIT_Y_BASE = "fab_portrait_y_"; private static final String POST_DETAIL_FAB_LANDSCAPE_X_BASE = "fab_landscape_x_"; private static final String POST_DETAIL_FAB_LANDSCAPE_Y_BASE = "fab_landscape_y_"; public static final String REDDIT_VIDEO_DEFAULT_RESOLUTION_NO_DATA_SAVING = "reddit_video_default_resolution_no_data_saving"; public static final String HIDE_FAB_IN_POST_DETAILS = "hide_fab_in_post_details"; public static final String LONG_PRESS_POST_NON_MEDIA_AREA = "long_press_post_non_media_area"; public static final String LONG_PRESS_POST_MEDIA = "long_press_post_media"; public static final String LONG_PRESS_POST_VALUE_NONE = "0"; public static final String LONG_PRESS_POST_VALUE_SHOW_POST_OPTIONS = "1"; public static final String LONG_PRESS_POST_VALUE_PREVIEW_IN_FULLSCREEN = "2"; public static final String TAB_SWITCHING_SENSITIVITY = "tab_switching_sensitivity"; public static final String SWIPE_RIGHT_TO_GO_BACK_SENSITIVITY = "swipe_right_to_go_back_sensitivity"; public static final String SWIPE_ACTION_SENSITIVITY_IN_COMMENTS = "swipe_action_sensitivity_in_comments"; public static final String NAVIGATION_DRAWER_SWIPE_AREA = "navigation_drawer_swipe_area"; public static String getPostDetailFabPortraitX(@Nullable Display display) { if (display == null) { return POST_DETAIL_FAB_PORTRAIT_X_BASE; } return POST_DETAIL_FAB_PORTRAIT_X_BASE + display.getDisplayId(); } public static String getPostDetailFabPortraitY(@Nullable Display display) { if (display == null) { return POST_DETAIL_FAB_PORTRAIT_Y_BASE; } return POST_DETAIL_FAB_PORTRAIT_Y_BASE + display.getDisplayId(); } public static String getPostDetailFabLandscapeX(@Nullable Display display) { if (display == null) { return POST_DETAIL_FAB_LANDSCAPE_X_BASE; } return POST_DETAIL_FAB_LANDSCAPE_X_BASE + display.getDisplayId(); } public static String getPostDetailFabLandscapeY(@Nullable Display display) { if (display == null) { return POST_DETAIL_FAB_LANDSCAPE_Y_BASE; } return POST_DETAIL_FAB_LANDSCAPE_Y_BASE + display.getDisplayId(); } public static final String EMBEDDED_MEDIA_TYPE = "embedded_media_type"; public static final int EMBEDDED_MEDIA_ALL = 15; public static boolean canShowImage(int embeddedMediaType) { return embeddedMediaType == 15 || embeddedMediaType == 7 || embeddedMediaType == 6 || embeddedMediaType == 3; } public static boolean canShowGif(int embeddedMediaType) { return embeddedMediaType == 15 || embeddedMediaType == 7 || embeddedMediaType == 5 || embeddedMediaType == 2; } public static boolean canShowEmote(int embeddedMediaType) { return embeddedMediaType == 15 || embeddedMediaType == 6 || embeddedMediaType == 5 || embeddedMediaType == 1; } public static final String DEFAULT_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit_preferences"; public static final String SHARED_PREFERENCES_FILE = DEFAULT_PREFERENCES_FILE; public static final String SUBSCRIBE_TO_SUB_WHEN_SCROLLING_THROUGH_POSTS_AFTER_SUB_BROWSING_HISTORY_KEY = "subscribe_to_subs_when_scrolling_through_posts_after_sub_browsing_history"; public static final String MAIN_PAGE_TABS_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.main_page_tabs"; public static final String MAIN_PAGE_TAB_COUNT = "_main_page_tab_count"; public static final String MAIN_PAGE_SHOW_TAB_NAMES = "_main_page_show_tab_names"; public static final String MAIN_PAGE_TAB_1_TITLE = "_main_page_tab_1_title"; public static final String MAIN_PAGE_TAB_2_TITLE = "_main_page_tab_2_title"; public static final String MAIN_PAGE_TAB_3_TITLE = "_main_page_tab_3_title"; public static final String MAIN_PAGE_TAB_4_TITLE = "_main_page_tab_4_title"; public static final String MAIN_PAGE_TAB_5_TITLE = "_main_page_tab_5_title"; public static final String MAIN_PAGE_TAB_6_TITLE = "_main_page_tab_6_title"; public static final String MAIN_PAGE_TAB_1_POST_TYPE = "_main_page_tab_1_post_type"; public static final String MAIN_PAGE_TAB_2_POST_TYPE = "_main_page_tab_2_post_type"; public static final String MAIN_PAGE_TAB_3_POST_TYPE = "_main_page_tab_3_post_type"; public static final String MAIN_PAGE_TAB_4_POST_TYPE = "_main_page_tab_4_post_type"; public static final String MAIN_PAGE_TAB_5_POST_TYPE = "_main_page_tab_5_post_type"; public static final String MAIN_PAGE_TAB_6_POST_TYPE = "_main_page_tab_6_post_type"; public static final String MAIN_PAGE_TAB_1_NAME = "_main_page_tab_1_name"; public static final String MAIN_PAGE_TAB_2_NAME = "_main_page_tab_2_name"; public static final String MAIN_PAGE_TAB_3_NAME = "_main_page_tab_3_name"; public static final String MAIN_PAGE_TAB_4_NAME = "_main_page_tab_4_name"; public static final String MAIN_PAGE_TAB_5_NAME = "_main_page_tab_5_name"; public static final String MAIN_PAGE_TAB_6_NAME = "_main_page_tab_6_name"; public static final int MAIN_PAGE_TAB_POST_TYPE_HOME = 0; public static final int MAIN_PAGE_TAB_POST_TYPE_POPULAR = 1; public static final int MAIN_PAGE_TAB_POST_TYPE_ALL = 2; public static final int MAIN_PAGE_TAB_POST_TYPE_SUBREDDIT = 3; public static final int MAIN_PAGE_TAB_POST_TYPE_MULTIREDDIT = 4; public static final int MAIN_PAGE_TAB_POST_TYPE_USER = 5; public static final int MAIN_PAGE_TAB_POST_TYPE_UPVOTED = 6; public static final int MAIN_PAGE_TAB_POST_TYPE_DOWNVOTED = 7; public static final int MAIN_PAGE_TAB_POST_TYPE_HIDDEN = 8; public static final int MAIN_PAGE_TAB_POST_TYPE_SAVED = 9; public static final int MAIN_PAGE_TAB_POST_TYPE_GILDED = 10; public static final String MAIN_PAGE_SHOW_MULTIREDDITS = "_main_page_show_multireddits"; public static final String MAIN_PAGE_SHOW_FAVORITE_MULTIREDDITS = "_main_page_show_favorite_multireddits"; public static final String MAIN_PAGE_SHOW_SUBSCRIBED_SUBREDDITS = "_main_page_show_subscribed_subreddits"; public static final String MAIN_PAGE_SHOW_FAVORITE_SUBSCRIBED_SUBREDDITS = "_main_page_show_favorite_subscribed_subreddits"; public static final String BOTTOM_APP_BAR_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.bottom_app_bar"; public static final String MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_COUNT = "main_activity_bottom_app_bar_option_count"; public static final String MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_1 = "main_activity_bottom_app_bar_option_1"; public static final String MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_2 = "main_activity_bottom_app_bar_option_2"; public static final String MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_3 = "main_activity_bottom_app_bar_option_3"; public static final String MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_4 = "main_activity_bottom_app_bar_option_4"; public static final String MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB = "main_activity_bottom_app_bar_fab"; public static final String OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_COUNT = "other_activities_bottom_app_bar_option_count"; public static final String OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_1 = "other_activities_bottom_app_bar_option_1"; public static final String OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_2 = "other_activities_bottom_app_bar_option_2"; public static final String OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_3 = "other_activities_bottom_app_bar_option_3"; public static final String OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_4 = "other_activities_bottom_app_bar_option_4"; public static final String OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB = "other_activities_bottom_app_bar_fab"; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS = 0; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_MULTIREDDITS = 1; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_INBOX = 2; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_PROFILE = 3; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS = 4; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_REFRESH = 5; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE = 6; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT = 7; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SEARCH = 8; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT = 9; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_USER = 10; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS = 11; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_FILTER_POSTS = 12; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_UPVOTED = 13; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_DOWNVOTED = 14; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_HIDDEN = 15; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_SAVED = 16; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_OPTION_GO_TO_TOP = 17; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS = 0; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_REFRESH = 1; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE = 2; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT = 3; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_SEARCH = 4; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT = 5; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_USER = 6; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS = 7; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_FILTER_POSTS = 8; public static final int MAIN_ACTIVITY_BOTTOM_APP_BAR_FAB_GO_TO_TOP = 9; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HOME = 0; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBSCRIPTIONS = 1; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_INBOX = 2; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_PROFILE = 3; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_MULTIREDDITS = 4; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SUBMIT_POSTS = 5; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_REFRESH = 6; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_SORT_TYPE = 7; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_CHANGE_POST_LAYOUT = 8; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SEARCH = 9; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_SUBREDDIT = 10; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_USER = 11; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDE_READ_POSTS = 12; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_FILTER_POSTS = 13; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_UPVOTED = 14; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_DOWNVOTED = 15; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_HIDDEN = 16; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_SAVED = 17; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_OPTION_GO_TO_TOP = 18; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SUBMIT_POSTS = 0; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_REFRESH = 1; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_SORT_TYPE = 2; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_CHANGE_POST_LAYOUT = 3; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_SEARCH = 4; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_SUBREDDIT = 5; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_USER = 6; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_HIDE_READ_POSTS = 7; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_FILTER_POSTS = 8; public static final int OTHER_ACTIVITIES_BOTTOM_APP_BAR_FAB_GO_TO_TOP = 9; public static final String NSFW_AND_SPOILER_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.nsfw_and_spoiler"; public static final String NSFW_BASE = "_nsfw"; public static final String BLUR_NSFW_BASE = "_blur_nsfw"; public static final String DO_NOT_BLUR_NSFW_IN_NSFW_SUBREDDITS = "do_not_blur_nsfw_in_nsfw_subreddits"; public static final String BLUR_SPOILER_BASE = "_blur_spoiler"; public static final String POST_HISTORY_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.post_history"; public static final String MARK_POSTS_AS_READ_BASE = "_mark_posts_as_read"; public static final String READ_POSTS_LIMIT_ENABLED = "_read_posts_limit_enabled"; public static final String READ_POSTS_LIMIT = "_read_posts_limit"; public static final String MARK_POSTS_AS_READ_AFTER_VOTING_BASE = "_mark_posts_as_read_after_voting"; public static final String MARK_POSTS_AS_READ_ON_SCROLL_BASE = "_mark_posts_as_read_on_scroll"; public static final String HIDE_READ_POSTS_AUTOMATICALLY_BASE = "_hide_read_posts_automatically"; public static final String HIDE_READ_POSTS_AUTOMATICALLY_IN_SUBREDDITS_BASE = "_hide_read_posts_automatically_in_subreddits"; public static final String HIDE_READ_POSTS_AUTOMATICALLY_IN_USERS_BASE = "_hide_read_posts_automatically_in_users"; public static final String HIDE_READ_POSTS_AUTOMATICALLY_IN_SEARCH_BASE = "_hide_read_posts_automatically_in_search"; public static final String CURRENT_ACCOUNT_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.current_account"; public static final String ACCOUNT_NAME = "account_name"; public static final String ACCESS_TOKEN = "access_token"; public static final String ACCOUNT_IMAGE_URL = "account_image_url"; public static final String REDGIFS_ACCESS_TOKEN = "redgifs_access_token"; public static final String INBOX_COUNT = "inbox_count"; public static final String NAVIGATION_DRAWER_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.navigation_drawer"; public static final String COLLAPSE_ACCOUNT_SECTION = "collapse_account_section"; public static final String COLLAPSE_REDDIT_SECTION = "collapse_reddit_section"; public static final String COLLAPSE_POST_SECTION = "collapse_post_section"; public static final String COLLAPSE_PREFERENCES_SECTION = "collapse_preferences_section"; public static final String COLLAPSE_FAVORITE_SUBREDDITS_SECTION = "collapse_favorite_subreddits_section"; public static final String COLLAPSE_SUBSCRIBED_SUBREDDITS_SECTION = "collapse_subscribed_subreddits_section"; public static final String HIDE_FAVORITE_SUBREDDITS_SECTION = "hide_favorite_subreddits_sections"; public static final String HIDE_SUBSCRIBED_SUBREDDITS_SECTIONS = "hide_subscribed_subreddits_sections"; public static final String POST_DETAILS_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.post_details"; public static final String SEPARATE_POST_AND_COMMENTS_IN_PORTRAIT_MODE = "separate_post_and_comments_in_portrait_mode"; public static final String SEPARATE_POST_AND_COMMENTS_IN_LANDSCAPE_MODE = "separate_post_and_comments_in_landscape_mode"; public static final String SWAP_POST_AND_COMMENTS_IN_SPLIT_MODE = "swap_post_and_comments_in_split_mode"; public static final String SECURITY_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.security"; public static final String REQUIRE_AUTHENTICATION_TO_GO_TO_ACCOUNT_SECTION_IN_NAVIGATION_DRAWER = "require_auth_to_account_section"; public static final String SECURE_MODE = "secure_mode"; public static final String APP_LOCK = "app_lock"; public static final String APP_LOCK_TIMEOUT = "app_lock_timeout"; public static final String LAST_FOREGROUND_TIME = "last_foreground_time"; public static final String INTERNAL_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.internal"; public static final String HAS_REQUESTED_NOTIFICATION_PERMISSION = "has_requested_notification_permission"; public static final String DO_NOT_SHOW_REDDIT_API_INFO_V2_AGAIN = "do_not_show_reddit_api_info_v2_again"; public static final String MATERIAL_YOU_SENTRY_COLOR = "material_you_sentry_color"; public static final String PROXY_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.proxy"; public static final String PROXY_ENABLED = "proxy_enabled"; public static final String PROXY_TYPE = "proxy_type"; public static final String PROXY_HOSTNAME = "proxy_hostname"; public static final String PROXY_PORT = "proxy_port"; public static final String CLIENT_ID_PREF_KEY = "client_id_pref_key"; public static final String GIPHY_API_KEY_PREF_KEY = "giphy_api_key_pref_key"; public static final String USER_AGENT_PREF_KEY = "user_agent_pref_key"; public static final String REDIRECT_URI_PREF_KEY = "redirect_uri_pref_key"; //Legacy Settings public static final String MAIN_PAGE_TAB_1_TITLE_LEGACY = "main_page_tab_1_title"; public static final String MAIN_PAGE_TAB_2_TITLE_LEGACY = "main_page_tab_2_title"; public static final String MAIN_PAGE_TAB_3_TITLE_LEGACY = "main_page_tab_3_title"; public static final String MAIN_PAGE_TAB_4_TITLE_LEGACY = "main_page_tab_4_title"; public static final String MAIN_PAGE_TAB_5_TITLE_LEGACY = "main_page_tab_5_title"; public static final String MAIN_PAGE_TAB_6_TITLE_LEGACY = "main_page_tab_6_title"; public static final String MAIN_PAGE_TAB_1_POST_TYPE_LEGACY = "main_page_tab_1_post_type"; public static final String MAIN_PAGE_TAB_2_POST_TYPE_LEGACY = "main_page_tab_2_post_type"; public static final String MAIN_PAGE_TAB_3_POST_TYPE_LEGACY = "main_page_tab_3_post_type"; public static final String MAIN_PAGE_TAB_4_POST_TYPE_LEGACY = "main_page_tab_4_post_type"; public static final String MAIN_PAGE_TAB_5_POST_TYPE_LEGACY = "main_page_tab_5_post_type"; public static final String MAIN_PAGE_TAB_6_POST_TYPE_LEGACY = "main_page_tab_6_post_type"; public static final String MAIN_PAGE_TAB_1_NAME_LEGACY = "main_page_tab_1_name"; public static final String MAIN_PAGE_TAB_2_NAME_LEGACY = "main_page_tab_2_name"; public static final String MAIN_PAGE_TAB_3_NAME_LEGACY = "main_page_tab_3_name"; public static final String MAIN_PAGE_TAB_4_NAME_LEGACY = "main_page_tab_4_name"; public static final String MAIN_PAGE_TAB_5_NAME_LEGACY = "main_page_tab_5_name"; public static final String MAIN_PAGE_TAB_6_NAME_LEGACY = "main_page_tab_6_name"; public static final String SORT_TYPE_ALL_POST_LEGACY = "sort_type_all_post"; public static final String SORT_TIME_ALL_POST_LEGACY = "sort_time_all_post"; public static final String SORT_TYPE_POPULAR_POST_LEGACY = "sort_type_popular_post"; public static final String SORT_TIME_POPULAR_POST_LEGACY = "sort_time_popular_post"; public static final String POST_LAYOUT_POPULAR_POST_LEGACY = "post_layout_popular_post"; public static final String POST_LAYOUT_ALL_POST_LEGACY = "post_layout_all_post"; public static final String NSFW_KEY_LEGACY = "nsfw"; public static final String BLUR_NSFW_KEY_LEGACY = "blur_nsfw"; public static final String BLUR_SPOILER_KEY_LEGACY = "blur_spoiler"; public static final String CONFIRM_TO_EXIT_LEGACY = "confirm_to_exit"; public static final String OPEN_LINK_IN_APP_LEGACY = "open_link_in_app"; public static final String AUTOMATICALLY_TRY_REDGIFS_LEGACY = "automatically_try_redgifs"; public static final String DO_NOT_SHOW_REDDIT_API_INFO_AGAIN_LEGACY = "do_not_show_reddit_api_info_again"; public static final String HIDE_THE_NUMBER_OF_AWARDS_LEGACY = "hide_the_number_of_awards"; public static final String HIDE_COMMENT_AWARDS_LEGACY = "hide_comment_awards"; public static final String IMMERSIVE_INTERFACE_IGNORE_NAV_BAR_KEY_LEGACY = "immersive_interface_ignore_nav_bar"; //Current account public static final String APPLICATION_ONLY_ACCESS_TOKEN_LEGACY = "app_only_access_token"; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/UploadImageUtils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.content.ContentResolver; import android.graphics.Bitmap; import android.net.Uri; import android.webkit.MimeTypeMap; import androidx.annotation.Nullable; import org.apache.commons.io.IOUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.HashMap; import java.util.Map; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; public class UploadImageUtils { @Nullable public static String uploadVideoPosterImage(Retrofit oauthRetrofit, Retrofit uploadMediaRetrofit, String accessToken, Bitmap image) throws IOException, JSONException, XmlPullParserException { RedditAPI api = oauthRetrofit.create(RedditAPI.class); Map uploadImageParams = new HashMap<>(); uploadImageParams.put(APIUtils.FILEPATH_KEY, "post_image.jpg"); uploadImageParams.put(APIUtils.MIMETYPE_KEY, "image/jpeg"); Call uploadImageCall = api.uploadImage(APIUtils.getOAuthHeader(accessToken), uploadImageParams); Response uploadImageResponse = uploadImageCall.execute(); if (uploadImageResponse.isSuccessful()) { Map nameValuePairsMap = parseJSONResponseFromAWS(uploadImageResponse.body()); ByteArrayOutputStream stream = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, stream); byte[] byteArray = stream.toByteArray(); RequestBody fileBody = RequestBody.create(byteArray, MediaType.parse("application/octet-stream")); MultipartBody.Part fileToUpload = MultipartBody.Part.createFormData("file", "post_image.jpg", fileBody); RedditAPI uploadMediaToAWSApi = uploadMediaRetrofit.create(RedditAPI.class); Call uploadMediaToAWS = uploadMediaToAWSApi.uploadMediaToAWS(nameValuePairsMap, fileToUpload); Response uploadMediaToAWSResponse = uploadMediaToAWS.execute(); if (uploadMediaToAWSResponse.isSuccessful()) { return parseImageFromXMLResponseFromAWS(uploadMediaToAWSResponse.body(), false); } else { return "Error: " + uploadMediaToAWSResponse.code(); } } else { return "Error: " + uploadImageResponse.message(); } } @Nullable public static String uploadImage(Retrofit oauthRetrofit, Retrofit uploadMediaRetrofit, ContentResolver contentResolver, String accessToken, Uri imageUri) throws IOException, JSONException, XmlPullParserException { return uploadImage(oauthRetrofit, uploadMediaRetrofit, contentResolver, accessToken, imageUri, false); } @Nullable public static String uploadImage(Retrofit oauthRetrofit, Retrofit uploadMediaRetrofit, ContentResolver contentResolver, String accessToken, Uri imageUri, boolean getImageKey) throws IOException, JSONException, XmlPullParserException { return uploadImage(oauthRetrofit, uploadMediaRetrofit, contentResolver, accessToken, imageUri, false, getImageKey); } @Nullable public static String uploadImage(Retrofit oauthRetrofit, Retrofit uploadMediaRetrofit, ContentResolver contentResolver, String accessToken, Uri imageUri, boolean returnResponseForGallerySubmission, boolean getImageKey) throws IOException, JSONException, XmlPullParserException { String mimeType = contentResolver.getType(imageUri); String extension = "jpg"; if (mimeType != null) { String extensionFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); extension = extensionFromMimeType == null ? extension : extensionFromMimeType; } RedditAPI api = oauthRetrofit.create(RedditAPI.class); Map uploadImageParams = new HashMap<>(); uploadImageParams.put(APIUtils.FILEPATH_KEY, "post_image.jpg"); uploadImageParams.put(APIUtils.MIMETYPE_KEY, mimeType); Call uploadImageCall = api.uploadImage(APIUtils.getOAuthHeader(accessToken), uploadImageParams); Response uploadImageResponse = uploadImageCall.execute(); if (uploadImageResponse.isSuccessful()) { Map nameValuePairsMap = parseJSONResponseFromAWS(uploadImageResponse.body()); try (InputStream inputStream = contentResolver.openInputStream(imageUri)) { byte[] buf = IOUtils.toByteArray(inputStream); RequestBody fileBody = RequestBody.create(buf, MediaType.parse("application/octet-stream")); MultipartBody.Part fileToUpload = MultipartBody.Part.createFormData("file", "post_image." + extension, fileBody); RedditAPI uploadMediaToAWSApi = uploadMediaRetrofit.create(RedditAPI.class); Call uploadMediaToAWS = uploadMediaToAWSApi.uploadMediaToAWS(nameValuePairsMap, fileToUpload); Response uploadMediaToAWSResponse = uploadMediaToAWS.execute(); if (uploadMediaToAWSResponse.isSuccessful()) { if (returnResponseForGallerySubmission) { return uploadImageResponse.body(); } return parseImageFromXMLResponseFromAWS(uploadMediaToAWSResponse.body(), getImageKey); } else { return "Error: " + uploadMediaToAWSResponse.code(); } } } else { return "Error: " + uploadImageResponse.message(); } } @Nullable public static String parseImageFromXMLResponseFromAWS(String response) throws XmlPullParserException, IOException { //Get Image URL return parseImageFromXMLResponseFromAWS(response, false); } @Nullable public static String parseImageFromXMLResponseFromAWS(String response, boolean getImageKey) throws XmlPullParserException, IOException { XmlPullParser xmlPullParser = XmlPullParserFactory.newInstance().newPullParser(); xmlPullParser.setInput(new StringReader(response)); boolean isKeyTag = false; int eventType = xmlPullParser.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { if ((xmlPullParser.getName().equals("Key") && getImageKey) || (xmlPullParser.getName().equals("Location") && !getImageKey)) { isKeyTag = true; } } else if (eventType == XmlPullParser.TEXT) { if (isKeyTag) { return xmlPullParser.getText(); } } eventType = xmlPullParser.next(); } return null; } public static Map parseJSONResponseFromAWS(String response) throws JSONException { JSONObject responseObject = new JSONObject(response); JSONArray nameValuePairs = responseObject.getJSONObject(JSONUtils.ARGS_KEY).getJSONArray(JSONUtils.FIELDS_KEY); Map nameValuePairsMap = new HashMap<>(); for (int i = 0; i < nameValuePairs.length(); i++) { nameValuePairsMap.put(nameValuePairs.getJSONObject(i).getString(JSONUtils.NAME_KEY), APIUtils.getRequestBody(nameValuePairs.getJSONObject(i).getString(JSONUtils.VALUE_KEY))); } return nameValuePairsMap; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/utils/Utils.java ================================================ package ml.docilealligator.infinityforreddit.utils; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.provider.OpenableColumns; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.TypefaceSpan; import android.util.DisplayMetrics; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.Insets; import androidx.core.text.HtmlCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; import com.google.android.material.textfield.TextInputLayout; import org.json.JSONException; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; import io.noties.markwon.core.spans.CustomTypefaceSpan; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.thing.MediaMetadata; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.thing.UploadedImage; import retrofit2.Retrofit; public final class Utils { public static final int NETWORK_TYPE_OTHER = -1; public static final int NETWORK_TYPE_WIFI = 0; public static final int NETWORK_TYPE_CELLULAR = 1; private static final long SECOND_MILLIS = 1000; private static final long MINUTE_MILLIS = 60 * SECOND_MILLIS; private static final long HOUR_MILLIS = 60 * MINUTE_MILLIS; private static final long DAY_MILLIS = 24 * HOUR_MILLIS; private static final long MONTH_MILLIS = 30 * DAY_MILLIS; private static final long YEAR_MILLIS = 12 * MONTH_MILLIS; public static String HOSTNAME_REGEX = "^(?=^.{1,253}$)(([a-z\\d]([a-z\\d-]{0,62}[a-z\\d])*[\\.]){1,3}[a-z]{1,61})$"; private static final Pattern[] REGEX_PATTERNS = { Pattern.compile("((?<=[\\s])|^)/[rRuU]/[\\w-]+/{0,1}"), Pattern.compile("((?<=[\\s])|^)[rRuU]/[\\w-]+/{0,1}"), Pattern.compile("\\^{2,}"), //Sometimes the reddit preview images and gifs have a caption and the markdown will become [caption](image_link) //Matches preview.redd.it and i.redd.it media //For i.redd.it media, it only matches [caption](image-link. Notice there is no ) at the end. //i.redd.it: (\\[(?:(?!((? linkRanges = new ArrayList<>(); while (linkMatcher.find()) { linkRanges.add(new int[]{linkMatcher.start(), linkMatcher.end()}); } if (linkRanges.isEmpty()) { return pattern.matcher(text).replaceAll(replacement); } Matcher matcher = pattern.matcher(text); StringBuffer sb = new StringBuffer(); while (matcher.find()) { boolean insideLink = false; for (int[] range : linkRanges) { if (matcher.start() >= range[0] && matcher.end() <= range[1]) { insideLink = true; break; } } if (insideLink) { matcher.appendReplacement(sb, Matcher.quoteReplacement(matcher.group())); } else { matcher.appendReplacement(sb, replacement); } } matcher.appendTail(sb); return sb.toString(); } private static final Pattern PROCESSING_IMG_PATTERN = Pattern.compile("\\*?Processing img (\\w+)\\.{3}\\*?"); public static String parseRedditImagesBlock(String markdown, @Nullable Map mediaMetadataMap) { if (mediaMetadataMap == null) { return markdown; } // Replace "Processing img ..." placeholders with the actual URL from media_metadata. // The bare URL will then be wrapped by the existing preview.redd.it / i.redd.it logic below. Matcher processingMatcher = PROCESSING_IMG_PATTERN.matcher(markdown); StringBuffer sb = new StringBuffer(); while (processingMatcher.find()) { String imgId = processingMatcher.group(1); MediaMetadata mediaMetadata = mediaMetadataMap.get(imgId); if (mediaMetadata != null && mediaMetadata.original != null) { processingMatcher.appendReplacement(sb, Matcher.quoteReplacement(mediaMetadata.original.url)); } } processingMatcher.appendTail(sb); markdown = sb.toString(); StringBuilder markdownStringBuilder = new StringBuilder(markdown); Pattern previewReddItAndIReddItImagePattern = REGEX_PATTERNS[3]; Matcher matcher = previewReddItAndIReddItImagePattern.matcher(markdownStringBuilder); int start = 0; int previewReddItLength = "https://preview.redd.it/".length(); int iReddItLength = "https://i.redd.it/".length(); while (matcher.find(start)) { if (matcher.group(1) != null) { String id; String caption = null; if (markdownStringBuilder.charAt(matcher.start()) == '[') { //Has caption int urlStartIndex = markdownStringBuilder.lastIndexOf("https://preview.redd.it/", matcher.end()); id = markdownStringBuilder.substring(previewReddItLength + urlStartIndex, markdownStringBuilder.indexOf(".", previewReddItLength + urlStartIndex)); //Minus "](".length() caption = markdownStringBuilder.substring(matcher.start() + 1, urlStartIndex - 2); } else { id = markdownStringBuilder.substring(matcher.start() + previewReddItLength, markdownStringBuilder.indexOf(".", matcher.start() + previewReddItLength)); } MediaMetadata mediaMetadata = mediaMetadataMap.get(id); if (mediaMetadata == null) { start = matcher.end(); continue; } mediaMetadata.caption = caption; if (markdownStringBuilder.charAt(matcher.start()) == '[') { //Has caption markdownStringBuilder.insert(matcher.start(), '!'); start = matcher.end() + 1; } else { String replacingText = "![](" + markdownStringBuilder.substring(matcher.start(), matcher.end()) + ")"; markdownStringBuilder.replace(matcher.start(), matcher.end(), replacingText); start = replacingText.length() + matcher.start(); } matcher = previewReddItAndIReddItImagePattern.matcher(markdownStringBuilder); } else if (matcher.group(2) != null) { String id; String caption = null; if (markdownStringBuilder.charAt(matcher.start()) == '[') { //Has caption int urlStartIndex = markdownStringBuilder.lastIndexOf("https://i.redd.it/", matcher.end()); id = markdownStringBuilder.substring(iReddItLength + urlStartIndex, markdownStringBuilder.indexOf(".", iReddItLength + urlStartIndex)); //Minus "](".length() caption = markdownStringBuilder.substring(matcher.start() + 1, urlStartIndex - 2); } else { id = markdownStringBuilder.substring(matcher.start() + iReddItLength, markdownStringBuilder.indexOf(".", matcher.start() + iReddItLength)); } MediaMetadata mediaMetadata = mediaMetadataMap.get(id); if (mediaMetadata == null) { start = matcher.end(); continue; } mediaMetadata.caption = caption; if (markdownStringBuilder.charAt(matcher.start()) == '[') { //Has caption markdownStringBuilder.insert(matcher.start(), '!'); start = matcher.end() + 1; } else { String replacingText = "![](" + markdownStringBuilder.substring(matcher.start(), matcher.end()) + ")"; markdownStringBuilder.replace(matcher.start(), matcher.end(), replacingText); start = replacingText.length() + matcher.start(); } matcher = previewReddItAndIReddItImagePattern.matcher(markdownStringBuilder); } else { start = matcher.end(); } } return markdownStringBuilder.toString(); } public static String trimTrailingWhitespace(String source) { if (source == null) { return ""; } int i = source.length(); // loop back to the first non-whitespace character do { i--; } while (i >= 0 && Character.isWhitespace(source.charAt(i))); return source.substring(0, i + 1); } public static CharSequence trimTrailingWhitespace(CharSequence source) { if (source == null) { return ""; } int i = source.length(); // loop back to the first non-whitespace character do { i--; } while (i >= 0 && Character.isWhitespace(source.charAt(i))); return source.subSequence(0, i + 1); } public static String getFormattedTime(Locale locale, long time, String pattern) { Calendar postTimeCalendar = Calendar.getInstance(); postTimeCalendar.setTimeInMillis(time); return new SimpleDateFormat(pattern, locale).format(postTimeCalendar.getTime()); } public static String getElapsedTime(Context context, long time) { long now = System.currentTimeMillis(); long diff = now - time; if (diff < MINUTE_MILLIS) { return context.getString(R.string.elapsed_time_just_now); } else if (diff < 2 * MINUTE_MILLIS) { return context.getString(R.string.elapsed_time_a_minute_ago); } else if (diff < 50 * MINUTE_MILLIS) { return context.getString(R.string.elapsed_time_minutes_ago, diff / MINUTE_MILLIS); } else if (diff < 120 * MINUTE_MILLIS) { return context.getString(R.string.elapsed_time_an_hour_ago); } else if (diff < 24 * HOUR_MILLIS) { return context.getString(R.string.elapsed_time_hours_ago, diff / HOUR_MILLIS); } else if (diff < 48 * HOUR_MILLIS) { return context.getString(R.string.elapsed_time_yesterday); } else if (diff < MONTH_MILLIS) { return context.getString(R.string.elapsed_time_days_ago, diff / DAY_MILLIS); } else if (diff < 2 * MONTH_MILLIS) { return context.getString(R.string.elapsed_time_a_month_ago); } else if (diff < YEAR_MILLIS) { return context.getString(R.string.elapsed_time_months_ago, diff / MONTH_MILLIS); } else if (diff < 2 * YEAR_MILLIS) { return context.getString(R.string.elapsed_time_a_year_ago); } else { return context.getString(R.string.elapsed_time_years_ago, diff / YEAR_MILLIS); } } public static String getNVotes(boolean showAbsoluteNumberOfVotes, int votes) { if (showAbsoluteNumberOfVotes) { return Integer.toString(votes); } else { if (Math.abs(votes) < 1000) { return Integer.toString(votes); } return String.format(Locale.US, "%.1f", (float) votes / 1000) + "K"; } } public static void setHTMLWithImageToTextView(TextView textView, String content, boolean enlargeImage) { GlideImageGetter glideImageGetter = new GlideImageGetter(textView, enlargeImage); Spannable html = (Spannable) HtmlCompat.fromHtml( content, HtmlCompat.FROM_HTML_MODE_LEGACY, glideImageGetter, null); textView.setText(html); } public static int getConnectedNetwork(Context context) { ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); if (connMgr != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Network nw = connMgr.getActiveNetwork(); if (nw == null) return NETWORK_TYPE_OTHER; try { NetworkCapabilities actNw = connMgr.getNetworkCapabilities(nw); if (actNw != null) { if (actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { return NETWORK_TYPE_WIFI; } if (actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { return NETWORK_TYPE_CELLULAR; } } } catch (SecurityException ignore) { } } else { boolean isWifi = false; boolean isCellular = false; for (Network network : connMgr.getAllNetworks()) { NetworkInfo networkInfo = connMgr.getNetworkInfo(network); if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { isWifi = true; } if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) { isCellular = true; } } if (isWifi) { return NETWORK_TYPE_WIFI; } if (isCellular) { return NETWORK_TYPE_CELLULAR; } } return NETWORK_TYPE_OTHER; } return NETWORK_TYPE_OTHER; } public static boolean isConnectedToWifi(Context context) { ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); if (connMgr != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Network nw = connMgr.getActiveNetwork(); if (nw == null) return false; NetworkCapabilities actNw = connMgr.getNetworkCapabilities(nw); return actNw != null && actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI); } else { for (Network network : connMgr.getAllNetworks()) { NetworkInfo networkInfo = connMgr.getNetworkInfo(network); if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { return networkInfo.isConnected(); } } } } return false; } public static boolean isConnectedToCellularData(Context context) { ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); if (connMgr != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Network nw = connMgr.getActiveNetwork(); if (nw == null) return false; NetworkCapabilities actNw = connMgr.getNetworkCapabilities(nw); return actNw != null && actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); } else { for (Network network : connMgr.getAllNetworks()) { NetworkInfo networkInfo = connMgr.getNetworkInfo(network); if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) { return networkInfo.isConnected(); } } } } return false; } public static boolean isConnectedToInternet(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivityManager != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Network network = connectivityManager.getActiveNetwork(); if (network == null) { return false; } NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network); return networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); } else { NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } } return false; } public static void displaySortTypeInToolbar(SortType sortType, Toolbar toolbar) { if (sortType != null) { if (sortType.getTime() != null) { toolbar.setSubtitle(sortType.getType().fullName + ": " + sortType.getTime().fullName); } else { toolbar.setSubtitle(sortType.getType().fullName); } } } public static void showKeyboard(Context context, Handler handler, View view) { handler.postDelayed(() -> { InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } }, 300); } public static void hideKeyboard(Activity activity) { InputMethodManager inputMethodManager = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); if (inputMethodManager != null && activity.getCurrentFocus() != null) { inputMethodManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0); } } public static float convertDpToPixel(float dp, Context context) { return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); } @Nullable public static Drawable getTintedDrawable(Context context, int drawableId, int color) { final Drawable drawable = AppCompatResources.getDrawable(context, drawableId); if (drawable != null) { drawable.setTint(color); } return drawable; } public static void uploadImageToReddit(Context context, Executor executor, Retrofit oauthRetrofit, Retrofit uploadMediaRetrofit, String accessToken, EditText editText, CoordinatorLayout coordinatorLayout, Uri imageUri, ArrayList uploadedImages) { Toast.makeText(context, R.string.uploading_image, Toast.LENGTH_SHORT).show(); Handler handler = new Handler(); executor.execute(() -> { try { String imageKeyOrError = UploadImageUtils.uploadImage(oauthRetrofit, uploadMediaRetrofit, context.getContentResolver(), accessToken, imageUri, true); handler.post(() -> { if (imageKeyOrError != null && !imageKeyOrError.startsWith("Error: ")) { String fileName = Utils.getFileName(context, imageUri); if (fileName == null) { fileName = imageKeyOrError; } uploadedImages.add(new UploadedImage(fileName, imageKeyOrError)); int start = Math.max(editText.getSelectionStart(), 0); int end = Math.max(editText.getSelectionEnd(), 0); int realStart = Math.min(start, end); if (realStart > 0 && editText.getText().toString().charAt(realStart - 1) != '\n') { editText.getText().replace(realStart, Math.max(start, end), "\n![](" + imageKeyOrError + ")\n", 0, "\n![]()\n".length() + imageKeyOrError.length()); } else { editText.getText().replace(realStart, Math.max(start, end), "![](" + imageKeyOrError + ")\n", 0, "![]()\n".length() + imageKeyOrError.length()); } Snackbar.make(coordinatorLayout, R.string.upload_image_success, Snackbar.LENGTH_LONG).show(); } else { Toast.makeText(context, R.string.upload_image_failed, Toast.LENGTH_LONG).show(); } }); } catch (XmlPullParserException | JSONException | IOException e) { e.printStackTrace(); handler.post(() -> Toast.makeText(context, R.string.error_processing_image, Toast.LENGTH_LONG).show()); } }); } @Nullable public static String getFileName(Context context, Uri uri) { ContentResolver contentResolver = context.getContentResolver(); if (contentResolver != null) { Cursor cursor = contentResolver.query(uri, null, null, null, null); if (cursor != null) { int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); cursor.moveToFirst(); String fileName = cursor.getString(nameIndex); if (fileName != null && fileName.contains(".")) { fileName = fileName.substring(0, fileName.lastIndexOf('.')); } return fileName; } } return null; } public static void setTitleWithCustomFontToMenuItem(Typeface typeface, MenuItem item, String desiredTitle) { if (typeface != null) { CharSequence title = desiredTitle == null ? item.getTitle() : desiredTitle; if (title != null) { SpannableStringBuilder spannableTitle = new SpannableStringBuilder(title); spannableTitle.setSpan(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? new TypefaceSpan(typeface) : new CustomTypefaceSpan(typeface), 0, spannableTitle.length(), 0); item.setTitle(spannableTitle); } } else if (desiredTitle != null) { item.setTitle(desiredTitle); } } public static void setTitleWithCustomFontToTab(Typeface typeface, TabLayout.Tab tab, String title) { if (typeface != null) { if (title != null) { SpannableStringBuilder spannableTitle = new SpannableStringBuilder(title); spannableTitle.setSpan(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? new TypefaceSpan(typeface) : new CustomTypefaceSpan(typeface), 0, spannableTitle.length(), 0); tab.setText(spannableTitle); } } else { tab.setText(title); } } public static CharSequence getTabTextWithCustomFont(Typeface typeface, CharSequence title) { if (typeface != null && title != null) { SpannableStringBuilder spannableTitle = new SpannableStringBuilder(title); spannableTitle.setSpan(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? new TypefaceSpan(typeface) : new CustomTypefaceSpan(typeface), 0, spannableTitle.length(), 0); return spannableTitle; } else { return title; } } public static void setFontToAllTextViews(View rootView, Typeface typeface) { if (rootView instanceof TextInputLayout) { ((TextInputLayout) rootView).setTypeface(typeface); } else if (rootView instanceof ViewGroup) { ViewGroup rootViewGroup = ((ViewGroup) rootView); int childViewCount = rootViewGroup.getChildCount(); for (int i = 0; i < childViewCount; i++) { setFontToAllTextViews(rootViewGroup.getChildAt(i), typeface); } } else if (rootView instanceof TextView) { ((TextView) rootView).setTypeface(typeface); } } public static int fixIndexOutOfBounds(T[] array, int index) { return index >= array.length ? array.length - 1 : index; } public static int fixIndexOutOfBoundsUsingPredetermined(T[] array, int index, int predeterminedIndex) { return index >= array.length ? predeterminedIndex : index; } @Nullable public static File getCacheDir(Context context) { File cacheDir = context.getExternalCacheDir(); if (cacheDir != null) { return cacheDir; } cacheDir = context.getCacheDir(); if (cacheDir != null) { return cacheDir; } cacheDir = context.getExternalFilesDir(null); if (cacheDir != null) { return cacheDir; } return context.getFilesDir(); } public static Insets getInsets(WindowInsetsCompat insets, boolean includeIME, boolean forcedImmersiveMode) { int insetTypes = WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(); if (includeIME) { insetTypes |= WindowInsetsCompat.Type.ime(); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { // For Android 10 and below return insets.getInsetsIgnoringVisibility(insetTypes); } else { Insets originalInsets = insets.getInsets(insetTypes); return forcedImmersiveMode ? Insets.of(0, 0, 0, originalInsets.bottom) : originalInsets; } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/BaseMeter.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; /** * Abstract the {@link DefaultBandwidthMeter}, provide a wider use. * * @author eneim (2018/01/26). * @since 3.4.0 */ @UnstableApi @SuppressWarnings("WeakerAccess") // public final class BaseMeter implements BandwidthMeter, TransferListener { @NonNull private final T bandwidthMeter; @NonNull private final TransferListener transferListener; /** * @deprecated use {@link #BaseMeter(BandwidthMeter)} instead. */ @SuppressWarnings({ "unused" }) // @Deprecated // public BaseMeter(@NonNull T bandwidthMeter, @NonNull TransferListener transferListener) { this(bandwidthMeter); } public BaseMeter(@NonNull T bandwidthMeter) { this.bandwidthMeter = ToroUtil.checkNotNull(bandwidthMeter); this.transferListener = ToroUtil.checkNotNull(this.bandwidthMeter.getTransferListener()); } @Override public long getBitrateEstimate() { return bandwidthMeter.getBitrateEstimate(); } @Override @Nullable public TransferListener getTransferListener() { return bandwidthMeter.getTransferListener(); } @Override public void addEventListener(Handler eventHandler, EventListener eventListener) { bandwidthMeter.addEventListener(eventHandler, eventListener); } @Override public void removeEventListener(EventListener eventListener) { bandwidthMeter.removeEventListener(eventListener); } @Override public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) { transferListener.onTransferInitializing(source, dataSpec, isNetwork); } @Override public void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork) { transferListener.onTransferStart(source, dataSpec, isNetwork); } @Override public void onBytesTransferred(DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred) { transferListener.onBytesTransferred(source, dataSpec, isNetwork, bytesTransferred); } @Override public void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) { transferListener.onTransferEnd(source, dataSpec, isNetwork); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/CacheManager.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import java.util.LinkedHashMap; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; /** * {@link CacheManager} is a helper interface used by {@link Container} to manage the * {@link PlaybackInfo} of {@link ToroPlayer}s. For each {@link ToroPlayer}, * {@link CacheManager} will ask for a unique key for its {@link PlaybackInfo} cache. * {@link Container} uses a {@link LinkedHashMap} to implement the caching mechanism, so * {@link CacheManager} must provide keys which are uniquely distinguished by * {@link Object#equals(Object)}. * * @author eneim (7/5/17). */ public interface CacheManager { /** * Get the unique key for the {@link ToroPlayer} of a specific order. Note that this key must * also be managed by {@link RecyclerView.Adapter} so that it prevents the uniqueness at data * change events. * * @param order order of the {@link ToroPlayer}. * @return the unique key of the {@link ToroPlayer}. */ @Nullable Object getKeyForOrder(int order); /** * Get the order of a specific key value. Returning a {@code null} order value here will tell * {@link Container} to ignore this key's cache order. * * @param key the key value to lookup. * @return the order of the {@link ToroPlayer} whose unique key value is key. */ @Nullable Integer getOrderForKey(@NonNull Object key); /** * A built-in {@link CacheManager} that use the order as the unique key. Note that this is not * data-changes-proof. Which means that after data change events, the map may need to be * updated. */ CacheManager DEFAULT = new CacheManager() { @Override public Object getKeyForOrder(int order) { return order; } @Override public Integer getOrderForKey(@NonNull Object key) { return key instanceof Integer ? (Integer) key : null; } }; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.core.util.ObjectsCompat; import androidx.media3.common.util.UnstableApi; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; /** * Necessary configuration for {@link ExoCreator} to produces {@link ExoPlayer} and * {@link MediaSource}. Instance of this class must be construct using {@link Builder}. * * @author eneim (2018/01/23). * @since 3.4.0 */ @UnstableApi @SuppressWarnings("SimplifiableIfStatement") // public final class Config { @Nullable private final Context context; // primitive flags @DefaultRenderersFactory.ExtensionRendererMode final int extensionMode; // NonNull options @NonNull final BaseMeter meter; @NonNull final MediaSourceBuilder mediaSourceBuilder; // Nullable options @UnstableApi @Nullable final Cache cache; // null by default // If null, ExoCreator must come up with a default one. // This is to help customizing the Data source, for example using OkHttp extension. @Nullable final DataSource.Factory dataSourceFactory; @OptIn(markerClass = UnstableApi.class) @SuppressWarnings("WeakerAccess") Config(@Nullable Context context, int extensionMode, @NonNull BaseMeter meter, @Nullable DataSource.Factory dataSourceFactory, @NonNull MediaSourceBuilder mediaSourceBuilder, @Nullable Cache cache) { this.context = context != null ? context.getApplicationContext() : null; this.extensionMode = extensionMode; this.meter = meter; this.dataSourceFactory = dataSourceFactory; this.mediaSourceBuilder = mediaSourceBuilder; this.cache = cache; } @OptIn(markerClass = UnstableApi.class) @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Config config = (Config) o; if (extensionMode != config.extensionMode) return false; if (!meter.equals(config.meter)) return false; if (!mediaSourceBuilder.equals(config.mediaSourceBuilder)) return false; if (!ObjectsCompat.equals(cache, config.cache)) return false; return ObjectsCompat.equals(dataSourceFactory, config.dataSourceFactory); } @OptIn(markerClass = UnstableApi.class) @Override public int hashCode() { int result = extensionMode; result = 31 * result + meter.hashCode(); result = 31 * result + mediaSourceBuilder.hashCode(); result = 31 * result + (cache != null ? cache.hashCode() : 0); result = 31 * result + (dataSourceFactory != null ? dataSourceFactory.hashCode() : 0); return result; } @OptIn(markerClass = UnstableApi.class) @SuppressWarnings("unused") public Builder newBuilder() { return new Builder(context).setCache(this.cache) .setExtensionMode(this.extensionMode) .setMediaSourceBuilder(this.mediaSourceBuilder) .setMeter(this.meter); } /// Builder @UnstableApi @SuppressWarnings({"unused", "WeakerAccess"}) // public static final class Builder { @Nullable // only for backward compatibility final Context context; /** * @deprecated Use the constructor with nonnull {@link Context} instead. */ @Deprecated public Builder() { this(null); } @OptIn(markerClass = UnstableApi.class) public Builder(@Nullable Context context) { this.context = context != null ? context.getApplicationContext() : null; DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(this.context).build(); meter = new BaseMeter<>(bandwidthMeter); } @UnstableApi @DefaultRenderersFactory.ExtensionRendererMode private int extensionMode = EXTENSION_RENDERER_MODE_OFF; private BaseMeter meter; private DataSource.Factory dataSourceFactory = null; private MediaSourceBuilder mediaSourceBuilder = MediaSourceBuilder.DEFAULT; @UnstableApi private Cache cache = null; @OptIn(markerClass = UnstableApi.class) public Builder setExtensionMode(@DefaultRenderersFactory.ExtensionRendererMode int extensionMode) { this.extensionMode = extensionMode; return this; } @OptIn(markerClass = UnstableApi.class) public Builder setMeter(@NonNull BaseMeter meter) { this.meter = checkNotNull(meter, "Need non-null BaseMeter"); return this; } // Option is Nullable, but if user customize this, it must be a Nonnull one. public Builder setDataSourceFactory(@NonNull DataSource.Factory dataSourceFactory) { this.dataSourceFactory = checkNotNull(dataSourceFactory); return this; } public Builder setMediaSourceBuilder(@NonNull MediaSourceBuilder mediaSourceBuilder) { this.mediaSourceBuilder = checkNotNull(mediaSourceBuilder, "Need non-null MediaSourceBuilder"); return this; } @OptIn(markerClass = UnstableApi.class) public Builder setCache(@Nullable Cache cache) { this.cache = cache; return this; } @OptIn(markerClass = UnstableApi.class) public Config build() { return new Config(context, extensionMode, meter, dataSourceFactory, mediaSourceBuilder, cache); } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.with; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import android.content.Context; import android.net.Uri; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; import java.io.IOException; import ml.docilealligator.infinityforreddit.utils.APIUtils; /** * Usage: use this as-it or inheritance. * * @author eneim (2018/02/04). * @since 3.4.0 */ @UnstableApi @SuppressWarnings({"unused", "WeakerAccess"}) // public class DefaultExoCreator implements ExoCreator, MediaSourceEventListener { final ToroExo toro; // per application final Config config; private final MediaSourceBuilder mediaSourceBuilder; // stateless private final RenderersFactory renderersFactory; // stateless private final DataSource.Factory mediaDataSourceFactory; // stateless private final DataSource.Factory manifestDataSourceFactory; // stateless public DefaultExoCreator(@NonNull ToroExo toro, @NonNull Config config) { this.toro = checkNotNull(toro); this.config = checkNotNull(config); mediaSourceBuilder = config.mediaSourceBuilder; DefaultRenderersFactory tempFactory = new DefaultRenderersFactory(this.toro.context); tempFactory.setExtensionRendererMode(config.extensionMode); renderersFactory = tempFactory; DataSource.Factory baseFactory = config.dataSourceFactory; if (baseFactory == null) { baseFactory = new DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true).setUserAgent(APIUtils.USER_AGENT); } DataSource.Factory factory = new DefaultDataSource.Factory(this.toro.context, baseFactory); if (config.cache != null) factory = new CacheDataSource.Factory().setCache(config.cache).setUpstreamDataSourceFactory(baseFactory); mediaDataSourceFactory = factory; manifestDataSourceFactory = new DefaultDataSource.Factory(this.toro.context); } public DefaultExoCreator(Context context, Config config) { this(with(context), config); } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DefaultExoCreator that = (DefaultExoCreator) o; if (!toro.equals(that.toro)) return false; if (!mediaSourceBuilder.equals(that.mediaSourceBuilder)) return false; if (!renderersFactory.equals(that.renderersFactory)) return false; if (!mediaDataSourceFactory.equals(that.mediaDataSourceFactory)) return false; return manifestDataSourceFactory.equals(that.manifestDataSourceFactory); } @Override public int hashCode() { int result = toro.hashCode(); result = 31 * result + mediaSourceBuilder.hashCode(); result = 31 * result + renderersFactory.hashCode(); result = 31 * result + mediaDataSourceFactory.hashCode(); result = 31 * result + manifestDataSourceFactory.hashCode(); return result; } @Nullable @Override public Context getContext() { return toro.context; } @NonNull @Override public ExoPlayer createPlayer() { // Create a new TrackSelector for each player instance - they cannot be reused in media3 TrackSelector trackSelector = new DefaultTrackSelector(toro.context); return new ToroExoPlayer(toro.context, renderersFactory, trackSelector, new DefaultLoadControl(), new DefaultBandwidthMeter.Builder(toro.context).build(), Util.getCurrentOrMainLooper()).getPlayer(); } @NonNull @Override public MediaSource createMediaSource(@NonNull Uri uri, String fileExt) { return mediaSourceBuilder.buildMediaSource(this.toro.context, uri, fileExt, new Handler(), manifestDataSourceFactory, mediaDataSourceFactory, this); } @NonNull @Override // public Playable createPlayable(@NonNull Uri uri, String fileExt) { return new PlayableImpl(this, uri, fileExt); } /// MediaSourceEventListener @Override public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // no-ops } @Override public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // no-ops } @Override public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { // no-ops } @Override public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { // no-ops } @Override public void onDownstreamFormatChanged(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { // no-ops } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.MediaSource; /** * A simple interface whose implementation helps Client to easily create {@link ExoPlayer} * instance, {@link MediaSource} instance or specifically a {@link Playable} instance. * * Most of the time, Client just needs to request for a {@link Playable} for a specific Uri. * * @author eneim (2018/02/04). * @since 3.4.0 */ public interface ExoCreator { String TAG = "ToroExo:Creator"; /** * Return current Application context used in {@link ToroExo}. An {@link ExoCreator} must be used * within Application scope. */ @Nullable Context getContext(); /** * Create a new {@link ExoPlayer} instance. This method should always create new instance of * {@link ExoPlayer}, but client should use {@link ExoCreator} indirectly via * {@link ToroExo}. * * @return a new {@link ExoPlayer} instance. */ @NonNull ExoPlayer createPlayer(); /** * Create a {@link MediaSource} from media {@link Uri}. * * @param uri the media {@link Uri}. * @param fileExt the optional (File) extension of the media Uri. * @return a {@link MediaSource} for media {@link Uri}. */ @NonNull MediaSource createMediaSource(@NonNull Uri uri, @Nullable String fileExt); // Client just needs the method below to work with Toro, but I prepare both 2 above for custom use-cases. /** * Create a {@link Playable} for a media {@link Uri}. Client should always use this method for * quick and simple setup. Only use {@link #createMediaSource(Uri, String)} and/or * {@link #createPlayer()} when necessary. * * @param uri the media {@link Uri}. * @param fileExt the optional (File) extension of the media Uri. * @return the {@link Playable} to manage the media {@link Uri}. */ @NonNull Playable createPlayable(@NonNull Uri uri, @Nullable String fileExt); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.toro; import android.net.Uri; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.Tracks; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.BehindLiveWindowException; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.MappingTrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.ui.PlayerView; import com.google.common.collect.ImmutableList; import ml.docilealligator.infinityforreddit.R; /** * Making {@link Playable} extensible. This can be used with custom {@link ExoCreator}. Extending * this class must make sure the re-usability of the implementation. * * @author eneim (2018/02/26). * @since 3.4.0 */ @OptIn(markerClass = UnstableApi.class) @SuppressWarnings("WeakerAccess") public class ExoPlayable extends PlayableImpl { @SuppressWarnings("unused") private static final String TAG = "ToroExo:Playable"; private EventListener listener; // Adapt from ExoPlayer demo. protected boolean inErrorState = false; protected ImmutableList lastSeenTrackGroupArray; /** * Construct an instance of {@link ExoPlayable} from an {@link ExoCreator} and {@link Uri}. The * {@link ExoCreator} is used to request {@link ExoPlayer} instance, while {@link Uri} * defines the media to play. * * @param creator the {@link ExoCreator} instance. * @param uri the {@link Uri} of the media. * @param fileExt the custom extension of the media Uri. */ public ExoPlayable(ExoCreator creator, Uri uri, String fileExt) { super(creator, uri, fileExt); } @Override public void prepare(boolean prepareSource) { if (listener == null) { listener = new Listener(); super.addEventListener(listener); } super.prepare(prepareSource); this.lastSeenTrackGroupArray = null; this.inErrorState = false; } @Override public void setPlayerView(@Nullable PlayerView playerView) { // This will also clear these flags if (playerView != this.playerView) { this.lastSeenTrackGroupArray = null; this.inErrorState = false; } super.setPlayerView(playerView); } @Override public void reset() { super.reset(); this.lastSeenTrackGroupArray = null; this.inErrorState = false; } @Override public void release() { if (listener != null) { super.removeEventListener(listener); listener = null; } super.release(); this.lastSeenTrackGroupArray = null; this.inErrorState = false; } @SuppressWarnings({"unused"}) // protected void onErrorMessage(@NonNull String message) { // Sub class can have custom reaction about the error here, including not to show this toast // (by not calling super.onErrorMessage(message)). if (this.errorListeners.size() > 0) { this.errorListeners.onError(new RuntimeException(message)); } else if (playerView != null) { Toast.makeText(playerView.getContext(), message, Toast.LENGTH_SHORT).show(); } } class Listener extends DefaultEventListener { @Override public void onTracksChanged(@NonNull Tracks tracks) { ImmutableList trackGroups = tracks.getGroups(); if (trackGroups == lastSeenTrackGroupArray) return; lastSeenTrackGroupArray = trackGroups; if (player == null) return; // Get the TrackSelector from the player instance instead of the creator TrackSelector selector = player.getPlayer().getTrackSelector(); if (selector instanceof DefaultTrackSelector) { MappingTrackSelector.MappedTrackInfo trackInfo = ((DefaultTrackSelector) selector).getCurrentMappedTrackInfo(); if (trackInfo != null) { if (trackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { onErrorMessage(toro.getString(R.string.error_unsupported_video)); } if (trackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { onErrorMessage(toro.getString(R.string.error_unsupported_audio)); } } } } @Override public void onPlayerError(@NonNull PlaybackException error) { inErrorState = true; if (isBehindLiveWindow(error)) { ExoPlayable.super.reset(); } else { ExoPlayable.super.updatePlaybackInfo(); } super.onPlayerError(error); } @Override public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { if (inErrorState) { // Adapt from ExoPlayer demo. // "This will only occur if the user has performed a seek whilst in the error state. Update // the resume position so that if the user then retries, playback will resume from the // position to which they seek." - ExoPlayer ExoPlayable.super.updatePlaybackInfo(); } super.onPositionDiscontinuity(oldPosition, newPosition, reason); } } static boolean isBehindLiveWindow(PlaybackException error) { if (error instanceof ExoPlaybackException && ((ExoPlaybackException) error).type != ExoPlaybackException.TYPE_SOURCE) return false; Throwable cause = error.getCause(); while (cause != null) { if (cause instanceof BehindLiveWindowException) return true; cause = cause.getCause(); } return false; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerViewHelper.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.with; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.ui.PlayerView; import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn; import ml.docilealligator.infinityforreddit.videoautoplay.helper.ToroPlayerHelper; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; /** * An implementation of {@link ToroPlayerHelper} where the actual Player is an {@link ExoPlayer} * implementation. This is a bridge between ExoPlayer's callback and ToroPlayerHelper behaviors. * * @author eneim (2018/01/24). * @since 3.4.0 */ public class ExoPlayerViewHelper extends ToroPlayerHelper { @NonNull private final ExoPlayable playable; @UnstableApi @NonNull private final MyEventListeners listeners; private final boolean lazyPrepare; // Container is no longer required for constructing new instance. @SuppressWarnings("unused") @RemoveIn(version = "3.6.0") @Deprecated // public ExoPlayerViewHelper(Container container, @NonNull ToroPlayer player, @NonNull Uri uri) { this(player, uri); } public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri) { this(player, uri, null); } @OptIn(markerClass = UnstableApi.class) public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri, @Nullable String fileExt) { this(player, uri, fileExt, with(player.getPlayerView().getContext()).getDefaultCreator()); } /** * Config instance should be kept as global instance. */ @OptIn(markerClass = UnstableApi.class) public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri, @Nullable String fileExt, @NonNull Config config) { this(player, uri, fileExt, with(player.getPlayerView().getContext()).getCreator(checkNotNull(config))); } public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri, @Nullable String fileExt, @NonNull ExoCreator creator) { this(player, new ExoPlayable(creator, uri, fileExt)); } @OptIn(markerClass = UnstableApi.class) public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull ExoPlayable playable) { super(player); //noinspection ConstantConditions if (player.getPlayerView() == null || !(player.getPlayerView() instanceof PlayerView)) { throw new IllegalArgumentException("Require non-null PlayerView"); } listeners = new MyEventListeners(); this.playable = playable; this.lazyPrepare = true; } @OptIn(markerClass = UnstableApi.class) @Override protected void initialize(@NonNull PlaybackInfo playbackInfo) { playable.setPlaybackInfo(playbackInfo); playable.addEventListener(listeners); playable.addErrorListener(super.getErrorListeners()); playable.addOnVolumeChangeListener(super.getVolumeChangeListeners()); playable.prepare(!lazyPrepare); playable.setPlayerView((PlayerView) player.getPlayerView()); } @OptIn(markerClass = UnstableApi.class) @Override public void release() { super.release(); playable.setPlayerView(null); playable.removeOnVolumeChangeListener(super.getVolumeChangeListeners()); playable.removeErrorListener(super.getErrorListeners()); playable.removeEventListener(listeners); playable.release(); } @Override public void play() { playable.play(); } @Override public void pause() { playable.pause(); } @Override public boolean isPlaying() { return playable.isPlaying(); } @Override public void setVolume(float volume) { playable.setVolume(volume); } @Override public float getVolume() { return playable.getVolume(); } @Override public void setVolumeInfo(@NonNull VolumeInfo volumeInfo) { playable.setVolumeInfo(volumeInfo); } @Override @NonNull public VolumeInfo getVolumeInfo() { return playable.getVolumeInfo(); } @NonNull @Override public PlaybackInfo getLatestPlaybackInfo() { return playable.getPlaybackInfo(); } @Override public void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo) { this.playable.setPlaybackInfo(playbackInfo); } @OptIn(markerClass = UnstableApi.class) public void addEventListener(@NonNull Playable.EventListener listener) { //noinspection ConstantConditions if (listener != null) this.listeners.add(listener); } @OptIn(markerClass = UnstableApi.class) public void removeEventListener(Playable.EventListener listener) { this.listeners.remove(listener); } public ExoPlayer getPlayer() { return playable.player.getPlayer(); } // A proxy, to also hook into ToroPlayerHelper's state change event. @UnstableApi private class MyEventListeners extends Playable.EventListeners { MyEventListeners() { } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { ExoPlayerViewHelper.super.onPlayerStateUpdated(playWhenReady, playbackState); // important super.onPlayerStateChanged(playWhenReady, playbackState); } @Override public void onRenderedFirstFrame() { super.onRenderedFirstFrame(); internalListener.onFirstFrameRendered(); for (ToroPlayer.EventListener listener : ExoPlayerViewHelper.super.getEventListeners()) { listener.onFirstFrameRendered(); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static android.text.TextUtils.isEmpty; import static androidx.media3.common.util.Util.inferContentType; import android.content.Context; import android.net.Uri; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.C.ContentType; import androidx.media3.common.MediaItem; import androidx.media3.common.util.UnstableApi; import androidx.media3.datasource.DataSource; import androidx.media3.exoplayer.dash.DashMediaSource; import androidx.media3.exoplayer.dash.DefaultDashChunkSource; import androidx.media3.exoplayer.hls.HlsMediaSource; import androidx.media3.exoplayer.smoothstreaming.DefaultSsChunkSource; import androidx.media3.exoplayer.smoothstreaming.SsMediaSource; import androidx.media3.exoplayer.source.LoopingMediaSource; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.ProgressiveMediaSource; /** * @author eneim (2018/01/24). * @since 3.4.0 */ public interface MediaSourceBuilder { @OptIn(markerClass = UnstableApi.class) @NonNull MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, @Nullable String fileExt, @Nullable Handler handler, @NonNull DataSource.Factory manifestDataSourceFactory, @NonNull DataSource.Factory mediaDataSourceFactory, @Nullable MediaSourceEventListener listener); MediaSourceBuilder DEFAULT = new MediaSourceBuilder() { @OptIn(markerClass = UnstableApi.class) @NonNull @Override public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, @Nullable String ext, @Nullable Handler handler, @NonNull DataSource.Factory manifestDataSourceFactory, @NonNull DataSource.Factory mediaDataSourceFactory, MediaSourceEventListener listener) { @ContentType int type = isEmpty(ext) ? inferContentType(uri) : inferContentType(Uri.parse("." + ext)); MediaSource result; switch (type) { case C.CONTENT_TYPE_SS: result = new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) .createMediaSource(MediaItem.fromUri(uri)); break; case C.CONTENT_TYPE_DASH: result = new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) .createMediaSource(MediaItem.fromUri(uri)); break; case C.CONTENT_TYPE_HLS: result = new HlsMediaSource.Factory(mediaDataSourceFactory) // .createMediaSource(MediaItem.fromUri(uri)); break; case C.CONTENT_TYPE_OTHER: result = new ProgressiveMediaSource.Factory(mediaDataSourceFactory) // .createMediaSource(MediaItem.fromUri(uri)); break; default: throw new IllegalStateException("Unsupported type: " + type); } result.addEventListener(handler, listener); return result; } }; MediaSourceBuilder LOOPING = new MediaSourceBuilder() { @OptIn(markerClass = UnstableApi.class) @NonNull @Override public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, @Nullable String fileExt, @Nullable Handler handler, @NonNull DataSource.Factory manifestDataSourceFactory, @NonNull DataSource.Factory mediaDataSourceFactory, @Nullable MediaSourceEventListener listener) { return new LoopingMediaSource( DEFAULT.buildMediaSource(context, uri, fileExt, handler, manifestDataSourceFactory, mediaDataSourceFactory, listener)); } }; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MultiPlayPlayerSelector.kt ================================================ package ml.docilealligator.infinityforreddit.videoautoplay import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container import kotlin.math.min class MultiPlayPlayerSelector( var simultaneousAutoplayLimit: Int ): PlayerSelector { override fun select( container: Container, items: List ): Collection { if (simultaneousAutoplayLimit < 0) { return items } val result: MutableList = ArrayList() val count = min(items.size, simultaneousAutoplayLimit) for (i in 0.. * This interface is designed to be reused across Config change. Implementation must not hold any * strong reference to Activity, and if it supports any kind of that, make sure to implicitly clean * it up. * * @author eneim * @since 3.4.0 */ @SuppressWarnings("unused") // public interface Playable { /** * Prepare the resource for a {@link ExoPlayer}. This method should: * - Request for new {@link ExoPlayer} instance if there is not a usable one. * - Configure {@link EventListener} for it. * - If there is non-trivial PlaybackInfo, update it to the ExoPlayer. * - If client request to prepare MediaSource, then prepare it. *

* This method must be called before {@link #setPlayerView(PlayerView)}. * * @param prepareSource if {@code true}, also prepare the MediaSource when preparing the Player, * if {@code false} just do nothing for the MediaSource. */ void prepare(boolean prepareSource); /** * Set the {@link PlayerView} for this Playable. It is expected that a playback doesn't require a * UI, so this setup is optional. But it must be called after the ExoPlayer is prepared, * that is after {@link #prepare(boolean)} and before {@link #release()}. *

* Changing the PlayerView during playback is expected, though not always recommended, especially * on old Devices with low Android API. * * @param playerView the PlayerView to set to the ExoPlayer. */ void setPlayerView(@Nullable PlayerView playerView); /** * Get current {@link PlayerView} of this Playable. * * @return current PlayerView instance of this Playable. */ @Nullable PlayerView getPlayerView(); /** * Start the playback. If the {@link MediaSource} is not prepared, then also prepare it. */ void play(); /** * Pause the playback. */ void pause(); /** * Reset all resource, so that the playback can start all over again. This is to cleanup the * playback for reuse. The ExoPlayer instance must be still usable without calling * {@link #prepare(boolean)}. */ void reset(); /** * Release all resource. After this, the ExoPlayer is released to the Player pool and the * Playable must call {@link #prepare(boolean)} again to use it again. */ void release(); /** * Get current {@link PlaybackInfo} of the playback. * * @return current PlaybackInfo of the playback. */ @NonNull PlaybackInfo getPlaybackInfo(); /** * Set the custom {@link PlaybackInfo} for this playback. This could suggest a seek. * * @param playbackInfo the PlaybackInfo to set for this playback. */ void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo); /** * Add a new {@link EventListener} to this Playable. As calling {@link #prepare(boolean)} also * triggers some internal events, this method should be called before {@link #prepare(boolean)} so * that Client could received them all. * * @param listener the EventListener to add, must be not {@code null}. */ void addEventListener(@NonNull EventListener listener); /** * Remove an {@link EventListener} from this Playable. * * @param listener the EventListener to be removed. If null, nothing happens. */ void removeEventListener(EventListener listener); /** * !This must only work if the Player in use is a {@link ToroExoPlayer}. */ void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener); void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener); /** * Check if current Playable is playing or not. * * @return {@code true} if this Playable is playing, {@code false} otherwise. */ boolean isPlaying(); /** * Change the volume of current playback. * * @param volume the volume value to be set. Must be a {@code float} of range from 0 to 1. * @deprecated use {@link #setVolumeInfo(VolumeInfo)} instead. */ @RemoveIn(version = "3.6.0") @Deprecated // void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume); /** * Obtain current volume value. The returned value is a {@code float} of range from 0 to 1. * * @return current volume value. * @deprecated use {@link #getVolumeInfo()} instead. */ @RemoveIn(version = "3.6.0") @Deprecated // @FloatRange(from = 0.0, to = 1.0) float getVolume(); /** * Update playback's volume. * * @param volumeInfo the {@link VolumeInfo} to update to. * @return {@code true} if current Volume info is updated, {@code false} otherwise. */ boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo); /** * Get current {@link VolumeInfo}. */ @NonNull VolumeInfo getVolumeInfo(); /** * Same as {@link Player#setPlaybackParameters(PlaybackParameters)} */ void setParameters(@Nullable PlaybackParameters parameters); /** * Same as {@link Player#getPlaybackParameters()} */ @Nullable PlaybackParameters getParameters(); void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener); void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener); // Combine necessary interfaces. @UnstableApi interface EventListener extends Player.Listener, TextOutput, MetadataOutput { @Override default void onCues(@NonNull List cues) { } @Override default void onCues(@NonNull CueGroup cueGroup) { } @Override default void onMetadata(@NonNull Metadata metadata) { } } /** * Default empty implementation */ @UnstableApi class DefaultEventListener implements EventListener { @Override public void onTimelineChanged(@NonNull Timeline timeline, int reason) { } @Override public void onTracksChanged(@NonNull Tracks tracks) { } @Override public void onIsLoadingChanged(boolean isLoading) { } @Override public void onPlaybackStateChanged(int playbackState) { } @Override public void onRepeatModeChanged(int repeatMode) { } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { } @Override public void onPlayerError(@NonNull PlaybackException error) { } @Override public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { } @Override public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) { } @Override public void onVideoSizeChanged(@NonNull VideoSize videoSize) { EventListener.super.onVideoSizeChanged(videoSize); } @Override public void onRenderedFirstFrame() { } @Override public void onCues(@NonNull CueGroup cueGroup) { EventListener.super.onCues(cueGroup); } @Override public void onMetadata(@NonNull Metadata metadata) { } } /** * List of EventListener */ @UnstableApi class EventListeners extends CopyOnWriteArraySet implements EventListener { EventListeners() { } @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { for (EventListener eventListener : this) { eventListener.onEvents(player, events); } } @Override public void onVideoSizeChanged(@NonNull VideoSize videoSize) { for (EventListener eventListener : this) { eventListener.onVideoSizeChanged(videoSize); } } @Override public void onRenderedFirstFrame() { for (EventListener eventListener : this) { eventListener.onRenderedFirstFrame(); } } @Override public void onTimelineChanged(@NonNull Timeline timeline, int reason) { for (EventListener eventListener : this) { eventListener.onTimelineChanged(timeline, reason); } } @Override public void onTracksChanged(@NonNull Tracks tracks) { for (EventListener eventListener : this) { eventListener.onTracksChanged(tracks); } } @Override public void onIsLoadingChanged(boolean isLoading) { for (EventListener eventListener : this) { eventListener.onIsLoadingChanged(isLoading); } } @Override public void onPlaybackStateChanged(int playbackState) { for (EventListener eventListener : this) { eventListener.onPlaybackStateChanged(playbackState); } } @Override public void onRepeatModeChanged(int repeatMode) { for (EventListener eventListener : this) { eventListener.onRepeatModeChanged(repeatMode); } } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { for (EventListener eventListener : this) { eventListener.onShuffleModeEnabledChanged(shuffleModeEnabled); } } @Override public void onPlayerError(@NonNull PlaybackException error) { for (EventListener eventListener : this) { eventListener.onPlayerError(error); } } @Override public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { for (EventListener eventListener : this) { eventListener.onPositionDiscontinuity(oldPosition, newPosition, reason); } } @Override public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) { for (EventListener eventListener : this) { eventListener.onPlaybackParametersChanged(playbackParameters); } } @Override public void onCues(@NonNull CueGroup cueGroup) { for (EventListener eventListener : this) { eventListener.onCues(cueGroup); } } @Override public void onMetadata(@NonNull Metadata metadata) { for (EventListener eventListener : this) { eventListener.onMetadata(metadata); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.with; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET; import android.net.Uri; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.ui.PlayerView; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; /** * [20180225] *

* Default implementation of {@link Playable}. *

* Instance of {@link Playable} should be reusable. Retaining instance of Playable across config * change must guarantee that all {@link EventListener} are cleaned up on config change. * * @author eneim (2018/02/25). */ @SuppressWarnings("WeakerAccess") // @OptIn(markerClass = UnstableApi.class) class PlayableImpl implements Playable { private final PlaybackInfo playbackInfo = new PlaybackInfo(); // never expose to outside. protected final EventListeners listeners = new EventListeners(); // original listener. protected final ToroPlayer.VolumeChangeListeners volumeChangeListeners = new ToroPlayer.VolumeChangeListeners(); protected final ToroPlayer.ErrorListeners errorListeners = new ToroPlayer.ErrorListeners(); protected final Uri mediaUri; // immutable, parcelable protected final String fileExt; protected final ExoCreator creator; // required, cached protected ToroExoPlayer player; // on-demand, cached protected MediaSource mediaSource; // on-demand, since we do not reuse MediaSource now. protected PlayerView playerView; // on-demand, not always required. private boolean sourcePrepared = false; private boolean listenerApplied = false; PlayableImpl(ExoCreator creator, Uri uri, String fileExt) { this.creator = creator; this.mediaUri = uri; this.fileExt = fileExt; } @CallSuper @Override public void prepare(boolean prepareSource) { if (prepareSource) { ensureMediaSource(); ensurePlayerView(); } } @CallSuper @Override public void setPlayerView(@Nullable PlayerView playerView) { if (this.playerView == playerView) return; if (playerView == null) { this.playerView.setPlayer(null); } else { if (this.player != null) { PlayerView.switchTargetView(this.player.getPlayer(), this.playerView, playerView); } } this.playerView = playerView; } @Override public final PlayerView getPlayerView() { return this.playerView; } @CallSuper @Override public void play() { ensureMediaSource(); ensurePlayerView(); checkNotNull(player, "Playable#play(): Player is null!"); player.getPlayer().setPlayWhenReady(true); } @CallSuper @Override public void pause() { // Player is not required to be non-null here. if (player != null) player.getPlayer().setPlayWhenReady(false); } @CallSuper @Override public void reset() { this.playbackInfo.reset(); if (player != null) { // reset volume to default ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f)); player.getPlayer().stop(); player.getPlayer().clearMediaItems(); } this.mediaSource = null; // so it will be re-prepared when play() is called. this.sourcePrepared = false; } @CallSuper @Override public void release() { this.setPlayerView(null); if (this.player != null) { // reset volume to default ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f)); player.getPlayer().stop(); player.getPlayer().clearMediaItems(); if (listenerApplied) { player.getPlayer().removeListener(listeners); if (this.player != null) { this.player.removeOnVolumeChangeListener(this.volumeChangeListeners); } listenerApplied = false; } with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) // .releasePlayer(this.creator, this.player.getPlayer()); } this.player = null; this.mediaSource = null; this.sourcePrepared = false; } @CallSuper @NonNull @Override public PlaybackInfo getPlaybackInfo() { updatePlaybackInfo(); return new PlaybackInfo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition(), playbackInfo.getVolumeInfo()); } @CallSuper @Override public void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo) { this.playbackInfo.setResumeWindow(playbackInfo.getResumeWindow()); this.playbackInfo.setResumePosition(playbackInfo.getResumePosition()); this.setVolumeInfo(playbackInfo.getVolumeInfo()); if (player != null) { ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); boolean haveResumePosition = this.playbackInfo.getResumeWindow() != INDEX_UNSET; if (haveResumePosition) { player.getPlayer().seekTo(this.playbackInfo.getResumeWindow(), this.playbackInfo.getResumePosition()); } } } @Override public final void addEventListener(@NonNull EventListener listener) { //noinspection ConstantConditions if (listener != null) this.listeners.add(listener); } @Override public final void removeEventListener(EventListener listener) { this.listeners.remove(listener); } @CallSuper @Override public void setVolume(float volume) { checkNotNull(player, "Playable#setVolume(): Player is null!"); playbackInfo.getVolumeInfo().setTo(volume == 0, volume); ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); } @CallSuper @Override public float getVolume() { return checkNotNull(player.getPlayer(), "Playable#getVolume(): Player is null!").getVolume(); } @Override public boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) { boolean changed = !this.playbackInfo.getVolumeInfo().equals(checkNotNull(volumeInfo)); if (changed) { this.playbackInfo.getVolumeInfo().setTo(volumeInfo.isMute(), volumeInfo.getVolume()); if (player != null) ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); } return changed; } @NonNull @Override public VolumeInfo getVolumeInfo() { return this.playbackInfo.getVolumeInfo(); } @Override public void setParameters(@Nullable PlaybackParameters parameters) { checkNotNull(player.getPlayer(), "Playable#setParameters(PlaybackParameters): Player is null") // .setPlaybackParameters(parameters); } @Override public PlaybackParameters getParameters() { return checkNotNull(player.getPlayer(), "Playable#getParameters(): Player is null").getPlaybackParameters(); } @Override public void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) { volumeChangeListeners.add(checkNotNull(listener)); } @Override public void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener) { volumeChangeListeners.remove(listener); } @Override public boolean isPlaying() { return player != null && player.getPlayer().getPlayWhenReady(); } @Override public void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener) { this.errorListeners.add(checkNotNull(listener)); } @Override public void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener) { this.errorListeners.remove(listener); } final void updatePlaybackInfo() { if (player == null || player.getPlayer().getPlaybackState() == Player.STATE_IDLE) return; playbackInfo.setResumeWindow(player.getPlayer().getCurrentWindowIndex()); playbackInfo.setResumePosition(player.getPlayer().isCurrentWindowSeekable() ? // Math.max(0, player.getPlayer().getCurrentPosition()) : TIME_UNSET); playbackInfo.setVolumeInfo(ToroExo.getVolumeInfo(player)); } private void ensurePlayerView() { if (playerView != null && playerView.getPlayer() != player.getPlayer()) playerView.setPlayer(player.getPlayer()); } // TODO [20180822] Double check this. private void ensureMediaSource() { if (mediaSource == null) { // Only actually prepare the source when play() is called. sourcePrepared = false; mediaSource = creator.createMediaSource(mediaUri, fileExt); } if (!sourcePrepared) { ensurePlayer(); // sourcePrepared is set to false only when player is null. beforePrepareMediaSource(); player.getPlayer().prepare(mediaSource, playbackInfo.getResumeWindow() == C.INDEX_UNSET, false); sourcePrepared = true; } } private void ensurePlayer() { if (player == null) { sourcePrepared = false; player = with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) // .requestPlayer(creator); listenerApplied = false; } if (!listenerApplied) { player.addOnVolumeChangeListener(volumeChangeListeners); player.getPlayer().addListener(listeners); listenerApplied = true; } ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); boolean haveResumePosition = playbackInfo.getResumeWindow() != C.INDEX_UNSET; if (haveResumePosition) { player.getPlayer().seekTo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition()); } } // Trick to inject to the Player creation event. // Required for AdsLoader to set Player. protected void beforePrepareMediaSource() { } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerDispatcher.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; /** * This is an addition layer used in PlayerManager. Setting this where * {@link #getDelayToPlay(ToroPlayer)} returns a positive value will result in a delay in playback * play(). While returning {@link #DELAY_NONE} will dispatch the action immediately, and returning * {@link #DELAY_INFINITE} will not dispatch the action. * * @author eneim (2018/02/24). * @since 3.4.0 */ public interface PlayerDispatcher { int DELAY_INFINITE = -1; int DELAY_NONE = 0; /** * Return the number of milliseconds that a call to {@link ToroPlayer#play()} should be delayed. * Returning {@link #DELAY_INFINITE} will not start the playback, while returning {@link * #DELAY_NONE} will start it immediately. * * @param player the player that is about to play. * @return number of milliseconds to delay the play, or one of {@link #DELAY_INFINITE} or * {@link #DELAY_NONE}. No other negative number should be used. */ int getDelayToPlay(ToroPlayer player); PlayerDispatcher DEFAULT = new PlayerDispatcher() { @Override public int getDelayToPlay(ToroPlayer player) { return DELAY_NONE; } }; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerSelector.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.visibleAreaOffset; import static ml.docilealligator.infinityforreddit.videoautoplay.annotations.Sorted.Order.ASCENDING; import androidx.annotation.NonNull; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.NavigableMap; import java.util.TreeMap; import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Sorted; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; /** * @author eneim | 6/2/17. *

* PlayerSelector is a convenient class to help selecting the players to start Media * playback. *

* On specific event of RecyclerView, such as Child view attached/detached, scroll, the * Collection of players those are available for a playback will change. PlayerSelector is * responded to select a specific number of players from that updated Collection to start a * new playback or pause an old playback if the corresponding Player is not selected * anymore. *

* Client should implement a custom PlayerSelecter and set it to the Container for expected * behaviour. By default, Toro comes with linear selection implementation (the Selector * that will iterate over the Collection and select the players from top to bottom until a * certain condition is fullfilled, for example the maximum of player count is reached). *

* Custom Selector can have more complicated selecting logics, for example: among 2n + 1 * playable widgets, select n players in the middles ... */ @SuppressWarnings("unused") // public interface PlayerSelector { String TAG = "ToroLib:Selector"; /** * Select a collection of {@link ToroPlayer}s to start a playback (if there is non-playing) item. * Playing item are also selected. * * @param container current {@link Container} that holds the players. * @param items a mutable collection of candidate {@link ToroPlayer}s, which are the players * those can start a playback. Items are sorted in order obtained from * {@link ToroPlayer#getPlayerOrder()}. * @return the collection of {@link ToroPlayer}s to start a playback. An on-going playback can be * selected, but it will keep playing. */ @NonNull Collection select(@NonNull Container container, @Sorted(order = ASCENDING) @NonNull List items); /** * The 'reverse' selector of this selector, which can help to select the reversed collection of * that expected by this selector. * For example: this selector will select the first playable {@link ToroPlayer} from top, so the * 'reverse' selector will select the last playable {@link ToroPlayer} from top. * * @return The PlayerSelector that has opposite selecting logic. If there is no special one, * return "this". */ @NonNull PlayerSelector reverse(); PlayerSelector DEFAULT = new PlayerSelector() { @NonNull @Override public Collection select(@NonNull Container container, // @Sorted(order = ASCENDING) @NonNull List items) { int count = items.size(); return count > 0 ? singletonList(items.get(0)) : Collections.emptyList(); } @NonNull @Override public PlayerSelector reverse() { return DEFAULT_REVERSE; } }; PlayerSelector DEFAULT_REVERSE = new PlayerSelector() { @NonNull @Override public Collection select(@NonNull Container container, // @Sorted(order = ASCENDING) @NonNull List items) { int count = items.size(); return count > 0 ? singletonList(items.get(count - 1)) : Collections.emptyList(); } @NonNull @Override public PlayerSelector reverse() { return DEFAULT; } }; @SuppressWarnings("unused") PlayerSelector BY_AREA = new PlayerSelector() { final NavigableMap areas = new TreeMap<>(new Comparator() { @Override public int compare(Float o1, Float o2) { return Float.compare(o2, o1); // reverse order, from high to low. } }); @NonNull @Override public Collection select(@NonNull final Container container, @Sorted(order = ASCENDING) @NonNull List items) { areas.clear(); int count = items.size(); if (count > 0) { for (int i = 0; i < count; i++) { ToroPlayer item = items.get(i); if (!areas.containsValue(item)) areas.put(visibleAreaOffset(item, container), item); } count = areas.size(); } return count > 0 ? singletonList(areas.firstEntry().getValue()) : Collections.emptyList(); } @NonNull @Override public PlayerSelector reverse() { return this; } }; @SuppressWarnings("unused") PlayerSelector NONE = new PlayerSelector() { @NonNull @Override public Collection select(@NonNull Container container, // @Sorted(order = ASCENDING) @NonNull List items) { return emptyList(); } @NonNull @Override public PlayerSelector reverse() { return this; } }; } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static java.lang.Runtime.getRuntime; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.annotation.RestrictTo; import androidx.annotation.StringRes; import androidx.core.util.Pools; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlayer; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; /** * Global helper class to manage {@link ExoCreator} and {@link ExoPlayer} instances. * In this setup, {@link ExoCreator} and ExoPlayer pools are cached. A {@link Config} * is a key for each {@link ExoCreator}. *

* A suggested usage is as below: *


 * ExoCreator creator = ToroExo.with(this).getDefaultCreator();
 * Playable playable = creator.createPlayable(uri);
 * playable.prepare();
 * // next: setup PlayerView and start the playback.
 * 
* * @author eneim (2018/01/26). * @since 3.4.0 */ @UnstableApi public final class ToroExo { private static final String TAG = "ToroExo"; // Magic number: Build.VERSION.SDK_INT / 6 --> API 16 ~ 18 will set pool size to 2, etc. @UnstableApi @SuppressWarnings("WeakerAccess") // static final int MAX_POOL_SIZE = Math.max(Util.SDK_INT / 6, getRuntime().availableProcessors()); @SuppressLint("StaticFieldLeak") // static volatile ToroExo toro; public static ToroExo with(Context context) { if (toro == null) { synchronized (ToroExo.class) { if (toro == null) toro = new ToroExo(context.getApplicationContext()); } } return toro; } @NonNull final String appName; @NonNull final Context context; // Application context @NonNull private final Map creators; @NonNull private final Map> playerPools; private Config defaultConfig; // will be created on the first time it is used. private ToroExo(@NonNull Context context /* Application context */) { this.context = context; this.appName = getUserAgent(); this.playerPools = new HashMap<>(); this.creators = new HashMap<>(); // Adapt from ExoPlayer demo app. Start this on demand. CookieManager cookieManager = new CookieManager(); cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); if (CookieHandler.getDefault() != cookieManager) { CookieHandler.setDefault(cookieManager); } } /** * Utility method to produce {@link ExoCreator} instance from a {@link Config}. */ @OptIn(markerClass = UnstableApi.class) public ExoCreator getCreator(Config config) { ExoCreator creator = this.creators.get(config); if (creator == null) { creator = new DefaultExoCreator(this, config); this.creators.put(config, creator); } return creator; } @SuppressWarnings("WeakerAccess") public Config getDefaultConfig() { if (defaultConfig == null) defaultConfig = new Config.Builder(context).build(); return defaultConfig; } /** * Get the default {@link ExoCreator}. This ExoCreator is configured by {@link #defaultConfig}. */ public ExoCreator getDefaultCreator() { return getCreator(getDefaultConfig()); } /** * Request an instance of {@link ExoPlayer}. It can be an existing instance cached by Pool * or new one. *

* The creator may or may not be the one created by either {@link #getCreator(Config)} or * {@link #getDefaultCreator()}. * * @param creator the {@link ExoCreator} that is scoped to the {@link ExoPlayer} config. * @return an usable {@link ExoPlayer} instance. */ @NonNull // public ToroExoPlayer requestPlayer(@NonNull ExoCreator creator) { ExoPlayer player = getPool(checkNotNull(creator)).acquire(); if (player == null) player = creator.createPlayer(); return new ToroExoPlayer(player); } /** * Release player to Pool attached to the creator. * * @param creator the {@link ExoCreator} that created the player. * @param player the {@link ExoPlayer} to be released back to the Pool * @return true if player is released to relevant Pool, false otherwise. */ @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) // public boolean releasePlayer(@NonNull ExoCreator creator, @NonNull ExoPlayer player) { return getPool(checkNotNull(creator)).release(player); } /** * Release and clear all current cached ExoPlayer instances. This should be called when * client Application runs out of memory ({@link Application#onTrimMemory(int)} for example). */ public void cleanUp() { // TODO [2018/03/07] Test this. Ref: https://stackoverflow.com/a/1884916/1553254 for (Iterator>> it = playerPools.entrySet().iterator(); it.hasNext(); ) { Pools.Pool pool = it.next().getValue(); ExoPlayer item; while ((item = pool.acquire()) != null) item.release(); it.remove(); } } /// internal APIs @OptIn(markerClass = UnstableApi.class) private Pools.Pool getPool(ExoCreator creator) { Pools.Pool pool = playerPools.get(creator); if (pool == null) { pool = new Pools.SimplePool<>(MAX_POOL_SIZE); playerPools.put(creator, pool); } return pool; } /** * Get a possibly-non-localized String from existing resourceId. */ /* pkg */ String getString(@StringRes int resId, @Nullable Object... params) { return params == null || params.length < 1 ? // this.context.getString(resId) : this.context.getString(resId, params); } // Share the code of setting Volume. For use inside library only. @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // public static void setVolumeInfo(@NonNull ToroExoPlayer player, @NonNull VolumeInfo volumeInfo) { player.setVolumeInfo(volumeInfo); } @SuppressWarnings("WeakerAccess") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // public static VolumeInfo getVolumeInfo(ToroExoPlayer player) { return new VolumeInfo(player.getVolumeInfo()); } @SuppressWarnings("SameParameterValue") private static String getUserAgent() { return APIUtils.USER_AGENT; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import android.content.Context; import android.os.Looper; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.LoadControl; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.upstream.BandwidthMeter; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; /** * A custom {@link ExoPlayer} that also notify the change of Volume. * * @author eneim (2018/03/27). */ @SuppressWarnings("WeakerAccess") // public class ToroExoPlayer { private final ExoPlayer player; @OptIn(markerClass = UnstableApi.class) public ToroExoPlayer(Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, Looper looper) { player = new ExoPlayer.Builder(context).setRenderersFactory(renderersFactory).setTrackSelector(trackSelector).setLoadControl(loadControl).setBandwidthMeter(bandwidthMeter).setLooper(looper).build(); } public ToroExoPlayer(ExoPlayer exoPlayer) { this.player = exoPlayer; } private ToroPlayer.VolumeChangeListeners listeners; public final void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) { if (this.listeners == null) this.listeners = new ToroPlayer.VolumeChangeListeners(); this.listeners.add(checkNotNull(listener)); } public final void removeOnVolumeChangeListener(ToroPlayer.OnVolumeChangeListener listener) { if (this.listeners != null) this.listeners.remove(listener); } public final void clearOnVolumeChangeListener() { if (this.listeners != null) this.listeners.clear(); } @CallSuper public void setVolume(float audioVolume) { this.setVolumeInfo(new VolumeInfo(audioVolume == 0, audioVolume)); } private final VolumeInfo volumeInfo = new VolumeInfo(false, 1f); @SuppressWarnings("UnusedReturnValue") public final boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) { boolean changed = !this.volumeInfo.equals(volumeInfo); if (changed) { this.volumeInfo.setTo(volumeInfo.isMute(), volumeInfo.getVolume()); player.setVolume(volumeInfo.isMute() ? 0 : volumeInfo.getVolume()); if (listeners != null) { for (ToroPlayer.OnVolumeChangeListener listener : this.listeners) { listener.onVolumeChanged(volumeInfo); } } } return changed; } @SuppressWarnings("unused") @NonNull public final VolumeInfo getVolumeInfo() { return volumeInfo; } public ExoPlayer getPlayer() { return player; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroPlayer.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.CopyOnWriteArraySet; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; /** * Definition of a Player used in Toro. Besides common playback command ({@link #play()}, {@link * #pause()}, etc), it provides the library necessary information about the playback and * components. * * @author eneim | 5/31/17. */ public interface ToroPlayer { @NonNull View getPlayerView(); @NonNull PlaybackInfo getCurrentPlaybackInfo(); /** * Initialize resource for the incoming playback. After this point, {@link ToroPlayer} should be * able to start the playback at anytime in the future (This doesn't mean that any call to {@link * ToroPlayer#play()} will start the playback immediately. It can start buffering enough resource * before any rendering). * * @param container the RecyclerView contains this Player. * @param playbackInfo initialize info for the preparation. */ void initialize(@NonNull Container container, @NonNull PlaybackInfo playbackInfo); /** * Start playback or resume from a pausing state. */ void play(); /** * Pause current playback. */ void pause(); boolean isPlaying(); /** * Tear down all the setup. This should release all player instances. */ void release(); boolean wantsToPlay(); /** * @return prefer playback order in list. Can be customized. */ int getPlayerOrder(); /** * A convenient callback to help {@link ToroPlayer} to listen to different playback states. */ interface EventListener { void onFirstFrameRendered(); void onBuffering(); // ExoPlayer state: 2 void onPlaying(); // ExoPlayer state: 3, play flag: true void onPaused(); // ExoPlayer state: 3, play flag: false void onCompleted(); // ExoPlayer state: 4 } interface OnVolumeChangeListener { void onVolumeChanged(@NonNull VolumeInfo volumeInfo); } interface OnErrorListener { void onError(Exception error); } class EventListeners extends CopyOnWriteArraySet implements EventListener { @Override public void onFirstFrameRendered() { for (EventListener listener : this) { listener.onFirstFrameRendered(); } } @Override public void onBuffering() { for (EventListener listener : this) { listener.onBuffering(); } } @Override public void onPlaying() { for (EventListener listener : this) { listener.onPlaying(); } } @Override public void onPaused() { for (EventListener listener : this) { listener.onPaused(); } } @Override public void onCompleted() { for (EventListener listener : this) { listener.onCompleted(); } } } class ErrorListeners extends CopyOnWriteArraySet implements ToroPlayer.OnErrorListener { @Override public void onError(Exception error) { for (ToroPlayer.OnErrorListener listener : this) { listener.onError(error); } } } class VolumeChangeListeners extends CopyOnWriteArraySet implements ToroPlayer.OnVolumeChangeListener { @Override public void onVolumeChanged(@NonNull VolumeInfo volumeInfo) { for (ToroPlayer.OnVolumeChangeListener listener : this) { listener.onVolumeChanged(volumeInfo); } } } // Adapt from ExoPlayer. @Retention(RetentionPolicy.SOURCE) // @IntDef({ State.STATE_IDLE, State.STATE_BUFFERING, State.STATE_READY, State.STATE_END }) // @interface State { int STATE_IDLE = 1; int STATE_BUFFERING = 2; int STATE_READY = 3; int STATE_END = 4; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay; import android.graphics.Point; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import androidx.annotation.FloatRange; import androidx.annotation.NonNull; import androidx.coordinatorlayout.widget.CoordinatorLayout; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; /** * @author eneim | 5/31/17. */ public final class ToroUtil { @SuppressWarnings("unused") private static final String TAG = "ToroLib:Util"; private ToroUtil() { throw new RuntimeException("Meh!"); } /** * Get the ratio in range of 0.0 ~ 1.0 the visible area of a {@link ToroPlayer}'s playerView. * * @param player the {@link ToroPlayer} need to investigate. * @param container the {@link ViewParent} that holds the {@link ToroPlayer}. If {@code null} * then this method must returns 0.0f; * @return the value in range of 0.0 ~ 1.0 of the visible area. */ @FloatRange(from = 0.0, to = 1.0) // public static float visibleAreaOffset(@NonNull ToroPlayer player, ViewParent container) { if (container == null) return 0.0f; View playerView = player.getPlayerView(); Rect drawRect = new Rect(); playerView.getDrawingRect(drawRect); int drawArea = drawRect.width() * drawRect.height(); Rect playerRect = new Rect(); boolean visible = playerView.getGlobalVisibleRect(playerRect, new Point()); float offset = 0.f; if (visible && drawArea > 0) { int visibleArea = playerRect.height() * playerRect.width(); offset = visibleArea / (float) drawArea; } return offset; } /** * Ensures that an object reference passed as a parameter to the calling * method is not null. * * @param reference an object reference * @return the non-null reference that was validated * @throws NullPointerException if {@code reference} is null */ public static @NonNull T checkNotNull(final T reference) { if (reference == null) { throw new NullPointerException(); } return reference; } /** * Ensures that an object reference passed as a parameter to the calling * method is not null. * * @param reference an object reference * @param errorMessage the exception message to use if the check fails; will * be converted to a string using {@link String#valueOf(Object)} * @return the non-null reference that was validated * @throws NullPointerException if {@code reference} is null */ public static @NonNull T checkNotNull(final T reference, final Object errorMessage) { if (reference == null) { throw new NullPointerException(String.valueOf(errorMessage)); } return reference; } @SuppressWarnings("unchecked") // public static void wrapParamBehavior(@NonNull final Container container, final Container.BehaviorCallback callback) { container.setBehaviorCallback(callback); ViewGroup.LayoutParams params = container.getLayoutParams(); if (params instanceof CoordinatorLayout.LayoutParams) { CoordinatorLayout.Behavior temp = ((CoordinatorLayout.LayoutParams) params).getBehavior(); if (temp != null) { ((CoordinatorLayout.LayoutParams) params).setBehavior(new Container.Behavior(temp)); } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Beta.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Indicate that the feature is still in Beta testing and may not ready for production. * * @author eneim (2018/02/27). */ @Retention(RetentionPolicy.SOURCE) // public @interface Beta { } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/RemoveIn.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * This annotation is to mark deprecated objects to be removed from library from a certain version, * specific by {@link #version()}. This is to help quickly navigate through them, and to make it clear. * * @author eneim (2018/05/01). */ @Retention(RetentionPolicy.SOURCE) // public @interface RemoveIn { String version(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Sorted.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * @author eneim (2018/02/07). * * Annotate that a list of items are sorted in specific {@link Order}. */ @Retention(RetentionPolicy.SOURCE) // public @interface Sorted { Order order() default Order.ASCENDING; enum Order { ASCENDING, DESCENDING, UNSORTED } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/helper/ToroPlayerHelper.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.helper; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import android.os.Handler; import android.os.Looper; import android.os.Message; import androidx.annotation.CallSuper; import androidx.annotation.FloatRange; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; /** * General definition of a helper class for a specific {@link ToroPlayer}. This class helps * forwarding the playback state to the {@link ToroPlayer} if there is any {@link EventListener} * registered. It also requests the initialization for the Player. * * From 3.4.0, this class can be reused as much as possible. * * @author eneim | 6/11/17. */ public abstract class ToroPlayerHelper { private final Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message msg) { boolean playWhenReady = (boolean) msg.obj; switch (msg.what) { case ToroPlayer.State.STATE_IDLE: // TODO: deal with idle state, maybe error handling. break; case ToroPlayer.State.STATE_BUFFERING /* Player.STATE_BUFFERING */: internalListener.onBuffering(); for (ToroPlayer.EventListener listener : getEventListeners()) { listener.onBuffering(); } break; case ToroPlayer.State.STATE_READY /* Player.STATE_READY */: if (playWhenReady) { internalListener.onPlaying(); } else { internalListener.onPaused(); } for (ToroPlayer.EventListener listener : getEventListeners()) { if (playWhenReady) { listener.onPlaying(); } else { listener.onPaused(); } } break; case ToroPlayer.State.STATE_END /* Player.STATE_ENDED */: internalListener.onCompleted(); for (ToroPlayer.EventListener listener : getEventListeners()) { listener.onCompleted(); } break; default: break; } return true; } }); @NonNull protected final ToroPlayer player; // This instance should be setup from #initialize and cleared from #release protected Container container; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // private ToroPlayer.EventListeners eventListeners; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // private ToroPlayer.VolumeChangeListeners volumeChangeListeners; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // private ToroPlayer.ErrorListeners errorListeners; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // protected final ToroPlayer.EventListener internalListener = new ToroPlayer.EventListener() { @Override public void onFirstFrameRendered() { } @Override public void onBuffering() { // do nothing } @Override public void onPlaying() { player.getPlayerView().setKeepScreenOn(true); } @Override public void onPaused() { player.getPlayerView().setKeepScreenOn(false); if (container != null) { container.savePlaybackInfo( // player.getPlayerOrder(), checkNotNull(player.getCurrentPlaybackInfo())); } } @Override public void onCompleted() { if (container != null) { // Save PlaybackInfo.SCRAP to mark this player to be re-init. container.savePlaybackInfo(player.getPlayerOrder(), PlaybackInfo.SCRAP); } } }; public ToroPlayerHelper(@NonNull ToroPlayer player) { this.player = player; } public final void addPlayerEventListener(@NonNull ToroPlayer.EventListener listener) { getEventListeners().add(checkNotNull(listener)); } public final void removePlayerEventListener(ToroPlayer.EventListener listener) { if (eventListeners != null) eventListeners.remove(listener); } /** * Initialize the necessary resource for the incoming playback. For example, prepare the * ExoPlayer instance for ExoPlayerView. The initialization is feed by an initial playback * info, telling if the playback should start from a specific position or from beginning. * * Normally this info can be obtained from cache if there is cache manager, or {@link PlaybackInfo#SCRAP} * if there is no such cached information. * * @param playbackInfo the initial playback info. */ protected abstract void initialize(@NonNull PlaybackInfo playbackInfo); public final void initialize(@NonNull Container container, @NonNull PlaybackInfo playbackInfo) { this.container = container; this.initialize(playbackInfo); } public abstract void play(); public abstract void pause(); public abstract boolean isPlaying(); /** * @deprecated use {@link #setVolumeInfo(VolumeInfo)} instead. */ @RemoveIn(version = "3.6.0") @Deprecated // public abstract void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume); /** * @deprecated use {@link #getVolumeInfo()} instead. */ @RemoveIn(version = "3.6.0") @Deprecated // public abstract @FloatRange(from = 0.0, to = 1.0) float getVolume(); public abstract void setVolumeInfo(@NonNull VolumeInfo volumeInfo); @NonNull public abstract VolumeInfo getVolumeInfo(); /** * Get latest playback info. Either on-going playback info if current player is playing, or latest * playback info available if player is paused. * * @return latest {@link PlaybackInfo} of current Player. */ @NonNull public abstract PlaybackInfo getLatestPlaybackInfo(); public abstract void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo); @CallSuper public void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) { getVolumeChangeListeners().add(checkNotNull(listener)); } @CallSuper public void removeOnVolumeChangeListener(ToroPlayer.OnVolumeChangeListener listener) { if (volumeChangeListeners != null) volumeChangeListeners.remove(listener); } @CallSuper public void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener) { getErrorListeners().add(checkNotNull(listener)); } @CallSuper public void removeErrorListener(ToroPlayer.OnErrorListener listener) { if (errorListeners != null) errorListeners.remove(listener); } @NonNull protected final ToroPlayer.EventListeners getEventListeners() { if (eventListeners == null) eventListeners = new ToroPlayer.EventListeners(); return eventListeners; } @NonNull protected final ToroPlayer.VolumeChangeListeners getVolumeChangeListeners() { if (volumeChangeListeners == null) { volumeChangeListeners = new ToroPlayer.VolumeChangeListeners(); } return volumeChangeListeners; } @NonNull protected final ToroPlayer.ErrorListeners getErrorListeners() { if (errorListeners == null) errorListeners = new ToroPlayer.ErrorListeners(); return errorListeners; } // Mimic ExoPlayer @CallSuper protected final void onPlayerStateUpdated(boolean playWhenReady, @ToroPlayer.State int playbackState) { handler.obtainMessage(playbackState, playWhenReady).sendToTarget(); } @CallSuper public void release() { handler.removeCallbacksAndMessages(null); this.container = null; } @NonNull @Override public String toString() { return "ToroLib:Helper{" + "player=" + player + ", container=" + container + '}'; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/DrmMedia.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.media; import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** * @author eneim | 6/5/17. * * A definition of DRM media type. */ public interface DrmMedia { // DRM Scheme @NonNull String getType(); @Nullable String getLicenseUrl(); @Nullable String[] getKeyRequestPropertiesArray(); boolean multiSession(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/PlaybackInfo.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.media; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; /** * @author eneim | 6/6/17. */ public class PlaybackInfo implements Parcelable { public static final long TIME_UNSET = Long.MIN_VALUE + 1; public static final int INDEX_UNSET = -1; private int resumeWindow; private long resumePosition; @NonNull private VolumeInfo volumeInfo; public PlaybackInfo(int resumeWindow, long resumePosition) { this.resumeWindow = resumeWindow; this.resumePosition = resumePosition; this.volumeInfo = new VolumeInfo(false, 1.f); } public PlaybackInfo(int resumeWindow, long resumePosition, @NonNull VolumeInfo volumeInfo) { this.resumeWindow = resumeWindow; this.resumePosition = resumePosition; this.volumeInfo = volumeInfo; } public PlaybackInfo() { this(INDEX_UNSET, TIME_UNSET); } public PlaybackInfo(PlaybackInfo other) { this(other.getResumeWindow(), other.getResumePosition(), other.getVolumeInfo()); } public int getResumeWindow() { return resumeWindow; } public void setResumeWindow(int resumeWindow) { this.resumeWindow = resumeWindow; } public long getResumePosition() { return resumePosition; } public void setResumePosition(long resumePosition) { this.resumePosition = resumePosition; } @NonNull public VolumeInfo getVolumeInfo() { return volumeInfo; } public void setVolumeInfo(@NonNull VolumeInfo volumeInfo) { this.volumeInfo = volumeInfo; } public void reset() { resumeWindow = INDEX_UNSET; resumePosition = TIME_UNSET; volumeInfo = new VolumeInfo(false, 1.f); } @Override public String toString() { return this == SCRAP ? "Info:SCRAP" : // "Info{" + "window=" + resumeWindow + ", position=" + resumePosition + ", volume=" + volumeInfo + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PlaybackInfo)) return false; PlaybackInfo that = (PlaybackInfo) o; if (resumeWindow != that.resumeWindow) return false; return resumePosition == that.resumePosition; } @Override public int hashCode() { int result = resumeWindow; result = 31 * result + (int) (resumePosition ^ (resumePosition >>> 32)); return result; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.resumeWindow); dest.writeLong(this.resumePosition); dest.writeParcelable(this.volumeInfo, flags); } protected PlaybackInfo(Parcel in) { this.resumeWindow = in.readInt(); this.resumePosition = in.readLong(); this.volumeInfo = in.readParcelable(VolumeInfo.class.getClassLoader()); } public static final Creator CREATOR = new Creator() { @Override public PlaybackInfo createFromParcel(Parcel source) { return new PlaybackInfo(source); } @Override public PlaybackInfo[] newArray(int size) { return new PlaybackInfo[size]; } }; // A default PlaybackInfo instance, only use this to mark un-initialized players. public static final PlaybackInfo SCRAP = new PlaybackInfo(); } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/VolumeInfo.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.media; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.FloatRange; /** * Information about volume of a playback. There are a few state this class could show: * * - An expected volume value. * - State of mute or not. * * When {@link #mute} is {@code true}, {@link #volume} value will be ignored. But when {@link #mute} * is set to {@code false}, actual volume value will be brought to the playback. * * This volume information doesn't relate to system Volume. Which means that even if client set * this to non-mute volume, the device's volume setup wins the actual behavior. * * @author eneim (2018/03/14). */ public final class VolumeInfo implements Parcelable { // Indicate that the playback is in muted state or not. private boolean mute; // The actual Volume value if 'mute' is false. @FloatRange(from = 0, to = 1) private float volume; public VolumeInfo(boolean mute, @FloatRange(from = 0, to = 1) float volume) { this.mute = mute; this.volume = volume; } public VolumeInfo(VolumeInfo other) { this(other.isMute(), other.getVolume()); } public boolean isMute() { return mute; } public void setMute(boolean mute) { this.mute = mute; } @FloatRange(from = 0, to = 1) public float getVolume() { return volume; } public void setVolume(@FloatRange(from = 0, to = 1) float volume) { this.volume = volume; } public void setTo(boolean mute, @FloatRange(from = 0, to = 1) float volume) { this.mute = mute; this.volume = volume; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeByte(this.mute ? (byte) 1 : (byte) 0); dest.writeFloat(this.volume); } protected VolumeInfo(Parcel in) { this.mute = in.readByte() != 0; this.volume = in.readFloat(); } public static final Creator CREATOR = new ClassLoaderCreator() { @Override public VolumeInfo createFromParcel(Parcel source, ClassLoader loader) { return new VolumeInfo(source); } @Override public VolumeInfo createFromParcel(Parcel source) { return new VolumeInfo(source); } @Override public VolumeInfo[] newArray(int size) { return new VolumeInfo[size]; } }; @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VolumeInfo that = (VolumeInfo) o; if (mute != that.mute) return false; return Float.compare(that.volume, volume) == 0; } @Override public int hashCode() { int result = (mute ? 1 : 0); result = 31 * result + (volume != +0.0f ? Float.floatToIntBits(volume) : 0); return result; } @Override public String toString() { return "Vol{" + "mute=" + mute + ", volume=" + volume + '}'; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Common.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.widget; import android.graphics.Point; import android.graphics.Rect; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.recyclerview.widget.RecyclerView; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; /** * @author eneim | 6/2/17. * * A hub for internal convenient methods. */ @SuppressWarnings({ "unused", "WeakerAccess" }) // @RestrictTo(RestrictTo.Scope.LIBRARY) // final class Common { private static final String TAG = "ToroLib:Common"; // Keep static values to reduce instance initialization. We don't need to access its value. private static final Rect dummyRect = new Rect(); private static final Point dummyPoint = new Point(); interface Filter { boolean accept(T target); } static int compare(int x, int y) { //noinspection UseCompareMethod return (x < y) ? -1 : ((x == y) ? 0 : 1); } static long max(Long... numbers) { List list = Arrays.asList(numbers); return Collections.max(list); } static Comparator ORDER_COMPARATOR = new Comparator() { @Override public int compare(ToroPlayer o1, ToroPlayer o2) { return Common.compare(o1.getPlayerOrder(), o2.getPlayerOrder()); } }; static final Comparator ORDER_COMPARATOR_INT = new Comparator() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }; static boolean allowsToPlay(@NonNull ToroPlayer player) { dummyRect.setEmpty(); dummyPoint.set(0, 0); boolean valid = player instanceof RecyclerView.ViewHolder; // Should be true if (valid) valid = ((RecyclerView.ViewHolder) player).itemView.getParent() != null; if (valid) valid = player.getPlayerView().getGlobalVisibleRect(dummyRect, dummyPoint); return valid; } @Nullable static T findFirst(List source, Filter filter) { for (T t : source) { if (filter.accept(t)) return t; } return null; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Container.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.widget; import static android.content.Context.POWER_SERVICE; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.max; import android.app.Activity; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.os.PowerManager; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.view.WindowInsetsCompat; import androidx.customview.view.AbsSavedState; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager; import ml.docilealligator.infinityforreddit.videoautoplay.PlayerDispatcher; import ml.docilealligator.infinityforreddit.videoautoplay.PlayerSelector; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; /** * A custom {@link RecyclerView} that is capable of managing and controlling the {@link ToroPlayer}s' * playback behaviour. * * A client wish to have the auto playback behaviour should replace the normal use of * {@link RecyclerView} with {@link Container}. * * By default, {@link Container} doesn't support playback position saving/restoring. This is * because {@link Container} has no idea about the uniqueness of media content those are being * played. This can be archived by supplying {@link Container} with a valid {@link CacheManager}. A * {@link CacheManager} will help providing the uniqueness of Medias by which it can correctly * save/restore the playback state of a specific media item. Setup this can be done using * {@link Container#setCacheManager(CacheManager)}. * * {@link Container} uses {@link PlayerSelector} to control the {@link ToroPlayer}. A * {@link PlayerSelector} will be asked to select which {@link ToroPlayer} to start playback, and * those are not selected will be paused. By default, it uses {@link PlayerSelector#DEFAULT}. * Custom {@link PlayerSelector} can be set via {@link Container#setPlayerSelector(PlayerSelector)}. * * @author eneim | 5/31/17. */ @SuppressWarnings({ "unused", "ConstantConditions" }) // public class Container extends RecyclerView { private static final String TAG = "ToroLib:Container"; static final int SOME_BLINKS = 50; // 3 frames ... /* package */ final PlayerManager playerManager; /* package */ final ChildLayoutChangeListener childLayoutChangeListener; /* package */ PlayerDispatcher playerDispatcher = PlayerDispatcher.DEFAULT; /* package */ RecyclerListenerImpl recyclerListener; // null = not attached/detached /* package */ PlayerSelector playerSelector = PlayerSelector.DEFAULT; // null = do nothing /* package */ Handler animatorFinishHandler; // null = not attached/detached /* package */ BehaviorCallback behaviorCallback; public Container(Context context) { this(context, null); } public Container(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public Container(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); playerManager = new PlayerManager(); childLayoutChangeListener = new ChildLayoutChangeListener(this); requestDisallowInterceptTouchEvent(true); } @Override public final void setRecyclerListener(RecyclerListener listener) { if (recyclerListener == null) recyclerListener = new RecyclerListenerImpl(this); recyclerListener.delegate = listener; super.setRecyclerListener(recyclerListener); } @CallSuper @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (getAdapter() != null) dataObserver.registerAdapter(getAdapter()); if (animatorFinishHandler == null) { animatorFinishHandler = new Handler(new AnimatorHelper(this)); } PowerManager powerManager = (PowerManager) getContext().getSystemService(POWER_SERVICE); if (powerManager != null && powerManager.isScreenOn()) { this.screenState = View.SCREEN_STATE_ON; } else { this.screenState = View.SCREEN_STATE_OFF; } /* setRecyclerListener can be called before this, it is considered as user-setup */ if (recyclerListener == null) { recyclerListener = new RecyclerListenerImpl(this); recyclerListener.delegate = NULL; // mark as it is set by Toro, not user. super.setRecyclerListener(recyclerListener); // must be a super call } playbackInfoCache.onAttach(); playerManager.onAttach(); ViewGroup.LayoutParams params = getLayoutParams(); if (params instanceof CoordinatorLayout.LayoutParams) { CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior(); if (behavior instanceof Behavior) { ((Behavior) behavior).onViewAttached(this); } } } @CallSuper @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); ViewGroup.LayoutParams params = getLayoutParams(); if (params instanceof CoordinatorLayout.LayoutParams) { CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior(); if (behavior instanceof Behavior) { ((Behavior) behavior).onViewDetached(this); } } if (recyclerListener != null && recyclerListener.delegate == NULL) { // set by Toro, not user. super.setRecyclerListener(null); // must be a super call recyclerListener = null; } if (animatorFinishHandler != null) { animatorFinishHandler.removeCallbacksAndMessages(null); animatorFinishHandler = null; } List players = playerManager.getPlayers(); if (!players.isEmpty()) { for (int size = players.size(), i = size - 1; i >= 0; i--) { ToroPlayer player = players.get(i); if (player.isPlaying()) { this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); playerManager.pause(player); } playerManager.release(player); } playerManager.clear(); } playerManager.onDetach(); playbackInfoCache.onDetach(); dataObserver.registerAdapter(null); childLayoutChangeListener.containerRef.clear(); } /** * Filter current managed {@link ToroPlayer}s using {@link Filter}. Result is sorted by Player * order obtained from {@link ToroPlayer#getPlayerOrder()}. * * @param filter the {@link Filter} to a {@link ToroPlayer}. * @return list of players accepted by {@link Filter}. Empty list if there is no available player. */ @NonNull public final List filterBy(Filter filter) { List result = new ArrayList<>(); for (ToroPlayer player : playerManager.getPlayers()) { if (filter.accept(player)) result.add(player); } Collections.sort(result, Common.ORDER_COMPARATOR); return result; } // This method is called when: // [1] A ViewHolder is newly created, bound and then attached to RecyclerView. // [2] A ViewHolder is detached before, but still in bound state, not be recycled, // and now be re-attached to RecyclerView. // In either cases, PlayerManager should not manage the ViewHolder before this point. @CallSuper @Override public void onChildAttachedToWindow(@NonNull final View child) { super.onChildAttachedToWindow(child); child.addOnLayoutChangeListener(childLayoutChangeListener); final ViewHolder holder = getChildViewHolder(child); if (!(holder instanceof ToroPlayer)) return; final ToroPlayer player = (ToroPlayer) holder; final View playerView = player.getPlayerView(); if (playerView == null) { throw new NullPointerException("Expected non-null playerView, found null for: " + player); } playbackInfoCache.onPlayerAttached(player); if (playerManager.manages(player)) { // I don't expect this to be called. If this happens, make sure to note the scenario. Log.w(TAG, "!!Already managed: player = [" + player + "]"); // Only if container is in idle state and player is not playing. if (getScrollState() == SCROLL_STATE_IDLE && !player.isPlaying()) { playerManager.play(player, playerDispatcher); } } else { // LeakCanary report a leak of OnGlobalLayoutListener but I cannot figure out why ... child.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { child.getViewTreeObserver().removeOnGlobalLayoutListener(this); if (Common.allowsToPlay(player)) { if (playerManager.attachPlayer(player)) { dispatchUpdateOnAnimationFinished(false); } } } }); } } @CallSuper @Override public void onChildDetachedFromWindow(@NonNull View child) { super.onChildDetachedFromWindow(child); child.removeOnLayoutChangeListener(childLayoutChangeListener); ViewHolder holder = getChildViewHolder(child); if (!(holder instanceof ToroPlayer)) return; final ToroPlayer player = (ToroPlayer) holder; boolean playerManaged = playerManager.manages(player); if (player.isPlaying()) { if (!playerManaged) { player.pause(); // Unstable state, so forcefully pause this by itself. /* throw new IllegalStateException( "Player is playing while it is not in managed state: " + player); */ } this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); playerManager.pause(player); } if (playerManaged) { playerManager.detachPlayer(player); } playbackInfoCache.onPlayerDetached(player); // RecyclerView#onChildDetachedFromWindow(View) is called after other removal finishes, so // sometime it happens after all Animation, but we also need to update playback here. // If there is no anymore child view, this call will end early. dispatchUpdateOnAnimationFinished(true); // finally release the player // if player manager could not manager player, release by itself. if (!playerManager.release(player)) player.release(); } @CallSuper @Override public void onScrollStateChanged(int state) { // Need to handle the dead playback even when the Container is still scrolling/flinging. List players = playerManager.getPlayers(); // 1. Find players those are managed but not qualified to play anymore. for (int i = 0, size = players.size(); i < size; i++) { ToroPlayer player = players.get(i); if (Common.allowsToPlay(player)) continue; if (player.isPlaying()) { this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); playerManager.pause(player); } if (!playerManager.release(player)) player.release(); playerManager.detachPlayer(player); } // 2. Refresh the good players list. LayoutManager layout = super.getLayoutManager(); // current number of visible 'Virtual Children', or zero if there is no LayoutManager available. int childCount = layout != null ? layout.getChildCount() : 0; if (childCount <= 0 || state != SCROLL_STATE_IDLE) { playerManager.deferPlaybacks(); return; } for (int i = 0; i < childCount; i++) { View child = layout.getChildAt(i); ViewHolder holder = super.getChildViewHolder(child); if (holder instanceof ToroPlayer) { ToroPlayer player = (ToroPlayer) holder; // Check candidate's condition if (Common.allowsToPlay(player)) { if (!playerManager.manages(player)) { playerManager.attachPlayer(player); } // Don't check the attach result, because the player may be managed already. if (!player.isPlaying()) { // not playing or not ready to play. playerManager.initialize(player, Container.this); } } } } final List source = playerManager.getPlayers(); int count = source.size(); if (count < 1) return; // No available player, return. List candidates = new ArrayList<>(); for (int i = 0; i < count; i++) { ToroPlayer player = source.get(i); if (player.wantsToPlay()) candidates.add(player); } Collections.sort(candidates, Common.ORDER_COMPARATOR); Collection toPlay = playerSelector != null ? playerSelector.select(this, candidates) : Collections.emptyList(); for (ToroPlayer player : toPlay) { if (!player.isPlaying()) playerManager.play(player, playerDispatcher); } source.removeAll(toPlay); // Now 'source' contains only ones need to be paused. for (ToroPlayer player : source) { if (player.isPlaying()) { this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); playerManager.pause(player); } } } /** * Setup a {@link PlayerSelector}. Set a {@code null} {@link PlayerSelector} will stop all * playback. * * @param playerSelector new {@link PlayerSelector} for this {@link Container}. */ public final void setPlayerSelector(@Nullable PlayerSelector playerSelector) { if (this.playerSelector == playerSelector) return; this.playerSelector = playerSelector; // dispatchUpdateOnAnimationFinished(true); // doesn't work well :( // Immediately update. this.onScrollStateChanged(SCROLL_STATE_IDLE); } /** * Get current {@link PlayerSelector}. Can be {@code null}. * * @return current {@link #playerSelector} */ @Nullable public final PlayerSelector getPlayerSelector() { return playerSelector; } public final void setPlayerDispatcher(@NonNull PlayerDispatcher playerDispatcher) { this.playerDispatcher = checkNotNull(playerDispatcher); } /** Define the callback that to be used later by {@link Behavior} if setup. */ public final void setBehaviorCallback(@Nullable BehaviorCallback behaviorCallback) { this.behaviorCallback = behaviorCallback; } ////// Handle update after data change animation long getMaxAnimationDuration() { ItemAnimator animator = getItemAnimator(); if (animator == null) return SOME_BLINKS; return max(animator.getAddDuration(), animator.getMoveDuration(), animator.getRemoveDuration(), animator.getChangeDuration()); } void dispatchUpdateOnAnimationFinished(boolean immediate) { if (getScrollState() != SCROLL_STATE_IDLE) return; if (animatorFinishHandler == null) return; final long duration = immediate ? SOME_BLINKS : getMaxAnimationDuration(); if (getItemAnimator() != null) { getItemAnimator().isRunning(new ItemAnimator.ItemAnimatorFinishedListener() { @Override public void onAnimationsFinished() { animatorFinishHandler.removeCallbacksAndMessages(null); animatorFinishHandler.sendEmptyMessageDelayed(-1, duration); } }); } else { animatorFinishHandler.removeCallbacksAndMessages(null); animatorFinishHandler.sendEmptyMessageDelayed(-1, duration); } } ////// Adapter Data Observer setup /** * See {@link ToroDataObserver} */ private final ToroDataObserver dataObserver = new ToroDataObserver(); /** * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)} * See {@link Adapter#unregisterAdapterDataObserver(AdapterDataObserver)} */ @CallSuper @Override public void setAdapter(Adapter adapter) { super.setAdapter(adapter); dataObserver.registerAdapter(adapter); } /** * See {@link Container#setAdapter(Adapter)} */ @CallSuper @Override public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) { super.swapAdapter(adapter, removeAndRecycleExistingViews); dataObserver.registerAdapter(adapter); } //// PlaybackInfo Cache implementation /* pkg */ final PlaybackInfoCache playbackInfoCache = new PlaybackInfoCache(this); /* pkg */ Initializer playerInitializer = Initializer.DEFAULT; private CacheManager cacheManager = null; // null by default public final void setPlayerInitializer(@NonNull Initializer playerInitializer) { this.playerInitializer = playerInitializer; } /** * Save {@link PlaybackInfo} for the current {@link ToroPlayer} of a specific order. * If called with {@link PlaybackInfo#SCRAP}, it is a hint that the Player is completed and need * to be re-initialized. * * @param order order of the {@link ToroPlayer}. * @param playbackInfo current {@link PlaybackInfo} of the {@link ToroPlayer}. Null info will be ignored. */ public final void savePlaybackInfo(int order, @Nullable PlaybackInfo playbackInfo) { if (playbackInfo != null) playbackInfoCache.savePlaybackInfo(order, playbackInfo); } /** * Get the cached {@link PlaybackInfo} at a specific order. * * @param order order of the {@link ToroPlayer} to get the cached {@link PlaybackInfo}. * @return cached {@link PlaybackInfo} if available, a new one if there is no cached one. */ @NonNull public final PlaybackInfo getPlaybackInfo(int order) { return playbackInfoCache.getPlaybackInfo(order); } /** * Get current list of {@link ToroPlayer}s' orders whose {@link PlaybackInfo} are cached. * Returning an empty list will disable the save/restore of player's position. * * @return list of {@link ToroPlayer}s' orders. * @deprecated Use {@link #getLatestPlaybackInfos()} for the same purpose. */ @RemoveIn(version = "3.6.0") @Deprecated // @NonNull public List getSavedPlayerOrders() { return new ArrayList<>(playbackInfoCache.coldKeyToOrderMap.keySet()); } /** * Get a {@link SparseArray} contains cached {@link PlaybackInfo} of {@link ToroPlayer}s managed * by this {@link Container}. If there is non-null {@link CacheManager}, this method should * return the list of all {@link PlaybackInfo} cached by {@link PlaybackInfoCache}, otherwise, * this method returns current {@link PlaybackInfo} of attached {@link ToroPlayer}s only. */ @NonNull public SparseArray getLatestPlaybackInfos() { SparseArray cache = new SparseArray<>(); List activePlayers = this.filterBy(Container.Filter.PLAYING); // This will update hotCache and coldCache if they are available. for (ToroPlayer player : activePlayers) { this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); } if (cacheManager == null) { if (playbackInfoCache.hotCache != null) { for (Map.Entry entry : playbackInfoCache.hotCache.entrySet()) { cache.put(entry.getKey(), entry.getValue()); } } } else { for (Map.Entry entry : playbackInfoCache.coldKeyToOrderMap.entrySet()) { cache.put(entry.getKey(), playbackInfoCache.coldCache.get(entry.getValue())); } } return cache; } /** * Set a {@link CacheManager} to this {@link Container}. A {@link CacheManager} will * allow this {@link Container} to save/restore {@link PlaybackInfo} on various states or life * cycle events. Setting a {@code null} {@link CacheManager} will remove that ability. * {@link Container} doesn't have a non-null {@link CacheManager} by default. * * Setting this while there is a {@code non-null} {@link CacheManager} available will clear * current {@link PlaybackInfo} cache. * * @param cacheManager The {@link CacheManager} to set to the {@link Container}. */ public final void setCacheManager(@Nullable CacheManager cacheManager) { if (this.cacheManager == cacheManager) return; this.playbackInfoCache.clearCache(); this.cacheManager = cacheManager; } /** * Get current {@link CacheManager} of the {@link Container}. * * @return current {@link CacheManager} of the {@link Container}. Can be {@code null}. */ @Nullable public final CacheManager getCacheManager() { return cacheManager; } /** * Temporary save current playback infos when the App is stopped but not re-created. (For example: * User press App Stack). If not {@code empty} then user is back from a living-but-stopped state. */ final SparseArray tmpStates = new SparseArray<>(); /** * In case user press "App Stack" button, this View's window will have visibility change from * {@link #VISIBLE} to {@link #INVISIBLE} to {@link #GONE}. When user is back from that state, * the visibility changes from {@link #GONE} to {@link #INVISIBLE} to {@link #VISIBLE}. A proper * playback needs to handle this case too. */ @CallSuper @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); if (visibility == View.GONE) { List players = playerManager.getPlayers(); // if onSaveInstanceState is called before, source will contain no item, just fine. for (ToroPlayer player : players) { if (player.isPlaying()) { this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); playerManager.pause(player); } } } else if (visibility == View.VISIBLE) { if (tmpStates.size() > 0) { for (int i = 0; i < tmpStates.size(); i++) { int order = tmpStates.keyAt(i); PlaybackInfo playbackInfo = tmpStates.get(order); this.savePlaybackInfo(order, playbackInfo); } } tmpStates.clear(); dispatchUpdateOnAnimationFinished(true); } dispatchWindowVisibilityMayChange(); } private int screenState; @Override public void onScreenStateChanged(int screenState) { super.onScreenStateChanged(screenState); this.screenState = screenState; dispatchWindowVisibilityMayChange(); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); dispatchWindowVisibilityMayChange(); } /** * This method supports the case that by some reasons, Container should changes it behaviour not * caused by any Activity recreation (so {@link #onSaveInstanceState()} and * {@link #onRestoreInstanceState(Parcelable)} could not help). * * This method is called when: * - Screen state changed. * or - Window focus changed. * or - Window visibility changed. * * For each of that event, Screen may be turned off or Window's focus state may change, we need * to decide if Container should keep current playback state or change it. * * Discussion: In fact, we expect that: Container will be playing if the * following conditions are all satisfied: * - Current window is visible. (but not necessarily focused). * - Container is visible in Window (partly is fine, we care about the Media player). * - Container is focused in Window. (so we don't screw up other components' focuses). * * In lower API (eg: 16), {@link #getWindowVisibility()} always returns {@link #VISIBLE}, which * cannot tell much. We need to investigate this flag in various APIs in various Scenarios. */ private void dispatchWindowVisibilityMayChange() { if (screenState == SCREEN_STATE_OFF) { List players = playerManager.getPlayers(); for (ToroPlayer player : players) { if (player.isPlaying()) { this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); playerManager.pause(player); } } } else if (screenState == SCREEN_STATE_ON // Container is focused in current Window && hasFocus() // In fact, Android 24+ supports multi-window mode in which visible Window may not have focus. // In that case, other triggers are supposed to be called and we are safe here. // Need further investigation if need. && hasWindowFocus()) { // tmpStates may be consumed already, if there is a good reason for that, so not a big deal. if (tmpStates.size() > 0) { for (int i = 0, size = tmpStates.size(); i < size; i++) { int order = tmpStates.keyAt(i); this.savePlaybackInfo(order, tmpStates.get(order)); } } tmpStates.clear(); dispatchUpdateOnAnimationFinished(true); } } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); List source = playerManager.getPlayers(); for (ToroPlayer player : source) { if (player.isPlaying()) { this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo()); playerManager.pause(player); } } final SparseArray states = playbackInfoCache.saveStates(); boolean recreating = getContext() instanceof Activity && ((Activity) getContext()).isChangingConfigurations(); // Release current players on recreation event only. // Note that there are cases where this method is called without the activity destroying/recreating. // For example: in API 26 (my test mostly run on 8.0), when user click to "Current App" button, // current Activity will enter the "Stop" state but not be destroyed/recreated and View hierarchy // state will be saved (this method is called). // // We only need to release current resources when the recreation happens. if (recreating) { for (ToroPlayer player : source) { if (!playerManager.release(player)) player.release(); playerManager.detachPlayer(player); } } // Client must consider this behavior using CacheManager implement. PlayerViewState playerViewState = new PlayerViewState(superState); playerViewState.statesCache = states; // To mark that this method was called. An activity recreation will clear this. if (states != null && states.size() > 0) { for (int i = 0; i < states.size(); i++) { PlaybackInfo value = states.valueAt(i); if (value != null) tmpStates.put(states.keyAt(i), value); } } return playerViewState; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof PlayerViewState)) { super.onRestoreInstanceState(state); return; } PlayerViewState viewState = (PlayerViewState) state; super.onRestoreInstanceState(viewState.getSuperState()); SparseArray saveStates = viewState.statesCache; if (saveStates != null) playbackInfoCache.restoreStates(saveStates); } /** * Store the array of {@link PlaybackInfo} of recently cached playback. This state will be used * only when {@link #cacheManager} is not {@code null}. Extension of {@link Container} must * also have its own version of {@link SavedState} extends this {@link PlayerViewState}. */ public static class PlayerViewState extends AbsSavedState { SparseArray statesCache; /** * Called by onSaveInstanceState */ PlayerViewState(Parcelable superState) { super(superState); } /** * Called by CREATOR */ PlayerViewState(Parcel in, ClassLoader loader) { super(in, loader); statesCache = in.readSparseArray(loader); } PlayerViewState(Parcel in) { super(in); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); //noinspection unchecked dest.writeSparseArray((SparseArray) statesCache); } public static final Creator CREATOR = new ClassLoaderCreator() { // Added from API 13 @Override public PlayerViewState createFromParcel(Parcel in, ClassLoader loader) { return new PlayerViewState(in, loader); } @Override public PlayerViewState createFromParcel(Parcel source) { return new PlayerViewState(source); } @Override public PlayerViewState[] newArray(int size) { return new PlayerViewState[size]; } }; @NonNull @Override public String toString() { return "Cache{" + "states=" + statesCache + '}'; } } private final class ToroDataObserver extends AdapterDataObserver { private Adapter adapter; ToroDataObserver() { } void registerAdapter(Adapter adapter) { if (this.adapter == adapter) return; if (this.adapter != null) { this.adapter.unregisterAdapterDataObserver(this); this.adapter.unregisterAdapterDataObserver(playbackInfoCache); } this.adapter = adapter; if (this.adapter != null) { this.adapter.registerAdapterDataObserver(this); this.adapter.registerAdapterDataObserver(playbackInfoCache); } } @Override public void onChanged() { dispatchUpdateOnAnimationFinished(true); } @Override public void onItemRangeChanged(int positionStart, int itemCount) { dispatchUpdateOnAnimationFinished(false); } @Override public void onItemRangeInserted(int positionStart, int itemCount) { dispatchUpdateOnAnimationFinished(false); } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { dispatchUpdateOnAnimationFinished(false); } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { dispatchUpdateOnAnimationFinished(false); } } /** * A {@link Handler.Callback} that will fake a scroll with {@link #SCROLL_STATE_IDLE} to refresh * all the playback. This is relatively expensive. */ private static class AnimatorHelper implements Handler.Callback { @NonNull private final Container container; AnimatorHelper(@NonNull Container container) { this.container = container; } @Override public boolean handleMessage(Message msg) { this.container.onScrollStateChanged(SCROLL_STATE_IDLE); return true; } } private static class RecyclerListenerImpl implements RecyclerView.RecyclerListener { final Container container; RecyclerListener delegate; RecyclerListenerImpl(@NonNull Container container) { this.container = container; } @Override public void onViewRecycled(@NonNull ViewHolder holder) { if (this.delegate != null) this.delegate.onViewRecycled(holder); if (holder instanceof ToroPlayer) { ToroPlayer player = (ToroPlayer) holder; this.container.playbackInfoCache.onPlayerRecycled(player); this.container.playerManager.recycle(player); } } } // This instance is to mark a RecyclerListenerImpl to be set by Toro, not by user. private static final RecyclerListener NULL = new RecyclerListener() { @Override public void onViewRecycled(@NonNull ViewHolder holder) { // No-ops } }; /** * An utility interface, used by {@link Container} to filter for {@link ToroPlayer}. */ public interface Filter { /** * Check a {@link ToroPlayer} for a condition. * * @param player the {@link ToroPlayer} to check. * @return {@code true} if this accepts the {@link ToroPlayer}, {@code false} otherwise. */ boolean accept(@NonNull ToroPlayer player); /** * A built-in {@link Filter} that accepts only {@link ToroPlayer} that is playing. */ Filter PLAYING = new Filter() { @Override public boolean accept(@NonNull ToroPlayer player) { return player.isPlaying(); } }; /** * A built-in {@link Filter} that accepts only {@link ToroPlayer} that is managed by Container. * Actually any {@link ToroPlayer} to be filtered is already managed. */ Filter MANAGING = new Filter() { @Override public boolean accept(@NonNull ToroPlayer player) { return true; } }; } /** * This behaviour is to catch the touch/fling/scroll caused by other children of * {@link CoordinatorLayout}. We try to acknowledge user actions by intercepting the call but not * consume the events. * * This class helps solve the issue when Client has a {@link Container} inside a * {@link CoordinatorLayout} together with an {@link AppBarLayout} whose direct child is a * {@link CollapsingToolbarLayout} (which is 'scrollable'). This 'scroll behavior' is not the * same as Container's natural scroll. When user 'scrolls' to collapse or expand the {@link * AppBarLayout}, {@link CoordinatorLayout} will offset the {@link Container} to make room for * {@link AppBarLayout}, in which {@link Container} will not receive any scrolling event update, * but just be shifted along the scrolling axis. This behavior results in a bad case that after * the AppBarLayout collapse its direct {@link CollapsingToolbarLayout}, the Video may be fully * visible, but because the Container has no way to know about that event, there is no playback * update. * * @since 3.4.2 */ public static class Behavior extends CoordinatorLayout.Behavior implements Handler.Callback { @NonNull final CoordinatorLayout.Behavior delegate; @Nullable BehaviorCallback callback; static final int EVENT_IDLE = 1; static final int EVENT_SCROLL = 2; static final int EVENT_TOUCH = 3; static final int EVENT_DELAY = 150; final AtomicBoolean scrollConsumed = new AtomicBoolean(false); Handler handler; void onViewAttached(Container container) { if (handler == null) handler = new Handler(this); this.callback = container.behaviorCallback; } void onViewDetached(Container container) { if (handler != null) { handler.removeCallbacksAndMessages(null); handler = null; } this.callback = null; } @Override public boolean handleMessage(Message msg) { if (callback == null) return true; switch (msg.what) { case EVENT_SCROLL: case EVENT_TOUCH: scrollConsumed.set(false); handler.removeMessages(EVENT_IDLE); handler.sendEmptyMessageDelayed(EVENT_IDLE, EVENT_DELAY); break; case EVENT_IDLE: // idle --> consume it. if (!scrollConsumed.getAndSet(true)) callback.onFinishInteraction(); break; } return true; } /* No default constructors. Using this class from xml will result in error. */ // public Behavior() { // } // // public Behavior(Context context, AttributeSet attrs) { // super(context, attrs); // } public Behavior(@NonNull CoordinatorLayout.Behavior delegate) { this.delegate = checkNotNull(delegate, "Behavior is null."); } /// We only need to intercept the following 3 methods: @Override public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull Container child, @NonNull MotionEvent ev) { if (this.handler != null) { this.handler.removeCallbacksAndMessages(null); this.handler.sendEmptyMessage(EVENT_TOUCH); } return delegate.onInterceptTouchEvent(parent, child, ev); } @Override public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull Container child, @NonNull MotionEvent ev) { if (this.handler != null) { this.handler.removeCallbacksAndMessages(null); this.handler.sendEmptyMessage(EVENT_TOUCH); } return delegate.onTouchEvent(parent, child, ev); } @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { if (this.handler != null) { this.handler.removeCallbacksAndMessages(null); this.handler.sendEmptyMessage(EVENT_SCROLL); } return delegate.onStartNestedScroll(layout, child, directTargetChild, target, axes, type); } /// Other methods @Override public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) { if (handler == null) handler = new Handler(this); delegate.onAttachedToLayoutParams(params); } @Override public void onDetachedFromLayoutParams() { if (handler != null) { handler.removeCallbacksAndMessages(null); handler = null; } delegate.onDetachedFromLayoutParams(); } @Override @ColorInt public int getScrimColor(@NonNull CoordinatorLayout parent, @NonNull Container child) { return delegate.getScrimColor(parent, child); } @Override public float getScrimOpacity(@NonNull CoordinatorLayout parent, @NonNull Container child) { return delegate.getScrimOpacity(parent, child); } @Override public boolean blocksInteractionBelow(@NonNull CoordinatorLayout parent, @NonNull Container child) { return delegate.blocksInteractionBelow(parent, child); } @Override public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull Container child, @NonNull View dependency) { return delegate.layoutDependsOn(parent, child, dependency); } @Override public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull Container child, @NonNull View dependency) { return delegate.onDependentViewChanged(parent, child, dependency); } @Override public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull Container child, @NonNull View dependency) { delegate.onDependentViewRemoved(parent, child, dependency); } @Override public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull Container child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { return delegate.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); } @Override public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull Container child, int layoutDirection) { return delegate.onLayoutChild(parent, child, layoutDirection); } @Override public void onNestedScrollAccepted(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { delegate.onNestedScrollAccepted(layout, child, directTargetChild, target, axes, type); } @Override public void onStopNestedScroll(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull View target, int type) { delegate.onStopNestedScroll(layout, child, target, type); } @Override public void onNestedScroll(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { delegate.onNestedScroll(layout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { delegate.onNestedPreScroll(layout, child, target, dx, dy, consumed, type); } @Override public boolean onNestedFling(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull View target, float velocityX, float velocityY, boolean consumed) { return delegate.onNestedFling(layout, child, target, velocityX, velocityY, consumed); } @Override public boolean onNestedPreFling(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull View target, float velocityX, float velocityY) { return delegate.onNestedPreFling(layout, child, target, velocityX, velocityY); } @Override @NonNull public WindowInsetsCompat onApplyWindowInsets(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull WindowInsetsCompat insets) { return delegate.onApplyWindowInsets(layout, child, insets); } @Override public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout layout, @NonNull Container child, @NonNull Rect rectangle, boolean immediate) { return delegate.onRequestChildRectangleOnScreen(layout, child, rectangle, immediate); } @Override public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull Container child, @NonNull Parcelable state) { delegate.onRestoreInstanceState(parent, child, state); } @Override public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull Container child) { return delegate.onSaveInstanceState(parent, child); } @Override public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull Container child, @NonNull Rect rect) { return delegate.getInsetDodgeRect(parent, child, rect); } } /** * Callback for {@link Behavior} to tell the Client that User has finished the interaction for * enough amount of time, so it (the Client) should do something. Normally, we ask Container to * dispatch an 'idle scroll' to refresh the player list. */ public interface BehaviorCallback { void onFinishInteraction(); } public interface Initializer { @NonNull PlaybackInfo initPlaybackInfo(int order); Initializer DEFAULT = new Initializer() { @NonNull @Override public PlaybackInfo initPlaybackInfo(int order) { return new PlaybackInfo(); } }; } static class ChildLayoutChangeListener implements OnLayoutChangeListener { final WeakReference containerRef; ChildLayoutChangeListener(Container container) { this.containerRef = new WeakReference<>(container); } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { Container container = containerRef.get(); if (container == null) return; if (layoutDidChange(left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)) { container.dispatchUpdateOnAnimationFinished(false); } } } static boolean layoutDidChange(int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { return left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlaybackInfoCache.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.widget; import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.SCRAP; import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.ORDER_COMPARATOR_INT; import android.annotation.SuppressLint; import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil; import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo; /** * @author eneim (2018/04/24). * * Design Target: * * [1] Manage the {@link PlaybackInfo} of current {@link ToroPlayer}s. Should match 1-1 with the * {@link ToroPlayer}s that {@link PlayerManager} is managing. * * [2] If a non-null {@link CacheManager} provided to the {@link Container}, this class must * properly manage the {@link PlaybackInfo} of detached {@link ToroPlayer} and restore it to * previous state after being re-attached. */ @SuppressWarnings({ "unused" }) @SuppressLint("UseSparseArrays") // final class PlaybackInfoCache extends RecyclerView.AdapterDataObserver { @NonNull private final Container container; // Cold cache represents the map between key obtained from CacheManager and PlaybackInfo. If the // CacheManager is null, this cache will hold nothing. /* pkg */ HashMap coldCache = new HashMap<>(); // Hot cache represents the map between Player's order and its PlaybackInfo. A key-value map only // lives within a Player's attached state. // Being a TreeMap because we need to traversal through it in order sometime. /* pkg */ TreeMap hotCache; // only cache attached Views. // Holds the map between Player's order and its key obtain from CacheManager. /* pkg */ TreeMap coldKeyToOrderMap = new TreeMap<>(ORDER_COMPARATOR_INT); PlaybackInfoCache(@NonNull Container container) { this.container = container; } void onAttach() { hotCache = new TreeMap<>(ORDER_COMPARATOR_INT); } void onDetach() { if (hotCache != null) { hotCache.clear(); hotCache = null; } coldKeyToOrderMap.clear(); } void onPlayerAttached(ToroPlayer player) { int playerOrder = player.getPlayerOrder(); // [1] Check if there is cold cache for this player Object key = getKey(playerOrder); if (key != null) coldKeyToOrderMap.put(playerOrder, key); PlaybackInfo cache = key == null ? null : coldCache.get(key); if (cache == null || cache == SCRAP) { // We init this even if there is no CacheManager available, because this is what User expects. cache = container.playerInitializer.initPlaybackInfo(playerOrder); // Only save to cold cache when there is a valid CacheManager (key is not null). if (key != null) coldCache.put(key, cache); } if (hotCache != null) hotCache.put(playerOrder, cache); } // Will be called from Container#onChildViewDetachedFromWindow(View) // Therefore, it may not be called on all views. For example: when user close the App, by default // when RecyclerView is detached from Window, it will not call onChildViewDetachedFromWindow for // its children. // This method will: // [1] Take current hot cache entry of the player, and put back to cold cache. // [2] Remove the hot cache entry of the player. void onPlayerDetached(ToroPlayer player) { int playerOrder = player.getPlayerOrder(); if (hotCache != null && hotCache.containsKey(playerOrder)) { PlaybackInfo cache = hotCache.remove(playerOrder); Object key = getKey(playerOrder); if (key != null) coldCache.put(key, cache); } } @SuppressWarnings("unused") void onPlayerRecycled(ToroPlayer player) { // TODO do anything here? } /// Adapter change events handling @Override public void onChanged() { if (container.getCacheManager() != null) { for (Integer key : coldKeyToOrderMap.keySet()) { Object cacheKey = getKey(key); coldCache.put(cacheKey, SCRAP); coldKeyToOrderMap.put(key, cacheKey); } } if (hotCache != null) { for (Integer key : hotCache.keySet()) { hotCache.put(key, SCRAP); } } } @Override public void onItemRangeChanged(final int positionStart, final int itemCount) { if (itemCount == 0) return; if (container.getCacheManager() != null) { Set changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : coldKeyToOrderMap.keySet()) { if (key >= positionStart && key < positionStart + itemCount) { changedColdKeys.add(key); } } for (Integer key : changedColdKeys) { Object cacheKey = getKey(key); coldCache.put(cacheKey, SCRAP); coldKeyToOrderMap.put(key, cacheKey); } } if (hotCache != null) { Set changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : hotCache.keySet()) { if (key >= positionStart && key < positionStart + itemCount) { changedHotKeys.add(key); } } for (Integer key : changedHotKeys) { hotCache.put(key, SCRAP); } } } @Override public void onItemRangeInserted(final int positionStart, final int itemCount) { if (itemCount == 0) return; PlaybackInfo value; // Cold cache update if (container.getCacheManager() != null) { // [1] Take keys of old one. // 1.1 Extract subset of keys only: Set changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : coldKeyToOrderMap.keySet()) { if (key >= positionStart) { changedColdKeys.add(key); } } // 1.2 Extract entries from cold cache to a temp cache. final Map changeColdEntriesCache = new HashMap<>(); for (Integer key : changedColdKeys) { if ((value = coldCache.remove(coldKeyToOrderMap.get(key))) != null) { changeColdEntriesCache.put(key, value); } } // 1.2 Update cold Cache with new keys for (Integer key : changedColdKeys) { coldCache.put(getKey(key + itemCount), changeColdEntriesCache.get(key)); } // 1.3 Update coldKeyToOrderMap; for (Integer key : changedColdKeys) { coldKeyToOrderMap.put(key, getKey(key)); } } // [1] Remove cache if there is any appearance if (hotCache != null) { // [2] Shift cache by specific number Map changedHotEntriesCache = new HashMap<>(); Set changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : hotCache.keySet()) { if (key >= positionStart) { changedHotKeys.add(key); } } for (Integer key : changedHotKeys) { if ((value = hotCache.remove(key)) != null) { changedHotEntriesCache.put(key, value); } } for (Integer key : changedHotKeys) { hotCache.put(key + itemCount, changedHotEntriesCache.get(key)); } } } @Override public void onItemRangeRemoved(final int positionStart, final int itemCount) { if (itemCount == 0) return; PlaybackInfo value; // Cold cache update if (container.getCacheManager() != null) { // [1] Take keys of old one. // 1.1 Extract subset of keys only: Set changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : coldKeyToOrderMap.keySet()) { if (key >= positionStart + itemCount) changedColdKeys.add(key); } // 1.2 Extract entries from cold cache to a temp cache. final Map changeColdEntriesCache = new HashMap<>(); for (Integer key : changedColdKeys) { if ((value = coldCache.remove(coldKeyToOrderMap.get(key))) != null) { changeColdEntriesCache.put(key, value); } } // 1.2 Update cold Cache with new keys for (Integer key : changedColdKeys) { coldCache.put(getKey(key - itemCount), changeColdEntriesCache.get(key)); } // 1.3 Update coldKeyToOrderMap; for (Integer key : changedColdKeys) { coldKeyToOrderMap.put(key, getKey(key)); } } // [1] Remove cache if there is any appearance if (hotCache != null) { for (int i = 0; i < itemCount; i++) { hotCache.remove(positionStart + i); } // [2] Shift cache by specific number Map changedHotEntriesCache = new HashMap<>(); Set changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : hotCache.keySet()) { if (key >= positionStart + itemCount) changedHotKeys.add(key); } for (Integer key : changedHotKeys) { if ((value = hotCache.remove(key)) != null) { changedHotEntriesCache.put(key, value); } } for (Integer key : changedHotKeys) { hotCache.put(key - itemCount, changedHotEntriesCache.get(key)); } } } // Dude I wanna test this thing >.< @Override public void onItemRangeMoved(final int fromPos, final int toPos, int itemCount) { if (fromPos == toPos) return; final int low = fromPos < toPos ? fromPos : toPos; final int high = fromPos + toPos - low; final int shift = fromPos < toPos ? -1 : 1; // how item will be shifted due to the move PlaybackInfo value; // [1] Migrate cold cache. if (container.getCacheManager() != null) { // 1.1 Extract subset of keys only: Set changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : coldKeyToOrderMap.keySet()) { if (key >= low && key <= high) changedColdKeys.add(key); } // 1.2 Extract entries from cold cache to a temp cache. final Map changeColdEntries = new HashMap<>(); for (Integer key : changedColdKeys) { if ((value = coldCache.remove(coldKeyToOrderMap.get(key))) != null) { changeColdEntries.put(key, value); } } // 1.2 Update cold Cache with new keys for (Integer key : changedColdKeys) { if (key == low) { coldCache.put(getKey(high), changeColdEntries.get(key)); } else { coldCache.put(getKey(key + shift), changeColdEntries.get(key)); } } // 1.3 Update coldKeyToOrderMap; for (Integer key : changedColdKeys) { coldKeyToOrderMap.put(key, getKey(key)); } } // [2] Migrate hot cache. if (hotCache != null) { Set changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT); for (Integer key : hotCache.keySet()) { if (key >= low && key <= high) changedHotKeys.add(key); } Map changedHotEntriesCache = new HashMap<>(); for (Integer key : changedHotKeys) { if ((value = hotCache.remove(key)) != null) changedHotEntriesCache.put(key, value); } for (Integer key : changedHotKeys) { if (key == low) { hotCache.put(high, changedHotEntriesCache.get(key)); } else { hotCache.put(key + shift, changedHotEntriesCache.get(key)); } } } } @Nullable private Object getKey(int position) { return position == RecyclerView.NO_POSITION ? null : container.getCacheManager() == null ? null : container.getCacheManager().getKeyForOrder(position); } //@Nullable private Integer getOrder(Object key) { // return container.getCacheManager() == null ? null // : container.getCacheManager().getOrderForKey(key); //} @NonNull PlaybackInfo getPlaybackInfo(int position) { PlaybackInfo info = hotCache != null ? hotCache.get(position) : null; if (info == SCRAP) { // has hot cache, but was SCRAP. info = container.playerInitializer.initPlaybackInfo(position); } Object key = getKey(position); info = info != null ? info : (key != null ? coldCache.get(key) : null); if (info == null) info = container.playerInitializer.initPlaybackInfo(position); return info; } // Call by Container#savePlaybackInfo and that method is called right before any pausing. void savePlaybackInfo(int position, @NonNull PlaybackInfo playbackInfo) { ToroUtil.checkNotNull(playbackInfo); if (hotCache != null) hotCache.put(position, playbackInfo); Object key = getKey(position); if (key != null) coldCache.put(key, playbackInfo); } @NonNull SparseArray saveStates() { SparseArray states = new SparseArray<>(); if (container.getCacheManager() != null) { for (Map.Entry entry : coldKeyToOrderMap.entrySet()) { states.put(entry.getKey(), coldCache.get(entry.getValue())); } } else if (hotCache != null) { for (Map.Entry entry : hotCache.entrySet()) { states.put(entry.getKey(), entry.getValue()); } } return states; } void restoreStates(@Nullable SparseArray savedStates) { int cacheSize; if (savedStates != null && (cacheSize = savedStates.size()) > 0) { for (int i = 0; i < cacheSize; i++) { int order = savedStates.keyAt(i); Object key = getKey(order); coldKeyToOrderMap.put(order, key); PlaybackInfo playbackInfo = (PlaybackInfo) savedStates.get(order); if (playbackInfo != null) this.savePlaybackInfo(order, playbackInfo); } } } void clearCache() { coldCache.clear(); if (hotCache != null) hotCache.clear(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlayerManager.java ================================================ /* * Copyright (c) 2017 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.widget; import android.os.Handler; import android.os.Looper; import android.os.Message; import androidx.annotation.NonNull; import androidx.collection.ArraySet; import java.util.ArrayList; import java.util.List; import java.util.Set; import ml.docilealligator.infinityforreddit.videoautoplay.PlayerDispatcher; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; /** * Manage the collection of {@link ToroPlayer}s for a specific {@link Container}. * * Task: collect all Players in which "{@link Common#allowsToPlay(ToroPlayer)}" returns true, then * initialize them. * * @author eneim | 5/31/17. */ @SuppressWarnings({ "unused", "UnusedReturnValue", "StatementWithEmptyBody" }) // final class PlayerManager implements Handler.Callback { private static final String TAG = "ToroLib:Manager"; private Handler handler; // Make sure each ToroPlayer will present only once in this Manager. private final Set players = new ArraySet<>(); boolean attachPlayer(@NonNull ToroPlayer player) { return players.add(player); } boolean detachPlayer(@NonNull ToroPlayer player) { if (handler != null) handler.removeCallbacksAndMessages(player); return players.remove(player); } boolean manages(@NonNull ToroPlayer player) { return players.contains(player); } /** * Return a "Copy" of the collection of players this manager is managing. * * @return a non null collection of Players those a managed. */ @NonNull List getPlayers() { return new ArrayList<>(this.players); } void initialize(@NonNull ToroPlayer player, Container container) { player.initialize(container, container.getPlaybackInfo(player.getPlayerOrder())); } // 2018.07.02 Directly pass PlayerDispatcher so that we can easily expand the ability in the future. void play(@NonNull ToroPlayer player, PlayerDispatcher dispatcher) { this.play(player, dispatcher.getDelayToPlay(player)); } private void play(@NonNull ToroPlayer player, int delay) { if (delay < PlayerDispatcher.DELAY_INFINITE) throw new IllegalArgumentException("Too negative"); if (handler == null) return; // equals to that this is not attached yet. handler.removeMessages(MSG_PLAY, player); // remove undone msg for this player if (delay == PlayerDispatcher.DELAY_INFINITE) { // do nothing } else if (delay == PlayerDispatcher.DELAY_NONE) { player.play(); } else { handler.sendMessageDelayed(handler.obtainMessage(MSG_PLAY, player), delay); } } void pause(@NonNull ToroPlayer player) { // remove all msg sent for the player if (handler != null) handler.removeCallbacksAndMessages(player); player.pause(); } // return false if this manager could not release the player. // normally when this manager doesn't manage the player. boolean release(@NonNull ToroPlayer player) { if (handler != null) handler.removeCallbacksAndMessages(null); if (manages(player)) { player.release(); return true; } else { return false; } } void recycle(ToroPlayer player) { if (handler != null) handler.removeCallbacksAndMessages(player); } void clear() { if (handler != null) handler.removeCallbacksAndMessages(null); this.players.clear(); } void deferPlaybacks() { if (handler != null) handler.removeMessages(MSG_PLAY); } void onAttach() { // do nothing if (handler == null) handler = new Handler(Looper.getMainLooper(), this); } void onDetach() { if (handler != null) { handler.removeCallbacksAndMessages(null); handler = null; } } @SuppressWarnings("WeakerAccess") static final int MSG_PLAY = 100; @Override public boolean handleMessage(Message msg) { if (msg.what == MSG_PLAY && msg.obj instanceof ToroPlayer) { ToroPlayer player = (ToroPlayer) msg.obj; player.play(); } return true; } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PressablePlayerSelector.java ================================================ /* * Copyright (c) 2018 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ml.docilealligator.infinityforreddit.videoautoplay.widget; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static java.util.Collections.singletonList; import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.allowsToPlay; import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.findFirst; import android.view.View; import android.view.View.OnLongClickListener; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import ml.docilealligator.infinityforreddit.videoautoplay.PlayerSelector; import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil; import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta; /** * @author eneim (2018/08/17). * * A 'Press to Play' {@link PlayerSelector}. * * This is a {@link OnLongClickListener} that co-operates with {@link Container} to selectively * trigger the playback. The common usecase is to allow user to long click on a {@link ToroPlayer} * to trigger its playback. In that case, we should set that {@link ToroPlayer} to highest priority * among the candidates, and also to clear its priority when user scroll it out of the playable region. * * This class also have a {@link #toPause} field to handle the case where User want to specifically * pause a {@link ToroPlayer}. This selection will also be cleared with the same rules of toPlay. * @since 3.6.0.2802 */ @SuppressWarnings("WeakerAccess") @Beta // public class PressablePlayerSelector implements PlayerSelector, OnLongClickListener { protected final WeakReference weakContainer; protected final PlayerSelector delegate; protected final AtomicInteger toPlay = new AtomicInteger(NO_POSITION); protected final AtomicInteger toPause = new AtomicInteger(NO_POSITION); final Common.Filter filterToPlay = new Common.Filter() { @Override public boolean accept(ToroPlayer target) { return target.getPlayerOrder() == toPlay.get(); } }; final Common.Filter filterToPause = new Common.Filter() { @Override public boolean accept(ToroPlayer target) { return target.getPlayerOrder() == toPause.get(); } }; public PressablePlayerSelector(Container container) { this(container, DEFAULT); } public PressablePlayerSelector(Container container, PlayerSelector delegate) { this(new WeakReference<>(ToroUtil.checkNotNull(container)), ToroUtil.checkNotNull(delegate)); } PressablePlayerSelector(WeakReference container, PlayerSelector delegate) { this.weakContainer = container; this.delegate = ToroUtil.checkNotNull(delegate); } @Override public boolean onLongClick(View v) { Container container = weakContainer.get(); if (container == null) return false; // fail fast toPause.set(NO_POSITION); // long click will always mean to 'press to play'. RecyclerView.ViewHolder viewHolder = container.findContainingViewHolder(v); boolean handled = viewHolder instanceof ToroPlayer; if (handled) handled = allowsToPlay((ToroPlayer) viewHolder); int position = handled ? viewHolder.getAdapterPosition() : NO_POSITION; if (handled) handled = position != toPlay.getAndSet(position); if (handled) container.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); return handled; } @Override @NonNull public Collection select(@NonNull Container container, @NonNull List items) { // Make sure client doesn't use this instance to wrong Container. if (container != this.weakContainer.get()) return new ArrayList<>(); // If there is a request to pause, we need to prioritize that first. ToroPlayer toPauseCandidate = null; if (toPause.get() >= 0) { toPauseCandidate = findFirst(items, filterToPause); if (toPauseCandidate == null) { // the order to pause doesn't present in candidate, we clear the selection. toPause.set(NO_POSITION); // remove the paused one. } } if (toPlay.get() >= 0) { ToroPlayer toPlayCandidate = findFirst(items, filterToPlay); if (toPlayCandidate != null) { if (allowsToPlay(toPlayCandidate)) { return singletonList(toPlayCandidate); } } } // In the list of candidates, selected item no longer presents or is not allowed to play, // we should reset the selection. toPlay.set(NO_POSITION); // Wrap by an ArrayList to make it modifiable. Collection selected = new ArrayList<>(delegate.select(container, items)); if (toPauseCandidate != null) selected.remove(toPauseCandidate); return selected; } @Override @NonNull public PlayerSelector reverse() { return new PressablePlayerSelector(this.weakContainer, delegate.reverse()); } public boolean toPlay(int position) { if (toPause.get() == position) toPause.set(NO_POSITION); Container container = weakContainer.get(); if (container == null) return false; if (position != toPlay.getAndSet(position)) { container.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); return true; } return false; } public void toPause(int position) { toPlay.set(NO_POSITION); toPause.set(position); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/viewmodels/CommentActivityViewModel.kt ================================================ package ml.docilealligator.infinityforreddit.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import kotlinx.coroutines.launch import ml.docilealligator.infinityforreddit.comment.CommentDraft import ml.docilealligator.infinityforreddit.comment.DraftType import ml.docilealligator.infinityforreddit.repositories.CommentActivityRepository class CommentActivityViewModel( private val commentActivityRepository: CommentActivityRepository ): ViewModel() { fun getCommentDraft(fullname: String): LiveData { return commentActivityRepository.getCommentDraft(fullname) } fun saveCommentDraft(fullname: String, content: String, onSaved: () -> Unit) { viewModelScope.launch { commentActivityRepository.saveCommentDraft(fullname, content) onSaved() } } fun deleteCommentDraft(fullname: String, onDeleted: () -> Unit) { viewModelScope.launch { commentActivityRepository.deleteCommentDraft(fullname) onDeleted() } } companion object { fun provideFactory(commentActivityRepository: CommentActivityRepository) : ViewModelProvider.Factory { return object: ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class, extras: CreationExtras ): T { return CommentActivityViewModel( commentActivityRepository, ) as T } } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/viewmodels/CopyMultiRedditActivityViewModel.kt ================================================ package ml.docilealligator.infinityforreddit.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import ml.docilealligator.infinityforreddit.APIError import ml.docilealligator.infinityforreddit.APIResult import ml.docilealligator.infinityforreddit.ActionState import ml.docilealligator.infinityforreddit.ActionStateError import ml.docilealligator.infinityforreddit.DataLoadState import ml.docilealligator.infinityforreddit.multireddit.MultiReddit import ml.docilealligator.infinityforreddit.repositories.CopyMultiRedditActivityRepository class CopyMultiRedditActivityViewModel( val multipath: String, val copyMultiRedditActivityRepository: CopyMultiRedditActivityRepository ): ViewModel() { private val _name = MutableStateFlow("") val name = _name.asStateFlow() private val _description = MutableStateFlow("") val description = _description.asStateFlow() private val _multiRedditState = MutableStateFlow>(DataLoadState.Loading) val multiRedditState: StateFlow> = _multiRedditState.asStateFlow() private val _copyMultiRedditState = MutableStateFlow(ActionState.Idle) val copyMultiRedditState: StateFlow = _copyMultiRedditState.asStateFlow() fun setName(name: String) { _name.value = name } fun setDescription(description: String) { _description.value = description } fun fetchMultiRedditInfo() { if (_multiRedditState.value is DataLoadState.Success) { return } _multiRedditState.value = DataLoadState.Loading viewModelScope.launch { val multiReddit = copyMultiRedditActivityRepository.fetchMultiRedditInfo(multipath) _multiRedditState.value = multiReddit?.let { _name.value = it.name _description.value = it.description DataLoadState.Success(it) } ?: DataLoadState.Error("Failed to load multiReddit") } } fun copyMultiRedditInfo() { if (_copyMultiRedditState.value == ActionState.Running) { return } _copyMultiRedditState.value = ActionState.Running viewModelScope.launch { when (val result = copyMultiRedditActivityRepository.copyMultiReddit(multipath, _name.value, _description.value, (multiRedditState.value as DataLoadState.Success).data.subreddits)) { is APIResult.Success -> { _copyMultiRedditState.value = ActionState.Success(result.data) } is APIResult.Error -> { val error =result.error when (error) { is APIError.Message -> _copyMultiRedditState.value = ActionState.Error( ActionStateError.Message(error.message) ) is APIError.MessageRes -> _copyMultiRedditState.value = ActionState.Error( ActionStateError.MessageRes(error.resId) ) } } } } } companion object { fun provideFactory(multipath: String, copyMultiRedditActivityRepository: CopyMultiRedditActivityRepository) : ViewModelProvider.Factory { return object: ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class, extras: CreationExtras ): T { return CopyMultiRedditActivityViewModel( multipath, copyMultiRedditActivityRepository ) as T } } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/viewmodels/EditCommentActivityViewModel.kt ================================================ package ml.docilealligator.infinityforreddit.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import kotlinx.coroutines.launch import ml.docilealligator.infinityforreddit.comment.CommentDraft import ml.docilealligator.infinityforreddit.comment.DraftType import ml.docilealligator.infinityforreddit.repositories.EditCommentActivityRepository class EditCommentActivityViewModel( private val editCommentActivityRepository: EditCommentActivityRepository ): ViewModel() { fun getCommentDraft(fullname: String): LiveData { return editCommentActivityRepository.getCommentDraft(fullname) } fun saveCommentDraft(fullname: String, content: String, onSaved: () -> Unit) { viewModelScope.launch { editCommentActivityRepository.saveCommentDraft(fullname, content) onSaved() } } fun deleteCommentDraft(fullname: String, onDeleted: () -> Unit) { viewModelScope.launch { editCommentActivityRepository.deleteCommentDraft(fullname) onDeleted() } } companion object { fun provideFactory(editCommentActivityRepository: EditCommentActivityRepository) : ViewModelProvider.Factory { return object: ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class, extras: CreationExtras ): T { return EditCommentActivityViewModel( editCommentActivityRepository, ) as T } } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/viewmodels/ViewPostDetailActivityViewModel.java ================================================ package ml.docilealligator.infinityforreddit.viewmodels; import android.os.Handler; import androidx.annotation.NonNull; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import java.util.List; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.user.UserProfileImagesBatchLoader; import retrofit2.Retrofit; public class ViewPostDetailActivityViewModel extends ViewModel { private UserProfileImagesBatchLoader mLoader; public ViewPostDetailActivityViewModel(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, Retrofit retrofit) { mLoader = new UserProfileImagesBatchLoader(executor, handler, redditDataRoomDatabase, retrofit); } public void loadAuthorImages(List comments, @NonNull LoadIconListener loadIconListener) { mLoader.loadAuthorImages(comments, loadIconListener); } public static class Factory extends ViewModelProvider.NewInstanceFactory { private Executor mExecutor; private Handler mHandler; private RedditDataRoomDatabase mRedditDataRoomDatabase; private Retrofit mRetrofit; public Factory(Executor executor, Handler handler, RedditDataRoomDatabase redditDataRoomDatabase, Retrofit retrofit) { this.mExecutor = executor; this.mHandler = handler; this.mRedditDataRoomDatabase = redditDataRoomDatabase; this.mRetrofit = retrofit; } @NonNull @Override public T create(@NonNull Class modelClass) { return (T) new ViewPostDetailActivityViewModel(mExecutor, mHandler, mRedditDataRoomDatabase, mRetrofit); } } public interface LoadIconListener { void loadIconSuccess(String authorFullName, String iconUrl); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/viewmodels/ViewPostDetailFragmentViewModel.kt ================================================ package ml.docilealligator.infinityforreddit.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import ml.docilealligator.infinityforreddit.SingleLiveEvent import ml.docilealligator.infinityforreddit.apis.RedditAPI import ml.docilealligator.infinityforreddit.comment.Comment import ml.docilealligator.infinityforreddit.moderation.CommentModerationEvent import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.ApproveFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.Approved import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.DistinguishAsModFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.DistinguishedAsMod import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.LockFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.Locked import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.MarkAsSpamFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.MarkNSFWFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.MarkSpoilerFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.MarkedAsSpam import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.MarkedNSFW import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.MarkedSpoiler import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.RemoveFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.SetStickyPost import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.SetStickyPostFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UndistinguishAsModFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UndistinguishedAsMod import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UnlockFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.Unlocked import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UnmarkNSFWFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UnmarkSpoilerFailed import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UnmarkedNSFW import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UnmarkedSpoiler import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UnsetStickyPost import ml.docilealligator.infinityforreddit.moderation.PostModerationEvent.UnsetStickyPostFailed import ml.docilealligator.infinityforreddit.post.Post import ml.docilealligator.infinityforreddit.utils.APIUtils import retrofit2.Call import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit class ViewPostDetailFragmentViewModel( private val oauthRetrofit: Retrofit, private val accessToken: String?, private val accountName: String? ) : ViewModel() { val postModerationEventLiveData: SingleLiveEvent = SingleLiveEvent() val commentModerationEventLiveData: SingleLiveEvent = SingleLiveEvent() fun approvePost(post: Post, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = post.fullName oauthRetrofit.create(RedditAPI::class.java) .approveThing(APIUtils.getOAuthHeader(accessToken), params) .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.isApproved = true post.approvedBy = accountName post.approvedAtUTC = System.currentTimeMillis() post.setRemoved(false, false) postModerationEventLiveData.postValue(Approved(post, position)) } else { postModerationEventLiveData.postValue(ApproveFailed(post, position)) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue(ApproveFailed(post, position)) } }) } fun removePost(post: Post, position: Int, isSpam: Boolean) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = post.fullName params[APIUtils.SPAM_KEY] = isSpam.toString() oauthRetrofit.create(RedditAPI::class.java) .removeThing(APIUtils.getOAuthHeader(accessToken), params) .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.isApproved = false post.approvedBy = null post.approvedAtUTC = 0 post.setRemoved(true, isSpam) postModerationEventLiveData.postValue( if (isSpam) MarkedAsSpam( post, position ) else PostModerationEvent.Removed(post, position) ) } else { postModerationEventLiveData.postValue( if (isSpam) MarkAsSpamFailed( post, position ) else RemoveFailed(post, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue( if (isSpam) MarkAsSpamFailed( post, position ) else RemoveFailed(post, position) ) } }) } fun toggleSticky(post: Post, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = post.fullName params[APIUtils.STATE_KEY] = (!post.isStickied).toString() params[APIUtils.API_TYPE_KEY] = APIUtils.API_TYPE_JSON oauthRetrofit.create(RedditAPI::class.java) .toggleStickyPost(APIUtils.getOAuthHeader(accessToken), params) .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.setIsStickied(!post.isStickied) postModerationEventLiveData.postValue( if (post.isStickied) SetStickyPost( post, position ) else UnsetStickyPost(post, position) ) } else { postModerationEventLiveData.postValue( if (post.isStickied) UnsetStickyPostFailed( post, position ) else SetStickyPostFailed(post, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue( if (post.isStickied) UnsetStickyPostFailed( post, position ) else SetStickyPostFailed(post, position) ) } }) } fun toggleLock(post: Post, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = post.fullName val call: Call = if (post.isLocked) oauthRetrofit.create( RedditAPI::class.java ).unLockThing(APIUtils.getOAuthHeader(accessToken), params) else oauthRetrofit.create( RedditAPI::class.java ).lockThing(APIUtils.getOAuthHeader(accessToken), params) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.setIsLocked(!post.isLocked) postModerationEventLiveData.postValue( if (post.isLocked) Locked( post, position ) else Unlocked(post, position) ) } else { postModerationEventLiveData.postValue( if (post.isLocked) UnlockFailed( post, position ) else LockFailed(post, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue( if (post.isLocked) UnlockFailed( post, position ) else LockFailed(post, position) ) } }) } fun toggleNSFW(post: Post, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = post.fullName val call: Call = if (post.isNSFW) oauthRetrofit.create( RedditAPI::class.java ).unmarkNSFW(APIUtils.getOAuthHeader(accessToken), params) else oauthRetrofit.create( RedditAPI::class.java ).markNSFW(APIUtils.getOAuthHeader(accessToken), params) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.isNSFW = !post.isNSFW postModerationEventLiveData.postValue( if (post.isNSFW) MarkedNSFW( post, position ) else UnmarkedNSFW(post, position) ) } else { postModerationEventLiveData.postValue( if (post.isNSFW) UnmarkNSFWFailed( post, position ) else MarkNSFWFailed(post, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue( if (post.isNSFW) UnmarkNSFWFailed( post, position ) else MarkNSFWFailed(post, position) ) } }) } fun toggleSpoiler(post: Post, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = post.fullName val call: Call = if (post.isSpoiler) oauthRetrofit.create( RedditAPI::class.java ).unmarkSpoiler( APIUtils.getOAuthHeader(accessToken), params ) else oauthRetrofit.create( RedditAPI::class.java ).markSpoiler(APIUtils.getOAuthHeader(accessToken), params) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.isSpoiler = !post.isSpoiler postModerationEventLiveData.postValue( if (post.isSpoiler) MarkedSpoiler( post, position ) else UnmarkedSpoiler(post, position) ) } else { postModerationEventLiveData.postValue( if (post.isSpoiler) UnmarkSpoilerFailed( post, position ) else MarkSpoilerFailed(post, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue( if (post.isSpoiler) UnmarkSpoilerFailed( post, position ) else MarkSpoilerFailed(post, position) ) } }) } fun toggleMod(post: Post, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = post.fullName params[APIUtils.HOW_KEY] = if (post.isModerator) APIUtils.HOW_NO else APIUtils.HOW_YES oauthRetrofit.create(RedditAPI::class.java) .toggleDistinguishedThing(APIUtils.getOAuthHeader(accessToken), params) .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.setIsModerator(!post.isModerator) postModerationEventLiveData.postValue( if (post.isModerator) DistinguishedAsMod( post, position ) else UndistinguishedAsMod(post, position) ) } else { postModerationEventLiveData.postValue( if (post.isModerator) UndistinguishAsModFailed( post, position ) else DistinguishAsModFailed(post, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue( if (post.isModerator) UndistinguishAsModFailed( post, position ) else DistinguishAsModFailed(post, position) ) } }) } fun toggleNotification(post: Post, position: Int) { val params: MutableMap = HashMap() params.put(APIUtils.ID_KEY, post.fullName) params.put(APIUtils.STATE_KEY, (!post.isSendReplies).toString()) oauthRetrofit.create(RedditAPI::class.java) .toggleRepliesNotification(APIUtils.getOAuthHeader(accessToken), params) .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { post.isSendReplies = !post.isSendReplies postModerationEventLiveData.postValue( if (post.isSendReplies) PostModerationEvent.SetReceiveNotification( post, position ) else PostModerationEvent.UnsetReceiveNotification(post, position) ) } else { postModerationEventLiveData.postValue( if (post.isSendReplies) PostModerationEvent.UnsetReceiveNotificationFailed( post, position ) else PostModerationEvent.SetReceiveNotificationFailed(post, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { postModerationEventLiveData.postValue( if (post.isSendReplies) PostModerationEvent.UnsetReceiveNotificationFailed( post, position ) else PostModerationEvent.SetReceiveNotificationFailed(post, position) ) } }) } fun approveComment(comment: Comment, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = comment.fullName oauthRetrofit.create(RedditAPI::class.java) .approveThing(APIUtils.getOAuthHeader(accessToken), params) .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { comment.isApproved = true comment.approvedBy = accountName comment.approvedAtUTC = System.currentTimeMillis() comment.setRemoved(false, false) commentModerationEventLiveData.postValue(CommentModerationEvent.Approved(comment, position)) } else { commentModerationEventLiveData.postValue(CommentModerationEvent.ApproveFailed(comment, position)) } } override fun onFailure(call: Call, throwable: Throwable) { commentModerationEventLiveData.postValue(CommentModerationEvent.ApproveFailed(comment, position)) } }) } fun removeComment(comment: Comment, position: Int, isSpam: Boolean) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = comment.fullName params[APIUtils.SPAM_KEY] = isSpam.toString() oauthRetrofit.create(RedditAPI::class.java) .removeThing(APIUtils.getOAuthHeader(accessToken), params) .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { comment.isApproved = false comment.approvedBy = null comment.approvedAtUTC = 0 comment.setRemoved(true, isSpam) commentModerationEventLiveData.postValue( if (isSpam) CommentModerationEvent.MarkedAsSpam( comment, position ) else CommentModerationEvent.Removed(comment, position) ) } else { commentModerationEventLiveData.postValue( if (isSpam) CommentModerationEvent.MarkAsSpamFailed( comment, position ) else CommentModerationEvent.RemoveFailed(comment, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { commentModerationEventLiveData.postValue( if (isSpam) CommentModerationEvent.MarkAsSpamFailed( comment, position ) else CommentModerationEvent.RemoveFailed(comment, position) ) } }) } fun toggleLock(comment: Comment, position: Int) { val params: MutableMap = HashMap() params[APIUtils.ID_KEY] = comment.fullName val call: Call = if (comment.isLocked) oauthRetrofit.create( RedditAPI::class.java ).unLockThing(APIUtils.getOAuthHeader(accessToken), params) else oauthRetrofit.create( RedditAPI::class.java ).lockThing(APIUtils.getOAuthHeader(accessToken), params) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { comment.isLocked = !comment.isLocked commentModerationEventLiveData.postValue( if (comment.isLocked) CommentModerationEvent.Locked( comment, position ) else CommentModerationEvent.Unlocked(comment, position) ) } else { commentModerationEventLiveData.postValue( if (comment.isLocked) CommentModerationEvent.UnlockFailed( comment, position ) else CommentModerationEvent.LockFailed(comment, position) ) } } override fun onFailure(call: Call, throwable: Throwable) { commentModerationEventLiveData.postValue( if (comment.isLocked) CommentModerationEvent.UnlockFailed( comment, position ) else CommentModerationEvent.LockFailed(comment, position) ) } }) } companion object { fun provideFactory(oauthRetrofit: Retrofit, accessToken: String?, accountName: String?) : ViewModelProvider.Factory { return object: ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class, extras: CreationExtras ): T { return ViewPostDetailFragmentViewModel( oauthRetrofit, accessToken, accountName ) as T } } } } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/worker/MaterialYouWorker.java ================================================ package ml.docilealligator.infinityforreddit.worker; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.utils.MaterialYouUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; public class MaterialYouWorker extends Worker { public static final String UNIQUE_WORKER_NAME = "MYWT"; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("light_theme") SharedPreferences lightThemeSharedPreferences; @Inject @Named("dark_theme") SharedPreferences darkThemeSharedPreferences; @Inject @Named("amoled_theme") SharedPreferences amoledThemeSharedPreferences; @Inject @Named("internal") SharedPreferences mInternalSharedPreferences; @Inject RedditDataRoomDatabase redditDataRoomDatabase; @Inject CustomThemeWrapper customThemeWrapper; private final Context context; public MaterialYouWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); this.context = context; ((Infinity) context.getApplicationContext()).getAppComponent().inject(this); } @NonNull @Override public Result doWork() { if (mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_MATERIAL_YOU, false)) { MaterialYouUtils.changeThemeSync(context, redditDataRoomDatabase, customThemeWrapper, lightThemeSharedPreferences, darkThemeSharedPreferences, amoledThemeSharedPreferences, mInternalSharedPreferences); } return Result.success(); } } ================================================ FILE: app/src/main/java/ml/docilealligator/infinityforreddit/worker/PullNotificationWorker.java ================================================ package ml.docilealligator.infinityforreddit.worker; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.work.Worker; import androidx.work.WorkerParameters; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.activities.InboxActivity; import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.message.FetchMessage; import ml.docilealligator.infinityforreddit.message.Message; import ml.docilealligator.infinityforreddit.message.ParseMessage; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.NotificationUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; public class PullNotificationWorker extends Worker { public static final String UNIQUE_WORKER_NAME = "PNWT"; @Inject @Named("oauth_without_authenticator") Retrofit mOauthWithoutAuthenticatorRetrofit; @Inject @Named("no_oauth") Retrofit mRetrofit; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @Named("default") SharedPreferences mSharedPreferences; @Inject @Named("current_account") SharedPreferences mCurrentAccountSharedPreferences; @Inject CustomThemeWrapper mCustomThemeWrapper; private final Context context; public PullNotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); this.context = context; ((Infinity) context.getApplicationContext()).getAppComponent().inject(this); } @NonNull @Override public Result doWork() { NotificationManagerCompat notificationManager = NotificationUtils.getNotificationManager(context); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) { return Result.success(); } try { List accounts = mRedditDataRoomDatabase.accountDao().getAllAccounts(); int color = mCustomThemeWrapper.getColorPrimaryLightTheme(); for (int accountIndex = 0; accountIndex < accounts.size(); accountIndex++) { Account account = accounts.get(accountIndex); String accountName = account.getAccountName(); Response response = fetchMessages(account, 1); if (response != null && response.isSuccessful() && response.body() != null) { String responseBody = response.body(); JSONArray messageArray = new JSONObject(responseBody).getJSONObject(JSONUtils.DATA_KEY).getJSONArray(JSONUtils.CHILDREN_KEY); ArrayList messages = ParseMessage.parseMessages(messageArray, context.getResources().getConfiguration().locale, FetchMessage.MESSAGE_TYPE_NOTIFICATION); if (!messages.isEmpty()) { NotificationCompat.Builder summaryBuilder = NotificationUtils.buildSummaryNotification(context, notificationManager, accountName, context.getString(R.string.notification_new_messages, messages.size()), NotificationUtils.CHANNEL_ID_NEW_MESSAGES, NotificationUtils.CHANNEL_NEW_MESSAGES, NotificationUtils.getAccountGroupName(accountName), color); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); int messageSize = Math.min(messages.size(), 20); long lastNotificationTime = mSharedPreferences.getLong(SharedPreferencesUtils.PULL_NOTIFICATION_TIME, -1L); boolean hasValidMessage = false; long currentTime = Calendar.getInstance().getTimeInMillis(); mSharedPreferences.edit().putLong(SharedPreferencesUtils.PULL_NOTIFICATION_TIME, currentTime).apply(); int pendingIntentFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT; for (int messageIndex = messageSize - 1; messageIndex >= 0; messageIndex--) { Message message = messages.get(messageIndex); if (message.getTimeUTC() <= lastNotificationTime) { continue; } hasValidMessage = true; inboxStyle.addLine(message.getAuthor() + " " + message.getBody()); String kind = message.getKind(); String title; String summary; if (kind.equals(Message.TYPE_COMMENT) || kind.equals(Message.TYPE_LINK)) { title = message.getAuthor(); summary = message.getSubject().substring(0, 1).toUpperCase() + message.getSubject().substring(1); } else { title = message.getTitle() == null || message.getTitle().equals("") ? message.getSubject() : message.getTitle(); if (kind.equals(Message.TYPE_ACCOUNT)) { summary = context.getString(R.string.notification_summary_account); } else if (kind.equals(Message.TYPE_MESSAGE)) { summary = context.getString(R.string.notification_summary_message); } else if (kind.equals(Message.TYPE_SUBREDDIT)) { summary = context.getString(R.string.notification_summary_subreddit); } else { summary = context.getString(R.string.notification_summary_award); } } NotificationCompat.Builder builder = NotificationUtils.buildNotification(notificationManager, context, title, message.getBody(), summary, NotificationUtils.CHANNEL_ID_NEW_MESSAGES, NotificationUtils.CHANNEL_NEW_MESSAGES, NotificationUtils.getAccountGroupName(accountName), color); if (kind.equals(Message.TYPE_COMMENT)) { Intent intent = new Intent(context, LinkResolverActivity.class); Uri uri = Uri.parse(message.getContext()); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_NEW_ACCOUNT_NAME, accountName); intent.putExtra(LinkResolverActivity.EXTRA_MESSAGE_FULLNAME, message.getFullname()); PendingIntent pendingIntent = PendingIntent.getActivity(context, accountIndex * 6, intent, pendingIntentFlags); builder.setContentIntent(pendingIntent); } else if (kind.equals(Message.TYPE_ACCOUNT)) { Intent intent = new Intent(context, InboxActivity.class); intent.putExtra(InboxActivity.EXTRA_NEW_ACCOUNT_NAME, accountName); PendingIntent summaryPendingIntent = PendingIntent.getActivity(context, accountIndex * 6 + 1, intent, pendingIntentFlags); builder.setContentIntent(summaryPendingIntent); } else if (kind.equals(Message.TYPE_LINK)) { Intent intent = new Intent(context, LinkResolverActivity.class); Uri uri = Uri.parse(message.getContext()); intent.setData(uri); intent.putExtra(LinkResolverActivity.EXTRA_NEW_ACCOUNT_NAME, accountName); intent.putExtra(LinkResolverActivity.EXTRA_MESSAGE_FULLNAME, message.getFullname()); PendingIntent pendingIntent = PendingIntent.getActivity(context, accountIndex * 6 + 2, intent, pendingIntentFlags); builder.setContentIntent(pendingIntent); } else if (kind.equals(Message.TYPE_MESSAGE)) { Intent intent = new Intent(context, InboxActivity.class); intent.putExtra(InboxActivity.EXTRA_NEW_ACCOUNT_NAME, accountName); intent.putExtra(InboxActivity.EXTRA_VIEW_MESSAGE, true); PendingIntent summaryPendingIntent = PendingIntent.getActivity(context, accountIndex * 6 + 3, intent, pendingIntentFlags); builder.setContentIntent(summaryPendingIntent); } else if (kind.equals(Message.TYPE_SUBREDDIT)) { Intent intent = new Intent(context, InboxActivity.class); intent.putExtra(InboxActivity.EXTRA_NEW_ACCOUNT_NAME, accountName); PendingIntent summaryPendingIntent = PendingIntent.getActivity(context, accountIndex * 6 + 4, intent, pendingIntentFlags); builder.setContentIntent(summaryPendingIntent); } else { Intent intent = new Intent(context, InboxActivity.class); intent.putExtra(InboxActivity.EXTRA_NEW_ACCOUNT_NAME, accountName); PendingIntent summaryPendingIntent = PendingIntent.getActivity(context, accountIndex * 6 + 5, intent, pendingIntentFlags); builder.setContentIntent(summaryPendingIntent); } notificationManager.notify(NotificationUtils.getNotificationIdUnreadMessage(accountIndex, messageIndex), builder.build()); } if (hasValidMessage) { inboxStyle.setBigContentTitle(context.getString(R.string.notification_new_messages, messages.size())).setSummaryText(accountName); summaryBuilder.setStyle(inboxStyle); Intent summaryIntent = new Intent(context, InboxActivity.class); summaryIntent.putExtra(InboxActivity.EXTRA_NEW_ACCOUNT_NAME, accountName); PendingIntent summaryPendingIntent = PendingIntent.getActivity(context, accountIndex * 6 + 6, summaryIntent, pendingIntentFlags); summaryBuilder.setContentIntent(summaryPendingIntent); notificationManager.notify(NotificationUtils.getSummaryIdUnreadMessage(accountIndex), summaryBuilder.build()); } } else { return Result.success(); } } else { return Result.retry(); } } } catch (IOException | JSONException e) { e.printStackTrace(); return Result.retry(); } return Result.success(); } private Response fetchMessages(Account account, int retryCount) throws IOException, JSONException { if (retryCount < 0) { return null; } Call call = mOauthWithoutAuthenticatorRetrofit.create(RedditAPI.class).getMessages(APIUtils.getOAuthHeader(account.getAccessToken()),FetchMessage.WHERE_UNREAD, null); Response response = call.execute(); if (response.isSuccessful()) { return response; } else { if (response.code() == 401) { String accessToken = refreshAccessToken(account); if (!accessToken.equals("")) { account.setAccessToken(accessToken); return fetchMessages(account, retryCount - 1); } } return null; } } private String refreshAccessToken(Account account) { String refreshToken = account.getRefreshToken(); RedditAPI api = mRetrofit.create(RedditAPI.class); Map params = new HashMap<>(); params.put(APIUtils.GRANT_TYPE_KEY, APIUtils.GRANT_TYPE_REFRESH_TOKEN); params.put(APIUtils.REFRESH_TOKEN_KEY, refreshToken); // Construct header directly using the fetched clientId String clientId = APIUtils.getClientId(getApplicationContext()); Map authHeader = new HashMap<>(); String credentials = String.format("%s:%s", clientId, ""); String auth = "Basic " + android.util.Base64.encodeToString(credentials.getBytes(), android.util.Base64.NO_WRAP); authHeader.put(APIUtils.AUTHORIZATION_KEY, auth); Call accessTokenCall = api.getAccessToken(authHeader, params); try { Response response = accessTokenCall.execute(); if (response.isSuccessful() && response.body() != null) { JSONObject jsonObject = new JSONObject(response.body()); String newAccessToken = jsonObject.getString(APIUtils.ACCESS_TOKEN_KEY); String newRefreshToken = jsonObject.has(APIUtils.REFRESH_TOKEN_KEY) ? jsonObject.getString(APIUtils.REFRESH_TOKEN_KEY) : null; if (newRefreshToken == null) { mRedditDataRoomDatabase.accountDao().updateAccessToken(account.getAccountName(), newAccessToken); } else { mRedditDataRoomDatabase.accountDao().updateAccessTokenAndRefreshToken(account.getAccountName(), newAccessToken, newRefreshToken); } if (mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCOUNT_NAME, Account.ANONYMOUS_ACCOUNT).equals(account.getAccountName())) { mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.ACCESS_TOKEN, newAccessToken).apply(); } return newAccessToken; } return ""; } catch (IOException | JSONException e) { e.printStackTrace(); } return ""; } } ================================================ FILE: app/src/main/res/anim/enter_from_left.xml ================================================ ================================================ FILE: app/src/main/res/anim/enter_from_right.xml ================================================ ================================================ FILE: app/src/main/res/anim/exit_to_left.xml ================================================ ================================================ FILE: app/src/main/res/anim/exit_to_right.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_out_down.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_out_up.xml ================================================ ================================================ FILE: app/src/main/res/drawable/background_autoplay.xml ================================================ ================================================ FILE: app/src/main/res/drawable/circular_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/edit_text_cursor.xml ================================================ ================================================ FILE: app/src/main/res/drawable/error_image.xml ================================================ ================================================ FILE: app/src/main/res/drawable/exo_control_button_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/exo_player_control_button_circular_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/exo_player_control_view_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_brazil.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_bulgaria.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_china.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_croatia.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_france.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_germany.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_greece.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_hungary.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_india.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_israel.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_italy.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_japan.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_netherlands.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_norway.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_poland.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_portugal.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_romania.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_russia.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_somalia.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_south_korea.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_spain.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_sweden.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_turkey.xml ================================================ ================================================ FILE: app/src/main/res/drawable/flag_vietnam.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_about_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_access_time_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_account_circle_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_a_photo_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_a_photo_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_circle_outline_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_advanced_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_amoled_theme_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_anonymous_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_apply_to_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_approve_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_archive_outline.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_back_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_back_white_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_downward_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_downward_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_upward_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_upward_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_arrow_drop_down_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_arrow_drop_up_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_best_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bold_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark_border_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark_border_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark_border_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmarks_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_call_split_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_check_circle_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_check_circle_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_close_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_code_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_color_lens_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_comment_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_comment_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_controversial_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_copy_16dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_copy_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_crosspost_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_current_user_14dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_dark_theme_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_dark_theme_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_data_saving_mode_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_delete_all_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_delete_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_distinguish_as_mod_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_dot_outline.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_download_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_downvote_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_downvote_filled_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_edit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_error_outline_black_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_error_outline_white_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_error_white_36dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_exit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_expand_less_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_expand_more_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_fast_forward_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_fast_rewind_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_favorite_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_favorite_border_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_file_download_toolbar_white_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filter_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_font_size_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_fullscreen_white_rounded_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_gallery_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_gesture_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_gif_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_give_award_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_hide_post_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_hide_read_posts_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_history_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_home_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_hot_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_image_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_import_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_inbox_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_info_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_interface_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_italic_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_key_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_keyboard_arrow_down_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_keyboard_double_arrow_up_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_language_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_light_theme_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_light_theme_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_link_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_link_post_type_indicator_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_link_round_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_lock_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_log_out_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_login_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_mark_nsfw_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_mic_14dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_miscellaneous_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_mod_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_more_vert_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_multi_reddit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_mute_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_mute_preferences_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_new_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notifications_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_nsfw_off_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_nsfw_on_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_open_link_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_ordered_list_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_pause_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_play_arrow_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_play_circle_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_play_circle_36dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_playback_speed_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_playback_speed_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_poll_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_post_layout_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_preview_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_privacy_policy_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_quote_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_quote_left_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_random_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_refresh_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_remove_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_reply_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_reply_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_report_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_rising_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_save_to_database_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_search_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_search_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_security_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_select_photo_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_select_photo_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_select_query_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_send_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_send_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_settings_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_share_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_share_grey_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_share_toolbar_white_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_sort_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_sort_toolbar_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_spam_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_spoiler_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_spoiler_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_stick_post_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_strikethrough_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_submit_post_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_subreddit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_subscriptions_bottom_app_bar_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_superscript_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_text_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_thumbtack_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_title_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_top_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_undistinguish_as_mod_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_unlock_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_unmark_nsfw_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_unmark_spoiler_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_unmute_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_unordered_list_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_unstick_post_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_upvote_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_upvote_filled_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_upvote_ratio_18dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_user_agreement_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_user_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_verified_user_14dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_video_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_video_quality_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_volume_off_32dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_volume_up_32dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_wallpaper_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_wallpaper_both_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_wallpaper_home_screen_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_wallpaper_lock_screen_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/play_button_round_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/preference_background_bottom.xml ================================================ ================================================ FILE: app/src/main/res/drawable/preference_background_middle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/preference_background_top.xml ================================================ ================================================ FILE: app/src/main/res/drawable/preference_background_top_and_bottom.xml ================================================ ================================================ FILE: app/src/main/res/drawable/private_message_ballon.xml ================================================ ================================================ FILE: app/src/main/res/drawable/splash_screen.xml ================================================ ================================================ FILE: app/src/main/res/drawable/thumbnail_compact_layout_rounded_edge.xml ================================================ ================================================ FILE: app/src/main/res/drawable/trending_search_title_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_about_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_access_time_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_account_circle_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_add_a_photo_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_add_circle_outline_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_add_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_advanced_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_amoled_theme_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_anonymous_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_apply_to_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_arrow_downward_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_arrow_upward_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_bookmark_border_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_bookmark_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_bookmarks_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_check_circle_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_color_lens_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_copy_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_dark_theme_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_data_saving_mode_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_delete_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_download_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_edit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_error_outline_black_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_exit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_filter_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_font_size_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_gallery_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_gesture_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_give_award_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_hide_post_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_hide_read_posts_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_history_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_home_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_image_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_import_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_inbox_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_info_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_interface_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_key_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_keyboard_double_arrow_up_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_language_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_light_theme_preference_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_link_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_link_post_type_indicator_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_lock_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_log_out_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_miscellaneous_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_multi_reddit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_mute_preferences_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_notifications_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_nsfw_off_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_nsfw_on_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_open_link_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_playback_speed_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_poll_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_post_layout_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_preview_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_privacy_policy_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_random_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_refresh_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_reply_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_report_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_save_to_database_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_search_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_security_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_select_photo_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_settings_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_share_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_sort_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_submit_post_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_subreddit_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_subscriptions_bottom_app_bar_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_text_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_user_agreement_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_user_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_video_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_wallpaper_both_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_wallpaper_home_screen_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_wallpaper_lock_screen_day_night_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable-xhdpi/ic_cancel_24dp.xml ================================================ ================================================ FILE: app/src/main/res/font/atkinson_hyperlegible.xml ================================================ ================================================ FILE: app/src/main/res/font/atkinson_hyperlegible_bold_version.xml ================================================ ================================================ FILE: app/src/main/res/font/balsamiq_sans.xml ================================================ ================================================ FILE: app/src/main/res/font/balsamiq_sans_bold_version.xml ================================================ ================================================ FILE: app/src/main/res/font/harmonia_sans.xml ================================================ ================================================ FILE: app/src/main/res/font/inter.xml ================================================ ================================================ FILE: app/src/main/res/font/manrope.xml ================================================ ================================================ FILE: app/src/main/res/font/noto_sans.xml ================================================ ================================================ FILE: app/src/main/res/font/noto_sans_bold_version.xml ================================================ ================================================ FILE: app/src/main/res/font/roboto_condensed.xml ================================================ ================================================ FILE: app/src/main/res/font/roboto_condensed_bold_version.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_account_posts.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_account_saved_thing.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_comment.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_comment_filter_preference.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_comment_filter_usage_listing.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_comment_full_markdown.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_create_multi_reddit.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_custom_theme_listing.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_customize_comment_filter.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_customize_post_filter.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_customize_theme.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_edit_comment.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_edit_multi_reddit.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_edit_post.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_edit_profile.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_fetch_random_subreddit_or_post.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_filtered_thing.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_history.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_inbox.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_lock_screen.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_login.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_login_chrome_custom_tab.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_filter_application.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_filter_preference.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_gallery.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_image.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_link.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_poll.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_text.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_post_video.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_qrcode_scanner.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_report.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_rules.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_search.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_search_history.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_search_result.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_search_subreddits_result.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_search_users_result.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_select_user_flair.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_selected_subreddits.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_send_private_message.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_settings.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_submit_crosspost.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_subscribed_subreddits_multiselection.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_subscribed_thing_listing.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_subscribed_users_multiselection.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_suicide_prevention.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_theme_preview.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_image_or_gif.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_imgur_media.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_multi_reddit_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_post_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_private_messages.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_reddit_gallery.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_subreddit_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_user_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_video.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_view_video_zoomable.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_web_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_wiki.xml ================================================ ================================================ FILE: app/src/main/res/layout/adapter_default_entry.xml ================================================ ================================================ FILE: app/src/main/res/layout/adapter_table_block.xml ================================================ ================================================ FILE: app/src/main/res/layout/app_bar_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/bottom_app_bar.xml ================================================ ================================================ FILE: app/src/main/res/layout/color_picker.xml ================================================