diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java deleted file mode 100644 index ca61ee36570c..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java +++ /dev/null @@ -1,1155 +0,0 @@ -package org.wordpress.android.ui.reader; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Parcelable; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.SparseArray; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; - -import androidx.activity.OnBackPressedCallback; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.WindowInsetsControllerCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; -import androidx.lifecycle.ViewModelProvider; -import androidx.viewpager.widget.ViewPager; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.wordpress.android.R; -import org.wordpress.android.WordPress; -import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.datasets.ReaderPostTable; -import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper; -import org.wordpress.android.fluxc.Dispatcher; -import org.wordpress.android.fluxc.model.PostModel; -import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.fluxc.store.PostStore; -import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded; -import org.wordpress.android.fluxc.store.SiteStore; -import org.wordpress.android.models.ReaderPost; -import org.wordpress.android.models.ReaderTag; -import org.wordpress.android.ui.ActivityLauncher; -import org.wordpress.android.ui.RequestCodes; -import org.wordpress.android.ui.WPLaunchActivity; -import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenInReader; -import org.wordpress.android.ui.deeplinks.DeepLinkOpenWebLinksWithJetpackHelper; -import org.wordpress.android.ui.deeplinks.DeepLinkTrackingUtils; -import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment; -import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayViewModel; -import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureOverlayActions.ForwardToJetpack; -import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureCollectionOverlaySource; -import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper; -import org.wordpress.android.ui.main.BaseAppCompatActivity; -import org.wordpress.android.ui.main.WPMainActivity; -import org.wordpress.android.ui.mysite.SelectedSiteRepository; -import org.wordpress.android.ui.posts.EditPostActivity; -import org.wordpress.android.ui.posts.EditPostActivityConstants; -import org.wordpress.android.ui.prefs.AppPrefs; -import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType; -import org.wordpress.android.ui.reader.actions.ReaderActions; -import org.wordpress.android.ui.reader.actions.ReaderPostActions; -import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId; -import org.wordpress.android.ui.reader.models.ReaderBlogIdPostIdList; -import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter; -import org.wordpress.android.ui.reader.tracker.ReaderTracker; -import org.wordpress.android.ui.reader.tracker.ReaderTrackerType; -import org.wordpress.android.ui.reader.usecases.ReaderGetReadingPreferencesSyncUseCase; -import org.wordpress.android.ui.reader.utils.ReaderPostSeenStatusWrapper; -import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource; -import org.wordpress.android.ui.uploads.UploadActionUseCase; -import org.wordpress.android.ui.uploads.UploadUtils; -import org.wordpress.android.ui.uploads.UploadUtilsWrapper; -import org.wordpress.android.ui.utils.JetpackAppMigrationFlowUtils; -import org.wordpress.android.ui.utils.PreMigrationDeepLinkData; -import org.wordpress.android.util.ActivityUtils; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.FluxCUtils; -import org.wordpress.android.util.NetworkUtils; -import org.wordpress.android.util.ToastUtils; -import org.wordpress.android.util.UriWrapper; -import org.wordpress.android.util.UrlUtilsWrapper; -import org.wordpress.android.util.WPActivityUtils; -import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper; -import org.wordpress.android.util.config.SeenUnseenWithCounterFeatureConfig; -import org.wordpress.android.util.extensions.CompatExtensionsKt; -import org.wordpress.android.widgets.WPSwipeSnackbar; -import org.wordpress.android.widgets.WPViewPager; -import org.wordpress.android.widgets.WPViewPagerTransformer; - -import java.io.Serializable; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.HashSet; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; - -import static org.wordpress.android.ui.main.WPMainActivity.ARG_OPEN_PAGE; -import static org.wordpress.android.ui.main.WPMainActivity.ARG_READER; - -/* - * shows reader post detail fragments in a ViewPager - primarily used for easy swiping between - * posts with a specific tag or in a specific blog, but can also be used to show a single - * post detail. - * - * It also displays intercepted WordPress.com URls in the following forms - * - * http[s]://wordpress.com/read/blogs/{blogId}/posts/{postId} - * http[s]://wordpress.com/read/feeds/{feedId}/posts/{feedItemId} - * http[s]://{username}.wordpress.com/{year}/{month}/{day}/{postSlug} - * - * Will also handle jumping to the comments section, liking a commend and liking a post directly - */ -@AndroidEntryPoint -public class ReaderPostPagerActivity extends BaseAppCompatActivity { - /** - * Type of URL intercepted - */ - private enum InterceptType { - READER_BLOG, - READER_FEED, - WPCOM_POST_SLUG - } - - /** - * operation to perform automatically when opened via deeplinking - */ - public enum DirectOperation { - COMMENT_JUMP, - COMMENT_REPLY, - COMMENT_LIKE, - POST_LIKE, - } - - private WPViewPager mViewPager; - private ProgressBar mProgress; - - private ReaderTag mCurrentTag; - private boolean mIsFeed; - private long mBlogId; - private long mPostId; - private int mCommentId; - private DirectOperation mDirectOperation; - private String mInterceptedUri; - private int mLastSelectedPosition = -1; - private ReaderPostListType mPostListType; - - private boolean mPostSlugsResolutionUnderway; - private boolean mIsRequestingMorePosts; - private boolean mIsSinglePostView; - private boolean mIsRelatedPostView; - - private boolean mBackFromLogin; - - private final HashSet mTrackedPositions = new HashSet<>(); - - @Inject SiteStore mSiteStore; - @Inject ReaderTracker mReaderTracker; - @Inject AnalyticsUtilsWrapper mAnalyticsUtilsWrapper; - @Inject ReaderPostTableWrapper mReaderPostTableWrapper; - @Inject PostStore mPostStore; - @Inject Dispatcher mDispatcher; - @Inject UploadActionUseCase mUploadActionUseCase; - @Inject UploadUtilsWrapper mUploadUtilsWrapper; - @Inject ReaderPostSeenStatusWrapper mPostSeenStatusWrapper; - @Inject SeenUnseenWithCounterFeatureConfig mSeenUnseenWithCounterFeatureConfig; - @Inject UrlUtilsWrapper mUrlUtilsWrapper; - @Inject DeepLinkTrackingUtils mDeepLinkTrackingUtils; - @Inject SelectedSiteRepository mSelectedSiteRepository; - @Inject DeepLinkOpenWebLinksWithJetpackHelper mDeepLinkOpenWebLinksWithJetpackHelper; - @Inject JetpackAppMigrationFlowUtils mJetpackAppMigrationFlowUtils; - private JetpackFeatureFullScreenOverlayViewModel mJetpackFullScreenViewModel; - @Inject AccountStore mAccountStore; - @Inject JetpackFeatureRemovalPhaseHelper mJetpackFeatureRemovalPhaseHelper; - @Inject ReaderGetReadingPreferencesSyncUseCase mGetReadingPreferencesSyncUseCase; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) getApplication()).component().inject(this); - mJetpackFullScreenViewModel = new ViewModelProvider(this).get(JetpackFeatureFullScreenOverlayViewModel.class); - - setContentView(R.layout.reader_activity_post_pager); - - OnBackPressedCallback callback = new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - ReaderPostDetailFragment fragment = getActiveDetailFragment(); - if (fragment != null && fragment.isCustomViewShowing()) { - // if full screen video is showing, hide the custom view rather than navigate back - fragment.hideCustomView(); - } else { - if (fragment != null && fragment.goBackInPostHistory()) { - // noop - fragment moved back to a previous post - } else { - CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); - } - } - } - }; - getOnBackPressedDispatcher().addCallback(this, callback); - - // Start migration flow passing deep link data if requirements are met - if (mJetpackAppMigrationFlowUtils.shouldShowMigrationFlow()) { - PreMigrationDeepLinkData deepLinkData = new PreMigrationDeepLinkData( - getIntent().getAction(), - getIntent().getData() - ); - mJetpackAppMigrationFlowUtils.startJetpackMigrationFlow(deepLinkData); - finish(); - return; - } - - mViewPager = findViewById(R.id.viewpager); - mProgress = findViewById(R.id.progress_loading); - - if (savedInstanceState != null) { - mIsFeed = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_FEED); - mBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID); - mPostId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID); - mDirectOperation = (DirectOperation) savedInstanceState - .getSerializable(ReaderConstants.ARG_DIRECT_OPERATION); - mCommentId = savedInstanceState.getInt(ReaderConstants.ARG_COMMENT_ID); - mIsSinglePostView = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_SINGLE_POST); - mIsRelatedPostView = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_RELATED_POST); - mInterceptedUri = savedInstanceState.getString(ReaderConstants.ARG_INTERCEPTED_URI); - if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { - mPostListType = - (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE); - } - if (savedInstanceState.containsKey(ReaderConstants.ARG_TAG)) { - mCurrentTag = (ReaderTag) savedInstanceState.getSerializable(ReaderConstants.ARG_TAG); - } - mPostSlugsResolutionUnderway = - savedInstanceState.getBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY); - if (savedInstanceState.containsKey(ReaderConstants.KEY_TRACKED_POSITIONS)) { - Serializable positions = savedInstanceState.getSerializable(ReaderConstants.KEY_TRACKED_POSITIONS); - if (positions instanceof HashSet) { - mTrackedPositions.addAll((HashSet) positions); - } - } - } else { - mIsFeed = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_FEED, false); - mBlogId = getIntent().getLongExtra(ReaderConstants.ARG_BLOG_ID, 0); - mPostId = getIntent().getLongExtra(ReaderConstants.ARG_POST_ID, 0); - mDirectOperation = (DirectOperation) getIntent() - .getSerializableExtra(ReaderConstants.ARG_DIRECT_OPERATION); - mCommentId = getIntent().getIntExtra(ReaderConstants.ARG_COMMENT_ID, 0); - mIsSinglePostView = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_SINGLE_POST, false); - mIsRelatedPostView = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_RELATED_POST, false); - mInterceptedUri = getIntent().getStringExtra(ReaderConstants.ARG_INTERCEPTED_URI); - if (getIntent().hasExtra(ReaderConstants.ARG_POST_LIST_TYPE)) { - mPostListType = - (ReaderPostListType) getIntent().getSerializableExtra(ReaderConstants.ARG_POST_LIST_TYPE); - } - if (getIntent().hasExtra(ReaderConstants.ARG_TAG)) { - mCurrentTag = (ReaderTag) getIntent().getSerializableExtra(ReaderConstants.ARG_TAG); - } - } - - if (mPostListType == null) { - mPostListType = ReaderPostListType.TAG_FOLLOWED; - } - - mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { - @Override - public void onPageSelected(int position) { - super.onPageSelected(position); - trackPostAtPositionIfNeeded(position); - - if (mLastSelectedPosition > -1 && mLastSelectedPosition != position) { - // pause the previous web view - important because otherwise embedded content - // will continue to play - ReaderPostDetailFragment lastFragment = getDetailFragmentAtPosition(mLastSelectedPosition); - if (lastFragment != null) { - lastFragment.pauseWebView(); - } - } - - // resume the newly active webView if it was previously paused - ReaderPostDetailFragment thisFragment = getDetailFragmentAtPosition(position); - if (thisFragment != null) { - thisFragment.resumeWebViewIfPaused(); - } - - mLastSelectedPosition = position; - } - }); - - mViewPager.setPageTransformer( - false, - new WPViewPagerTransformer(WPViewPagerTransformer.TransformType.SLIDE_OVER) - ); - - observeOverlayEvents(); - } - - @Override - @Nullable - public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, - @NonNull AttributeSet attrs) { - // enable full screen for Android 33+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getWindow().setDecorFitsSystemWindows(false); - WindowInsetsControllerCompat controller = - new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView()); - controller.hide(WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.navigationBars()); - controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } - return super.onCreateView(parent, name, context, attrs); - } - - private void observeOverlayEvents() { - mJetpackFullScreenViewModel.getAction().observe(this, - action -> { - if (action instanceof ForwardToJetpack) { - if (!mDeepLinkOpenWebLinksWithJetpackHelper.handleOpenLinksInJetpackIfPossible()) { - finishDeepLinkRequestFromOverlay(getIntent().getAction(), getIntent().getData()); - } else { - WPActivityUtils.disableReaderDeeplinks(this); - ActivityLauncher.openJetpackForDeeplink(this, getIntent().getAction(), - new UriWrapper(getIntent().getData())); - finish(); - } - } else { - finishDeepLinkRequestFromOverlay(getIntent().getAction(), getIntent().getData()); - } - }); - } - - private void handleDeepLinking() { - String action = getIntent().getAction(); - Uri uri = getIntent().getData(); - - String host = ""; - if (uri != null) { - host = uri.getHost(); - } - - if (uri == null - || mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures() - || mJetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { - mReaderTracker.trackDeepLink(AnalyticsTracker.Stat.DEEP_LINKED, action, host, uri); - // invalid uri so, just show the entry screen - if (mJetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { - Intent intent = new Intent(this, WPMainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(ARG_OPEN_PAGE, ARG_READER); - startActivity(intent); - } else { - Intent intent = new Intent(this, WPLaunchActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(ARG_OPEN_PAGE, ARG_READER); - startActivity(intent); - } - finish(); - return; - } - - if (!checkAndShowOpenWebLinksWithJetpackOverlayIfNeeded()) { - finishDeepLinkRequest(action, uri); - } - } - - private void finishDeepLinkRequestFromOverlay(String action, Uri uri) { - finishDeepLinkRequest(action, uri); - // We interrupted the normal flow to show the overly, we now need to rerun these methods on a dismiss action - loadPosts(mBlogId, mPostId); - mBackFromLogin = false; - } - - private void finishDeepLinkRequest(String action, Uri uri) { - InterceptType interceptType = InterceptType.READER_BLOG; - String blogIdentifier = null; // can be an id or a slug - String postIdentifier = null; // can be an id or a slug - - mInterceptedUri = uri.toString(); - - List segments = uri.getPathSegments(); - - // Handled URLs look like this: http[s]://wordpress.com/read/feeds/{feedId}/posts/{feedItemId} - // with the first segment being 'read'. - if (segments != null) { - // Builds stripped URI for tracking purposes - UriWrapper wrappedUri = new UriWrapper(uri); - if (segments.get(0).equals("read")) { - if (segments.size() > 2) { - blogIdentifier = segments.get(2); - - if (segments.get(1).equals("blogs")) { - interceptType = InterceptType.READER_BLOG; - } else if (segments.get(1).equals("feeds")) { - interceptType = InterceptType.READER_FEED; - mIsFeed = true; - } - } - - if (segments.size() > 4 && segments.get(3).equals("posts")) { - postIdentifier = segments.get(4); - } - - parseFragment(uri); - mDeepLinkTrackingUtils.track(action, new OpenInReader(wrappedUri), wrappedUri); - showPost(interceptType, blogIdentifier, postIdentifier); - return; - } else if (segments.size() >= 4) { - blogIdentifier = uri.getHost(); - try { - postIdentifier = URLEncoder.encode(segments.get(3), "UTF-8"); - } catch (UnsupportedEncodingException e) { - AppLog.e(AppLog.T.READER, e); - ToastUtils.showToast(this, R.string.error_generic); - } - - parseFragment(uri); - detectLike(uri); - - interceptType = InterceptType.WPCOM_POST_SLUG; - mDeepLinkTrackingUtils.track(action, new OpenInReader(wrappedUri), wrappedUri); - showPost(interceptType, blogIdentifier, postIdentifier); - return; - } - } - - // at this point, just show the entry screen - Intent intent = new Intent(this, WPLaunchActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } - - private void showPost(@NonNull InterceptType interceptType, final String blogIdentifier, final String - postIdentifier) { - if (!TextUtils.isEmpty(blogIdentifier) && !TextUtils.isEmpty(postIdentifier)) { - mIsSinglePostView = true; - mIsRelatedPostView = false; - - switch (interceptType) { - case READER_BLOG: - if (parseIds(blogIdentifier, postIdentifier)) { - mReaderTracker.trackBlogPost( - AnalyticsTracker.Stat.READER_BLOG_POST_INTERCEPTED, - mBlogId, - mPostId - ); - // IDs have now been set so, let ReaderPostPagerActivity normally display the post - } else { - ToastUtils.showToast(this, R.string.error_generic); - } - break; - case READER_FEED: - if (parseIds(blogIdentifier, postIdentifier)) { - mReaderTracker.trackFeedPost( - AnalyticsTracker.Stat.READER_FEED_POST_INTERCEPTED, - mBlogId, - mPostId - ); - // IDs have now been set so, let ReaderPostPagerActivity normally display the post - } else { - ToastUtils.showToast(this, R.string.error_generic); - } - break; - case WPCOM_POST_SLUG: - mReaderTracker.trackBlogPost( - AnalyticsTracker.Stat.READER_WPCOM_BLOG_POST_INTERCEPTED, - blogIdentifier, - postIdentifier, - mCommentId - ); - - // try to get the post from the local db - ReaderPost post = ReaderPostTable.getBlogPost(blogIdentifier, postIdentifier, true); - if (post != null) { - // set the IDs and let ReaderPostPagerActivity normally display the post - mBlogId = post.blogId; - mPostId = post.postId; - } else { - // not stored locally, so request it - ReaderPostActions.requestBlogPost( - blogIdentifier, postIdentifier, - new ReaderActions.OnRequestListener() { - @Override - public void onSuccess(String blogUrl) { - mPostSlugsResolutionUnderway = false; - - // the scheme is removed to match the query pattern in ReaderPostTable - // .getBlogPost - String primaryBlogIdentifier = mUrlUtilsWrapper.removeScheme(blogUrl); - - // getBlogPost utilizes the primaryBlogIdentifier instead of blogIdentifier - // since - // the custom and *.wordpress.com domains need to be used interchangeably since - // they can both be used as the primary domain when identifying the blog_url - // in the ReaderPostTable query. - ReaderPost post = - ReaderPostTable.getBlogPost(primaryBlogIdentifier, postIdentifier, - true); - ReaderEvents.PostSlugsRequestCompleted slugsResolved = (post != null) - ? new ReaderEvents.PostSlugsRequestCompleted(200, post.blogId, - post.postId) - : new ReaderEvents.PostSlugsRequestCompleted(200, 0, 0); - // notify that the slug resolution request has completed - EventBus.getDefault().post(slugsResolved); - - // post wasn't available locally earlier so, track it now - if (post != null) { - trackPost(post.blogId, post.postId); - } - } - - @Override - public void onFailure(int statusCode) { - mPostSlugsResolutionUnderway = false; - // notify that the slug resolution request has completed - EventBus.getDefault() - .post(new ReaderEvents.PostSlugsRequestCompleted(statusCode, 0, 0)); - } - }); - mPostSlugsResolutionUnderway = true; - } - - break; - } - } else { - ToastUtils.showToast(this, R.string.error_generic); - } - } - - private boolean parseIds(String blogIdentifier, String postIdentifier) { - try { - mBlogId = Long.parseLong(blogIdentifier); - mPostId = Long.parseLong(postIdentifier); - } catch (NumberFormatException e) { - AppLog.e(AppLog.T.READER, e); - return false; - } - - return true; - } - - private Boolean checkAndShowOpenWebLinksWithJetpackOverlayIfNeeded() { - if (!isSignedInWPComOrHasWPOrgSite()) return false; - - if (!mDeepLinkOpenWebLinksWithJetpackHelper.shouldShowOpenLinksInJetpackOverlay()) return false; - - mDeepLinkOpenWebLinksWithJetpackHelper.onOverlayShown(); - JetpackFeatureFullScreenOverlayFragment - .newInstance( - null, - false, - true, - SiteCreationSource.UNSPECIFIED, - false, - JetpackFeatureCollectionOverlaySource.UNSPECIFIED) - .show(getSupportFragmentManager(), JetpackFeatureFullScreenOverlayFragment.TAG); - return true; - } - - private Boolean isSignedInWPComOrHasWPOrgSite() { - if (mAccountStore == null || mSiteStore == null) return false; - return FluxCUtils.isSignedInWPComOrHasWPOrgSite(mAccountStore, mSiteStore); - } - - /** - * Parse the URL fragment and interpret it as an operation to perform. For example, a "#comments" fragment is - * interpreted as a direct jump into the comments section of the post. - * - * @param uri the full URI input, including the fragment - */ - private void parseFragment(Uri uri) { - // default to do-nothing w.r.t. comments - mDirectOperation = null; - - if (uri == null || uri.getFragment() == null) { - return; - } - - final String fragment = uri.getFragment(); - - final Pattern fragmentCommentsPattern = Pattern.compile("comments", Pattern.CASE_INSENSITIVE); - final Pattern fragmentCommentIdPattern = Pattern.compile("comment-(\\d+)", Pattern.CASE_INSENSITIVE); - final Pattern fragmentRespondPattern = Pattern.compile("respond", Pattern.CASE_INSENSITIVE); - - // check for the general "#comments" fragment to jump to the comments section - Matcher commentsMatcher = fragmentCommentsPattern.matcher(fragment); - if (commentsMatcher.matches()) { - mDirectOperation = DirectOperation.COMMENT_JUMP; - mCommentId = 0; - return; - } - - // check for the "#respond" fragment to jump to the reply box - Matcher respondMatcher = fragmentRespondPattern.matcher(fragment); - if (respondMatcher.matches()) { - mDirectOperation = DirectOperation.COMMENT_REPLY; - - // check whether we are to reply to a specific comment - final String replyToCommentId = uri.getQueryParameter("replytocom"); - if (replyToCommentId != null) { - try { - mCommentId = Integer.parseInt(replyToCommentId); - } catch (NumberFormatException e) { - AppLog.e(AppLog.T.UTILS, "replytocom cannot be converted to int" + replyToCommentId, e); - } - } - - return; - } - - // check for the "#comment-xyz" fragment to jump to a specific comment - Matcher commentIdMatcher = fragmentCommentIdPattern.matcher(fragment); - if (commentIdMatcher.find() && commentIdMatcher.groupCount() > 0) { - mCommentId = Integer.valueOf(commentIdMatcher.group(1)); - mDirectOperation = DirectOperation.COMMENT_JUMP; - } - } - - /** - * Parse the URL query parameters and detect attempt to like a post or a comment - * - * @param uri the full URI input, including the query parameters - */ - private void detectLike(Uri uri) { - // check whether we are to like something - final boolean doLike = "1".equals(uri.getQueryParameter("like")); - final String likeActor = uri.getQueryParameter("like_actor"); - - if (doLike && likeActor != null && likeActor.trim().length() > 0) { - mDirectOperation = DirectOperation.POST_LIKE; - - // check whether we are to like a specific comment - final String likeCommentId = uri.getQueryParameter("commentid"); - if (likeCommentId != null) { - try { - mCommentId = Integer.parseInt(likeCommentId); - mDirectOperation = DirectOperation.COMMENT_LIKE; - } catch (NumberFormatException e) { - AppLog.e(AppLog.T.UTILS, "commentid cannot be converted to int" + likeCommentId, e); - } - } - } - } - - @Override - protected void onResume() { - super.onResume(); - AppLog.d(T.READER, "TRACK READER ReaderPostPagerActivity > START Count"); - mReaderTracker.start(ReaderTrackerType.PAGED_POST); - EventBus.getDefault().register(this); - - // We register the dispatcher in order to receive the OnPostUploaded event and show the snackbar - mDispatcher.register(this); - - if (!hasPagerAdapter() || mBackFromLogin) { - if (ActivityUtils.isDeepLinking(getIntent()) || ReaderConstants.ACTION_VIEW_POST - .equals(getIntent().getAction())) { - handleDeepLinking(); - } - - loadPosts(mBlogId, mPostId); - - // clear up the back-from-login flag anyway - mBackFromLogin = false; - } - } - - @Override - protected void onPause() { - super.onPause(); - AppLog.d(T.READER, "TRACK READER ReaderPostPagerActivity > STOP Count"); - mReaderTracker.stop(ReaderTrackerType.PAGED_POST); - EventBus.getDefault().unregister(this); - mDispatcher.unregister(this); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean hasPagerAdapter() { - return (mViewPager != null && mViewPager.getAdapter() != null); - } - - private PostPagerAdapter getPagerAdapter() { - if (mViewPager != null && mViewPager.getAdapter() != null) { - return (PostPagerAdapter) mViewPager.getAdapter(); - } else { - return null; - } - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(ReaderConstants.ARG_IS_SINGLE_POST, mIsSinglePostView); - outState.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, mIsRelatedPostView); - outState.putString(ReaderConstants.ARG_INTERCEPTED_URI, mInterceptedUri); - - outState.putSerializable(ReaderConstants.ARG_DIRECT_OPERATION, mDirectOperation); - outState.putInt(ReaderConstants.ARG_COMMENT_ID, mCommentId); - - if (hasCurrentTag()) { - outState.putSerializable(ReaderConstants.ARG_TAG, getCurrentTag()); - } - if (getPostListType() != null) { - outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType()); - } - - ReaderBlogIdPostId id = getAdapterCurrentBlogIdPostId(); - if (id != null) { - outState.putLong(ReaderConstants.ARG_BLOG_ID, id.getBlogId()); - outState.putLong(ReaderConstants.ARG_POST_ID, id.getPostId()); - } - - outState.putBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY, mPostSlugsResolutionUnderway); - - if (mTrackedPositions.size() > 0) { - outState.putSerializable(ReaderConstants.KEY_TRACKED_POSITIONS, mTrackedPositions); - } - - super.onSaveInstanceState(outState); - } - - private ReaderBlogIdPostId getAdapterCurrentBlogIdPostId() { - PostPagerAdapter adapter = getPagerAdapter(); - if (adapter == null) { - return null; - } - return adapter.getCurrentBlogIdPostId(); - } - - private ReaderBlogIdPostId getAdapterBlogIdPostIdAtPosition(int position) { - PostPagerAdapter adapter = getPagerAdapter(); - if (adapter == null) { - return null; - } - return adapter.getBlogIdPostIdAtPosition(position); - } - - /* - * perform analytics tracking and bump the page view for the post at the passed position - * if it hasn't already been done - */ - private void trackPostAtPositionIfNeeded(int position) { - if (!hasPagerAdapter() || mTrackedPositions.contains(position)) { - return; - } - - ReaderBlogIdPostId idPair = getAdapterBlogIdPostIdAtPosition(position); - if (idPair == null) { - return; - } - - AppLog.d(AppLog.T.READER, "reader pager > tracking post at position " + position); - mTrackedPositions.add(position); - - trackPost(idPair.getBlogId(), idPair.getPostId()); - } - - /* - * perform analytics tracking and bump the page view for the post - */ - private void trackPost(long blogId, long postId) { - // bump the page view - ReaderPostActions.bumpPageViewForPost(mSiteStore, blogId, postId); - - if (mSeenUnseenWithCounterFeatureConfig.isEnabled()) { - ReaderPost currentPost = ReaderPostTable.getBlogPost(blogId, postId, true); - if (currentPost != null) { - mPostSeenStatusWrapper.markPostAsSeenSilently(currentPost); - } - } - - // analytics tracking - mReaderTracker.trackPost( - AnalyticsTracker.Stat.READER_ARTICLE_OPENED, - mReaderPostTableWrapper.getBlogPost(blogId, postId, true), - mGetReadingPreferencesSyncUseCase.invoke() - ); - } - - /* - * loads the blogId/postId pairs used to populate the pager adapter - passed blogId/postId will - * be made active after loading unless gotoNext=true, in which case the post after the passed - * one will be made active - */ - private void loadPosts(final long blogId, final long postId) { - new Thread() { - @Override - public void run() { - final ReaderBlogIdPostIdList idList; - if (mIsSinglePostView) { - idList = new ReaderBlogIdPostIdList(); - idList.add(new ReaderBlogIdPostId(blogId, postId)); - } else { - int maxPosts = ReaderConstants.READER_MAX_POSTS_TO_DISPLAY; - switch (getPostListType()) { - case TAG_FOLLOWED: - case TAG_PREVIEW: - idList = ReaderPostTable.getBlogIdPostIdsWithTag(getCurrentTag(), maxPosts); - break; - case BLOG_PREVIEW: - idList = ReaderPostTable.getBlogIdPostIdsInBlog(blogId, maxPosts); - break; - case SEARCH_RESULTS: - default: - return; - } - } - - final int currentPosition = mViewPager.getCurrentItem(); - final int newPosition = idList.indexOf(blogId, postId); - - runOnUiThread(() -> { - if (isFinishing()) { - return; - } - - AppLog.d(T.READER, "reader pager > creating adapter"); - PostPagerAdapter adapter = - new PostPagerAdapter(getSupportFragmentManager(), idList); - mViewPager.setAdapter(adapter); - if (adapter.isValidPosition(newPosition)) { - mViewPager.setCurrentItem(newPosition); - trackPostAtPositionIfNeeded(newPosition); - } else if (adapter.isValidPosition(currentPosition)) { - mViewPager.setCurrentItem(currentPosition); - trackPostAtPositionIfNeeded(currentPosition); - } - - // let the user know they can swipe between posts - if (adapter.getCount() > 1 && !AppPrefs.isReaderSwipeToNavigateShown()) { - WPSwipeSnackbar.show(mViewPager); - AppPrefs.setReaderSwipeToNavigateShown(true); - } - }); - } - }.start(); - } - - private ReaderTag getCurrentTag() { - return mCurrentTag; - } - - private boolean hasCurrentTag() { - return mCurrentTag != null; - } - - private ReaderPostListType getPostListType() { - return mPostListType; - } - - private Fragment getActivePagerFragment() { - PostPagerAdapter adapter = getPagerAdapter(); - if (adapter == null) { - return null; - } - return adapter.getActiveFragment(); - } - - private ReaderPostDetailFragment getActiveDetailFragment() { - Fragment fragment = getActivePagerFragment(); - if (fragment instanceof ReaderPostDetailFragment) { - return (ReaderPostDetailFragment) fragment; - } else { - return null; - } - } - - private Fragment getPagerFragmentAtPosition(int position) { - PostPagerAdapter adapter = getPagerAdapter(); - if (adapter == null) { - return null; - } - return adapter.getFragmentAtPosition(position); - } - - private ReaderPostDetailFragment getDetailFragmentAtPosition(int position) { - Fragment fragment = getPagerFragmentAtPosition(position); - if (fragment instanceof ReaderPostDetailFragment) { - return (ReaderPostDetailFragment) fragment; - } else { - return null; - } - } - - /* - * called when user scrolls towards the last posts - requests older posts with the - * current tag or in the current blog - */ - private void requestMorePosts() { - if (mIsRequestingMorePosts) { - return; - } - - AppLog.d(AppLog.T.READER, "reader pager > requesting older posts"); - switch (getPostListType()) { - case TAG_PREVIEW: - case TAG_FOLLOWED: - ReaderPostServiceStarter.startServiceForTag( - this, - getCurrentTag(), - ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER); - break; - - case BLOG_PREVIEW: - ReaderPostServiceStarter.startServiceForBlog( - this, - mBlogId, - ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER); - break; - case SEARCH_RESULTS: - break; - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ReaderEvents.UpdatePostsStarted event) { - if (isFinishing()) { - return; - } - - mIsRequestingMorePosts = true; - mProgress.setVisibility(View.VISIBLE); - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) { - if (isFinishing()) { - return; - } - - PostPagerAdapter adapter = getPagerAdapter(); - if (adapter == null) { - return; - } - - mIsRequestingMorePosts = false; - mProgress.setVisibility(View.GONE); - - if (event.getResult() == ReaderActions.UpdateResult.HAS_NEW) { - AppLog.d(AppLog.T.READER, "reader pager > older posts received"); - // remember which post to keep active - ReaderBlogIdPostId id = adapter.getCurrentBlogIdPostId(); - long blogId = (id != null ? id.getBlogId() : 0); - long postId = (id != null ? id.getPostId() : 0); - loadPosts(blogId, postId); - } else { - AppLog.d(AppLog.T.READER, "reader pager > all posts loaded"); - adapter.mAllPostsLoaded = true; - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ReaderEvents.DoSignIn event) { - if (isFinishing()) { - return; - } - - mReaderTracker.trackUri(AnalyticsTracker.Stat.READER_SIGN_IN_INITIATED, mInterceptedUri); - ActivityLauncher.loginWithoutMagicLink(this); - } - - /** - * pager adapter containing post detail fragments - **/ - private class PostPagerAdapter extends FragmentStatePagerAdapter { - private final ReaderBlogIdPostIdList mIdList; - private boolean mAllPostsLoaded; - - // this is used to retain created fragments so we can access them in - // getFragmentAtPosition() - necessary because the pager provides no - // built-in way to do this - note that destroyItem() removes fragments - // from this map when they're removed from the adapter, so this doesn't - // retain *every* fragment - private final SparseArray mFragmentMap = new SparseArray<>(); - - @SuppressLint("WrongConstant") PostPagerAdapter(FragmentManager fm, ReaderBlogIdPostIdList ids) { - super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - mIdList = (ReaderBlogIdPostIdList) ids.clone(); - } - - @Override - public void restoreState(Parcelable state, ClassLoader loader) { - // work around "Fragement no longer exists for key" Android bug - // by catching the IllegalStateException - // https://code.google.com/p/android/issues/detail?id=42601 - try { - AppLog.d(AppLog.T.READER, "reader pager > adapter restoreState"); - super.restoreState(state, loader); - } catch (IllegalStateException e) { - AppLog.e(AppLog.T.READER, e); - } - } - - @Override - public Parcelable saveState() { - AppLog.d(AppLog.T.READER, "reader pager > adapter saveState"); - return super.saveState(); - } - - private boolean canRequestMostPosts() { - return !mAllPostsLoaded - && !mIsSinglePostView - && (mIdList != null && mIdList.size() < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) - && NetworkUtils.isNetworkAvailable(ReaderPostPagerActivity.this); - } - - boolean isValidPosition(int position) { - return (position >= 0 && position < getCount()); - } - - @Override - public int getCount() { - return mIdList.size(); - } - - @Override - public Fragment getItem(int position) { - if ((position == getCount() - 1) && canRequestMostPosts()) { - requestMorePosts(); - } - - return ReaderPostDetailFragment.Companion.newInstance( - mIsFeed, - mIdList.get(position).getBlogId(), - mIdList.get(position).getPostId(), - mDirectOperation, - mCommentId, - mIsRelatedPostView, - mInterceptedUri, - getPostListType(), - mPostSlugsResolutionUnderway); - } - - @Override - public @NonNull Object instantiateItem(ViewGroup container, int position) { - Object item = super.instantiateItem(container, position); - if (item instanceof Fragment) { - mFragmentMap.put(position, (Fragment) item); - } - return item; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object object) { - mFragmentMap.remove(position); - super.destroyItem(container, position, object); - } - - private Fragment getActiveFragment() { - return getFragmentAtPosition(mViewPager.getCurrentItem()); - } - - private Fragment getFragmentAtPosition(int position) { - if (isValidPosition(position)) { - return mFragmentMap.get(position); - } else { - return null; - } - } - - private ReaderBlogIdPostId getCurrentBlogIdPostId() { - return getBlogIdPostIdAtPosition(mViewPager.getCurrentItem()); - } - - ReaderBlogIdPostId getBlogIdPostIdAtPosition(int position) { - if (isValidPosition(position)) { - return mIdList.get(position); - } else { - return null; - } - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case RequestCodes.EDIT_POST: - if (resultCode != Activity.RESULT_OK || data == null || isFinishing()) { - return; - } - int localId = data.getIntExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, 0); - final SiteModel site = (SiteModel) data.getSerializableExtra(WordPress.SITE); - final PostModel post = mPostStore.getPostByLocalPostId(localId); - - if (EditPostActivity.checkToRestart(data)) { - ActivityLauncher.editPostOrPageForResult(data, ReaderPostPagerActivity.this, site, - data.getIntExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, 0)); - - // a restart will happen so, no need to continue here - break; - } - - View snackbarAttachView = findViewById(R.id.coordinator); - if (site != null && post != null && snackbarAttachView != null) { - mUploadUtilsWrapper.handleEditPostResultSnackbars( - this, - snackbarAttachView, - data, - post, - site, - mUploadActionUseCase.getUploadAction(post), - v -> UploadUtils.publishPost(ReaderPostPagerActivity.this, post, site, mDispatcher)); - } - break; - case RequestCodes.DO_LOGIN: - if (resultCode == Activity.RESULT_OK) { - mBackFromLogin = true; - } - break; - case RequestCodes.NO_REBLOG_SITE: - if (resultCode == Activity.RESULT_OK) { - finish(); // Finish activity to make My Site page visible - } - break; - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPostUploaded(OnPostUploaded event) { - SiteModel site = mSiteStore.getSiteByLocalId(mSelectedSiteRepository.getSelectedSiteLocalId()); - View snackbarAttachView = findViewById(R.id.coordinator); - if (site != null && event.post != null && snackbarAttachView != null) { - mUploadUtilsWrapper.onPostUploadedSnackbarHandler( - this, - snackbarAttachView, - event.isError(), - event.isFirstTimePublish, - event.post, - null, - site); - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.kt new file mode 100644 index 000000000000..df9e914f2e81 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.kt @@ -0,0 +1,1210 @@ +// TODO this class is deprecated due to the use of FragmentStatePagerAdapter which should be updated to a ViewPager2 +@file:Suppress("DEPRECATION") + +package org.wordpress.android.ui.reader + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import android.util.AttributeSet +import android.util.SparseArray +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import androidx.activity.OnBackPressedCallback +import androidx.core.os.BundleCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.ViewModelProvider +import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener +import dagger.hilt.android.AndroidEntryPoint +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.datasets.ReaderPostTable +import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.PostStore +import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.WPLaunchActivity +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenInReader +import org.wordpress.android.ui.deeplinks.DeepLinkOpenWebLinksWithJetpackHelper +import org.wordpress.android.ui.deeplinks.DeepLinkTrackingUtils +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment.Companion.newInstance +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayViewModel +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureOverlayActions +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureOverlayActions.ForwardToJetpack +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureCollectionOverlaySource +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper +import org.wordpress.android.ui.main.BaseAppCompatActivity +import org.wordpress.android.ui.main.WPMainActivity +import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.ui.posts.EditPostActivity.Companion.checkToRestart +import org.wordpress.android.ui.posts.EditPostActivityConstants +import org.wordpress.android.ui.prefs.AppPrefs +import org.wordpress.android.ui.reader.ReaderEvents.DoSignIn +import org.wordpress.android.ui.reader.ReaderEvents.PostSlugsRequestCompleted +import org.wordpress.android.ui.reader.ReaderEvents.UpdatePostsEnded +import org.wordpress.android.ui.reader.ReaderEvents.UpdatePostsStarted +import org.wordpress.android.ui.reader.ReaderPostDetailFragment.Companion.newInstance +import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType +import org.wordpress.android.ui.reader.actions.ReaderActions +import org.wordpress.android.ui.reader.actions.ReaderActions.OnRequestListener +import org.wordpress.android.ui.reader.actions.ReaderPostActions +import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId +import org.wordpress.android.ui.reader.models.ReaderBlogIdPostIdList +import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter +import org.wordpress.android.ui.reader.tracker.ReaderTracker +import org.wordpress.android.ui.reader.tracker.ReaderTrackerType +import org.wordpress.android.ui.reader.usecases.ReaderGetReadingPreferencesSyncUseCase +import org.wordpress.android.ui.reader.utils.ReaderPostSeenStatusWrapper +import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource +import org.wordpress.android.ui.uploads.UploadActionUseCase +import org.wordpress.android.ui.uploads.UploadUtils +import org.wordpress.android.ui.uploads.UploadUtilsWrapper +import org.wordpress.android.ui.utils.JetpackAppMigrationFlowUtils +import org.wordpress.android.ui.utils.PreMigrationDeepLinkData +import org.wordpress.android.util.ActivityUtils +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.FluxCUtils +import org.wordpress.android.util.NetworkUtils +import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.UriWrapper +import org.wordpress.android.util.UrlUtilsWrapper +import org.wordpress.android.util.WPActivityUtils +import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper +import org.wordpress.android.util.config.SeenUnseenWithCounterFeatureConfig +import org.wordpress.android.util.extensions.onBackPressedCompat +import org.wordpress.android.widgets.WPSwipeSnackbar +import org.wordpress.android.widgets.WPViewPager +import org.wordpress.android.widgets.WPViewPagerTransformer +import java.io.UnsupportedEncodingException +import java.net.URLEncoder +import java.util.regex.Pattern +import javax.inject.Inject + +/* +* shows reader post detail fragments in a ViewPager - primarily used for easy swiping between +* posts with a specific tag or in a specific blog, but can also be used to show a single +* post detail. +* +* It also displays intercepted WordPress.com URls in the following forms +* +* http[s]://wordpress.com/read/blogs/{blogId}/posts/{postId} +* http[s]://wordpress.com/read/feeds/{feedId}/posts/{feedItemId} +* http[s]://{username}.wordpress.com/{year}/{month}/{day}/{postSlug} +* +* Will also handle jumping to the comments section, liking a commend and liking a post directly +*/ +@AndroidEntryPoint +@Suppress("LargeClass") +class ReaderPostPagerActivity : BaseAppCompatActivity() { + /** + * Type of URL intercepted + */ + private enum class InterceptType { + READER_BLOG, + READER_FEED, + WPCOM_POST_SLUG + } + + /** + * operation to perform automatically when opened via deeplinking + */ + enum class DirectOperation { + COMMENT_JUMP, + COMMENT_REPLY, + COMMENT_LIKE, + POST_LIKE, + } + + private lateinit var viewPager: WPViewPager + private var progressBar: ProgressBar? = null + + private var currentTag: ReaderTag? = null + private var isFeed = false + private var blogId: Long = 0 + private var postId: Long = 0 + private var commentId = 0 + private var directOperation: DirectOperation? = null + private var interceptedUri: String? = null + private var lastSelectedPosition = -1 + private var postListType: ReaderPostListType? = null + + private var postSlugsResolutionUnderway = false + private var isRequestingMorePosts = false + private var isSinglePostView = false + private var isRelatedPostView = false + + private var backFromLogin = false + + private val trackedPositions = HashSet() + + @Inject + lateinit var siteStore: SiteStore + + @Inject + lateinit var readerTracker: ReaderTracker + + @Inject + lateinit var analyticsUtilsWrapper: AnalyticsUtilsWrapper + + @Inject + lateinit var readerPostTableWrapper: ReaderPostTableWrapper + + @Inject + lateinit var postStore: PostStore + + @Inject + lateinit var dispatcher: Dispatcher + + @Inject + lateinit var uploadActionUseCase: UploadActionUseCase + + @Inject + lateinit var uploadUtilsWrapper: UploadUtilsWrapper + + @Inject + lateinit var postSeenStatusWrapper: ReaderPostSeenStatusWrapper + + @Inject + lateinit var seenUnseenWithCounterFeatureConfig: SeenUnseenWithCounterFeatureConfig + + @Inject + lateinit var urlUtilsWrapper: UrlUtilsWrapper + + @Inject + lateinit var deepLinkTrackingUtils: DeepLinkTrackingUtils + + @Inject + lateinit var selectedSiteRepository: SelectedSiteRepository + + @Inject + lateinit var deepLinkOpenWebLinksWithJetpackHelper: DeepLinkOpenWebLinksWithJetpackHelper + + @Inject + lateinit var jetpackAppMigrationFlowUtils: JetpackAppMigrationFlowUtils + private var jetpackFullScreenViewModel: JetpackFeatureFullScreenOverlayViewModel? = null + + @Inject + lateinit var mAccountStore: AccountStore + + @Inject + lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper + + @Inject + lateinit var getReadingPreferencesSyncUseCase: ReaderGetReadingPreferencesSyncUseCase + + @Suppress("LongMethod") + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (application as WordPress).component().inject(this) + jetpackFullScreenViewModel = + ViewModelProvider(this)[JetpackFeatureFullScreenOverlayViewModel::class.java] + + setContentView(R.layout.reader_activity_post_pager) + + val callback: OnBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val fragment = activeDetailFragment + if (fragment != null && fragment.isCustomViewShowing) { + // if full screen video is showing, hide the custom view rather than navigate back + fragment.hideCustomView() + } else { + if (fragment != null && fragment.goBackInPostHistory()) { + // noop - fragment moved back to a previous post + } else { + onBackPressedDispatcher.onBackPressedCompat(this) + } + } + } + } + onBackPressedDispatcher.addCallback(this, callback) + + // Start migration flow passing deep link data if requirements are met + if (jetpackAppMigrationFlowUtils.shouldShowMigrationFlow()) { + val deepLinkData = PreMigrationDeepLinkData( + intent.action, + intent.data + ) + jetpackAppMigrationFlowUtils.startJetpackMigrationFlow(deepLinkData) + finish() + return + } + + viewPager = findViewById(R.id.viewpager) + progressBar = findViewById(R.id.progress_loading) + + if (savedInstanceState != null) { + isFeed = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_FEED) + blogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID) + postId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID) + directOperation = BundleCompat.getSerializable( + savedInstanceState, + ReaderConstants.ARG_DIRECT_OPERATION, + DirectOperation::class.java + ) + commentId = savedInstanceState.getInt(ReaderConstants.ARG_COMMENT_ID) + isSinglePostView = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_SINGLE_POST) + isRelatedPostView = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_RELATED_POST) + interceptedUri = savedInstanceState.getString(ReaderConstants.ARG_INTERCEPTED_URI) + if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { + postListType = + BundleCompat.getSerializable( + savedInstanceState, + ReaderConstants.ARG_POST_LIST_TYPE, + ReaderPostListType::class.java + ) + } + if (savedInstanceState.containsKey(ReaderConstants.ARG_TAG)) { + currentTag = + BundleCompat.getSerializable( + savedInstanceState, + ReaderConstants.ARG_TAG, + ReaderTag::class.java + ) + } + postSlugsResolutionUnderway = + savedInstanceState.getBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY) + if (savedInstanceState.containsKey(ReaderConstants.KEY_TRACKED_POSITIONS)) { + BundleCompat.getSerializable( + savedInstanceState, + ReaderConstants.KEY_TRACKED_POSITIONS, + HashSet::class.java + )?.let { positions -> + @Suppress("UNCHECKED_CAST") + trackedPositions.addAll(positions as HashSet) + } + } + } else { + isFeed = intent.getBooleanExtra(ReaderConstants.ARG_IS_FEED, false) + blogId = intent.getLongExtra(ReaderConstants.ARG_BLOG_ID, 0) + postId = intent.getLongExtra(ReaderConstants.ARG_POST_ID, 0) + commentId = intent.getIntExtra(ReaderConstants.ARG_COMMENT_ID, 0) + isSinglePostView = intent.getBooleanExtra(ReaderConstants.ARG_IS_SINGLE_POST, false) + isRelatedPostView = intent.getBooleanExtra(ReaderConstants.ARG_IS_RELATED_POST, false) + interceptedUri = intent.getStringExtra(ReaderConstants.ARG_INTERCEPTED_URI) + if (intent.hasExtra(ReaderConstants.ARG_DIRECT_OPERATION)) { + directOperation = BundleCompat.getSerializable( + intent.extras!!, + ReaderConstants.ARG_DIRECT_OPERATION, + DirectOperation::class.java + ) + } + if (intent.hasExtra(ReaderConstants.ARG_POST_LIST_TYPE)) { + postListType = + BundleCompat.getSerializable( + intent.extras!!, + ReaderConstants.ARG_POST_LIST_TYPE, + ReaderPostListType::class.java + ) + } + if (intent.hasExtra(ReaderConstants.ARG_TAG)) { + currentTag = BundleCompat.getSerializable( + intent.extras!!, + ReaderConstants.ARG_TAG, + ReaderTag::class.java + ) + } + } + + if (postListType == null) { + postListType = ReaderPostListType.TAG_FOLLOWED + } + + viewPager.addOnPageChangeListener(object : SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + trackPostAtPositionIfNeeded(position) + + if (lastSelectedPosition > -1 && lastSelectedPosition != position) { + // pause the previous web view - important because otherwise embedded content + // will continue to play + val lastFragment = getDetailFragmentAtPosition(lastSelectedPosition) + lastFragment?.pauseWebView() + } + + // resume the newly active webView if it was previously paused + val thisFragment = getDetailFragmentAtPosition(position) + thisFragment?.resumeWebViewIfPaused() + + lastSelectedPosition = position + } + }) + + viewPager.setPageTransformer( + false, + WPViewPagerTransformer(WPViewPagerTransformer.TransformType.SLIDE_OVER) + ) + + observeOverlayEvents() + } + + @Suppress("DEPRECATION") + override fun onCreateView( + parent: View?, name: String, context: Context, + attrs: AttributeSet + ): View? { + // enable full screen for Android 33+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + window.setDecorFitsSystemWindows(false) + val controller = + WindowInsetsControllerCompat(window, window.decorView) + controller.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + return super.onCreateView(parent, name, context, attrs) + } + + private fun observeOverlayEvents() { + jetpackFullScreenViewModel!!.action.observe( + this + ) { action: JetpackFeatureOverlayActions? -> + if (action is ForwardToJetpack) { + if (!deepLinkOpenWebLinksWithJetpackHelper.handleOpenLinksInJetpackIfPossible()) { + finishDeepLinkRequestFromOverlay(intent.action!!, intent.data!!) + } else { + WPActivityUtils.disableReaderDeeplinks(this) + ActivityLauncher.openJetpackForDeeplink( + this, intent.action, + UriWrapper(intent.data!!) + ) + finish() + } + } else { + finishDeepLinkRequestFromOverlay(intent.action!!, intent.data!!) + } + } + } + + private fun handleDeepLinking() { + val action = intent.action + val uri = intent.data + + var host: String? = "" + if (uri != null) { + host = uri.host + } + + if (uri == null || jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures() + || jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage() + ) { + readerTracker.trackDeepLink(AnalyticsTracker.Stat.DEEP_LINKED, action!!, host!!, uri) + // invalid uri so, just show the entry screen + if (jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + val intent = Intent(this, WPMainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_READER) + startActivity(intent) + } else { + val intent = Intent(this, WPLaunchActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_READER) + startActivity(intent) + } + finish() + return + } + + if (!checkAndShowOpenWebLinksWithJetpackOverlayIfNeeded()) { + finishDeepLinkRequest(action!!, uri) + } + } + + private fun finishDeepLinkRequestFromOverlay(action: String, uri: Uri) { + finishDeepLinkRequest(action, uri) + // We interrupted the normal flow to show the overly, we now need to rerun these methods on a dismiss action + loadPosts(blogId, postId) + backFromLogin = false + } + + @Suppress("NestedBlockDepth", "MagicNumber") + private fun finishDeepLinkRequest(action: String, uri: Uri) { + var interceptType = InterceptType.READER_BLOG + var blogIdentifier: String? = null // can be an id or a slug + var postIdentifier: String? = null // can be an id or a slug + + interceptedUri = uri.toString() + + val segments = uri.pathSegments + + // Handled URLs look like this: http[s]://wordpress.com/read/feeds/{feedId}/posts/{feedItemId} + // with the first segment being 'read'. + if (segments != null) { + // Builds stripped URI for tracking purposes + val wrappedUri = UriWrapper(uri) + if (segments[0] == "read") { + if (segments.size > 2) { + blogIdentifier = segments[2] + + if (segments[1] == "blogs") { + interceptType = InterceptType.READER_BLOG + } else if (segments[1] == "feeds") { + interceptType = InterceptType.READER_FEED + isFeed = true + } + } + + if (segments.size > 4 && segments[3] == "posts") { + postIdentifier = segments[4] + } + + parseFragment(uri) + deepLinkTrackingUtils.track(action, OpenInReader(wrappedUri), wrappedUri) + showPost(interceptType, blogIdentifier, postIdentifier) + return + } else if (segments.size >= 4) { + blogIdentifier = uri.host + try { + postIdentifier = URLEncoder.encode(segments[3], "UTF-8") + } catch (e: UnsupportedEncodingException) { + AppLog.e(AppLog.T.READER, e) + ToastUtils.showToast(this, R.string.error_generic) + } + + parseFragment(uri) + detectLike(uri) + + interceptType = InterceptType.WPCOM_POST_SLUG + deepLinkTrackingUtils.track(action, OpenInReader(wrappedUri), wrappedUri) + showPost(interceptType, blogIdentifier, postIdentifier) + return + } + } + + // at this point, just show the entry screen + val intent = Intent(this, WPLaunchActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } + + @Suppress("MagicNumber") + private fun showPost( + interceptType: InterceptType, + blogIdentifier: String?, + postIdentifier: String? + ) { + if (!blogIdentifier.isNullOrEmpty() && !postIdentifier.isNullOrEmpty()) { + isSinglePostView = true + isRelatedPostView = false + + when (interceptType) { + InterceptType.READER_BLOG -> if (parseIds( + blogIdentifier, + postIdentifier + ) + ) { + readerTracker.trackBlogPost( + AnalyticsTracker.Stat.READER_BLOG_POST_INTERCEPTED, + blogId, + postId + ) + // IDs have now been set so, let ReaderPostPagerActivity normally display the post + } else { + ToastUtils.showToast(this, R.string.error_generic) + } + + InterceptType.READER_FEED -> if (parseIds(blogIdentifier, postIdentifier)) { + readerTracker.trackFeedPost( + AnalyticsTracker.Stat.READER_FEED_POST_INTERCEPTED, + blogId, + postId + ) + // IDs have now been set so, let ReaderPostPagerActivity normally display the post + } else { + ToastUtils.showToast(this, R.string.error_generic) + } + + InterceptType.WPCOM_POST_SLUG -> { + readerTracker.trackBlogPost( + AnalyticsTracker.Stat.READER_WPCOM_BLOG_POST_INTERCEPTED, + blogIdentifier, + postIdentifier, + commentId + ) + + // try to get the post from the local db + val post = ReaderPostTable.getBlogPost(blogIdentifier, postIdentifier, true) + if (post != null) { + // set the IDs and let ReaderPostPagerActivity normally display the post + blogId = post.blogId + postId = post.postId + } else { + // not stored locally, so request it + ReaderPostActions.requestBlogPost( + blogIdentifier, postIdentifier, + object : OnRequestListener { + override fun onSuccess(blogUrl: String?) { + postSlugsResolutionUnderway = false + + // the scheme is removed to match the query pattern in ReaderPostTable + // .getBlogPost + val primaryBlogIdentifier = urlUtilsWrapper.removeScheme( + blogUrl!! + ) + + // getBlogPost utilizes the primaryBlogIdentifier instead of blogIdentifier + // since + // the custom and *.wordpress.com domains need to be used interchangeably since + // they can both be used as the primary domain when identifying the blog_url + // in the ReaderPostTable query. + val readerPost = + ReaderPostTable.getBlogPost( + primaryBlogIdentifier, postIdentifier, + true + ) + val slugsResolved = if (readerPost != null) + PostSlugsRequestCompleted( + 200, readerPost.blogId, + readerPost.postId + ) + else + PostSlugsRequestCompleted(200, 0, 0) + // notify that the slug resolution request has completed + EventBus.getDefault().post(slugsResolved) + + // post wasn't available locally earlier so, track it now + if (readerPost != null) { + trackPost(readerPost.blogId, readerPost.postId) + } + } + + override fun onFailure(statusCode: Int) { + postSlugsResolutionUnderway = false + // notify that the slug resolution request has completed + EventBus.getDefault() + .post(PostSlugsRequestCompleted(statusCode, 0, 0)) + } + }) + postSlugsResolutionUnderway = true + } + } + } + } else { + ToastUtils.showToast(this, R.string.error_generic) + } + } + + private fun parseIds(blogIdentifier: String, postIdentifier: String): Boolean { + try { + blogId = blogIdentifier.toLong() + postId = postIdentifier.toLong() + return true + } catch (e: NumberFormatException) { + AppLog.e(AppLog.T.READER, e) + return false + } + } + + @Suppress("ReturnCount") + private fun checkAndShowOpenWebLinksWithJetpackOverlayIfNeeded(): Boolean { + if (!isSignedInWPComOrHasWPOrgSite) return false + + if (!deepLinkOpenWebLinksWithJetpackHelper.shouldShowOpenLinksInJetpackOverlay()) return false + + deepLinkOpenWebLinksWithJetpackHelper.onOverlayShown() + newInstance( + null, + isSiteCreationOverlay = false, + isDeepLinkOverlay = true, + siteCreationSource = SiteCreationSource.UNSPECIFIED, + isFeatureCollectionOverlay = false, + featureCollectionOverlaySource = JetpackFeatureCollectionOverlaySource.UNSPECIFIED + ) + .show(supportFragmentManager, JetpackFeatureFullScreenOverlayFragment.TAG) + return true + } + + private val isSignedInWPComOrHasWPOrgSite: Boolean + get() { + return FluxCUtils.isSignedInWPComOrHasWPOrgSite(mAccountStore, siteStore) + } + + /** + * Parse the URL fragment and interpret it as an operation to perform. For example, a "#comments" fragment is + * interpreted as a direct jump into the comments section of the post. + * + * @param uri the full URI input, including the fragment + */ + @Suppress("ReturnCount") + private fun parseFragment(uri: Uri?) { + // default to do-nothing w.r.t. comments + directOperation = null + + if (uri == null || uri.fragment == null) { + return + } + + val fragment: CharSequence = uri.fragment ?: "" + + val fragmentCommentsPattern = Pattern.compile("comments", Pattern.CASE_INSENSITIVE) + val fragmentCommentIdPattern = Pattern.compile("comment-(\\d+)", Pattern.CASE_INSENSITIVE) + val fragmentRespondPattern = Pattern.compile("respond", Pattern.CASE_INSENSITIVE) + + // check for the general "#comments" fragment to jump to the comments section + val commentsMatcher = fragmentCommentsPattern.matcher(fragment) + if (commentsMatcher.matches()) { + directOperation = DirectOperation.COMMENT_JUMP + commentId = 0 + return + } + + // check for the "#respond" fragment to jump to the reply box + val respondMatcher = fragmentRespondPattern.matcher(fragment) + if (respondMatcher.matches()) { + directOperation = DirectOperation.COMMENT_REPLY + + // check whether we are to reply to a specific comment + val replyToCommentId = uri.getQueryParameter("replytocom") + if (replyToCommentId != null) { + try { + commentId = replyToCommentId.toInt() + } catch (e: NumberFormatException) { + AppLog.e( + AppLog.T.UTILS, + "replytocom cannot be converted to int$replyToCommentId", e + ) + } + } + + return + } + + // check for the "#comment-xyz" fragment to jump to a specific comment + val commentIdMatcher = fragmentCommentIdPattern.matcher(fragment) + if (commentIdMatcher.find() && commentIdMatcher.groupCount() > 0) { + commentIdMatcher.group(1)?.toInt() ?: 0 + directOperation = DirectOperation.COMMENT_JUMP + } + } + + /** + * Parse the URL query parameters and detect attempt to like a post or a comment + * + * @param uri the full URI input, including the query parameters + */ + private fun detectLike(uri: Uri) { + // check whether we are to like something + val doLike = "1" == uri.getQueryParameter("like") + val likeActor = uri.getQueryParameter("like_actor") + + if (doLike && likeActor != null && likeActor.trim { it <= ' ' }.isNotEmpty()) { + directOperation = DirectOperation.POST_LIKE + + // check whether we are to like a specific comment + val likeCommentId = uri.getQueryParameter("commentid") + if (likeCommentId != null) { + try { + commentId = likeCommentId.toInt() + directOperation = DirectOperation.COMMENT_LIKE + } catch (e: NumberFormatException) { + AppLog.e( + AppLog.T.UTILS, + "commentid cannot be converted to int$likeCommentId", e + ) + } + } + } + } + + override fun onResume() { + super.onResume() + AppLog.d(AppLog.T.READER, "TRACK READER ReaderPostPagerActivity > START Count") + readerTracker.start(ReaderTrackerType.PAGED_POST) + EventBus.getDefault().register(this) + + // We register the dispatcher in order to receive the OnPostUploaded event and show the snackbar + dispatcher.register(this) + + if (!hasPagerAdapter() || backFromLogin) { + if (ActivityUtils.isDeepLinking(intent) || (ReaderConstants.ACTION_VIEW_POST + == intent.action) + ) { + handleDeepLinking() + } + + loadPosts(blogId, postId) + + // clear up the back-from-login flag anyway + backFromLogin = false + } + } + + override fun onPause() { + super.onPause() + AppLog.d(AppLog.T.READER, "TRACK READER ReaderPostPagerActivity > STOP Count") + readerTracker.stop(ReaderTrackerType.PAGED_POST) + EventBus.getDefault().unregister(this) + dispatcher.unregister(this) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finish() + return true + } + return super.onOptionsItemSelected(item) + } + + private fun hasPagerAdapter(): Boolean { + return (viewPager.adapter != null) + } + + private val pagerAdapter: PostPagerAdapter? + get() { + return if (viewPager.adapter != null) { + viewPager.adapter as PostPagerAdapter? + } else { + null + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(ReaderConstants.ARG_IS_SINGLE_POST, isSinglePostView) + outState.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, isRelatedPostView) + outState.putString(ReaderConstants.ARG_INTERCEPTED_URI, interceptedUri) + + outState.putSerializable(ReaderConstants.ARG_DIRECT_OPERATION, directOperation) + outState.putInt(ReaderConstants.ARG_COMMENT_ID, commentId) + + if (hasCurrentTag()) { + outState.putSerializable(ReaderConstants.ARG_TAG, currentTag) + } + if (postListType != null) { + outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, postListType) + } + + val id = adapterCurrentBlogIdPostId + if (id != null) { + outState.putLong(ReaderConstants.ARG_BLOG_ID, id.blogId) + outState.putLong(ReaderConstants.ARG_POST_ID, id.postId) + } + + outState.putBoolean( + ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY, + postSlugsResolutionUnderway + ) + + if (trackedPositions.size > 0) { + outState.putSerializable(ReaderConstants.KEY_TRACKED_POSITIONS, trackedPositions) + } + + super.onSaveInstanceState(outState) + } + + private val adapterCurrentBlogIdPostId: ReaderBlogIdPostId? + get() { + val adapter = pagerAdapter ?: return null + return adapter.currentBlogIdPostId + } + + private fun getAdapterBlogIdPostIdAtPosition(position: Int): ReaderBlogIdPostId? { + val adapter = pagerAdapter ?: return null + return adapter.getBlogIdPostIdAtPosition(position) + } + + /* + * perform analytics tracking and bump the page view for the post at the passed position + * if it hasn't already been done + */ + private fun trackPostAtPositionIfNeeded(position: Int) { + if (!hasPagerAdapter() || trackedPositions.contains(position)) { + return + } + + val idPair = getAdapterBlogIdPostIdAtPosition(position) ?: return + + AppLog.d( + AppLog.T.READER, + "reader pager > tracking post at position $position" + ) + trackedPositions.add(position) + + trackPost(idPair.blogId, idPair.postId) + } + + /* + * perform analytics tracking and bump the page view for the post + */ + private fun trackPost(blogId: Long, postId: Long) { + // bump the page view + ReaderPostActions.bumpPageViewForPost(siteStore, blogId, postId) + + if (seenUnseenWithCounterFeatureConfig.isEnabled()) { + val currentPost = ReaderPostTable.getBlogPost(blogId, postId, true) + if (currentPost != null) { + postSeenStatusWrapper.markPostAsSeenSilently(currentPost) + } + } + + // analytics tracking + readerTracker.trackPost( + AnalyticsTracker.Stat.READER_ARTICLE_OPENED, + readerPostTableWrapper.getBlogPost(blogId, postId, true), + getReadingPreferencesSyncUseCase.invoke() + ) + } + + /* + * loads the blogId/postId pairs used to populate the pager adapter - passed blogId/postId will + * be made active after loading unless gotoNext=true, in which case the post after the passed + * one will be made active + */ + private fun loadPosts(blogId: Long, postId: Long) { + object : Thread() { + override fun run() { + val idList: ReaderBlogIdPostIdList + if (isSinglePostView) { + idList = ReaderBlogIdPostIdList() + idList.add(ReaderBlogIdPostId(blogId, postId)) + } else { + val maxPosts = ReaderConstants.READER_MAX_POSTS_TO_DISPLAY + idList = + when (postListType) { + ReaderPostListType.TAG_FOLLOWED, + ReaderPostListType.TAG_PREVIEW -> + ReaderPostTable.getBlogIdPostIdsWithTag(currentTag, maxPosts) + + ReaderPostListType.BLOG_PREVIEW -> ReaderPostTable.getBlogIdPostIdsInBlog( + blogId, + maxPosts + ) + + ReaderPostListType.SEARCH_RESULTS -> return + else -> return + } + } + + val currentPosition = viewPager.currentItem + val newPosition = idList.indexOf(blogId, postId) + + runOnUiThread { + if (isFinishing) { + return@runOnUiThread + } + AppLog.d( + AppLog.T.READER, + "reader pager > creating adapter" + ) + val adapter = + PostPagerAdapter(supportFragmentManager, idList) + viewPager.adapter = adapter + if (adapter.isValidPosition(newPosition)) { + viewPager.currentItem = newPosition + trackPostAtPositionIfNeeded(newPosition) + } else if (adapter.isValidPosition(currentPosition)) { + viewPager.currentItem = currentPosition + trackPostAtPositionIfNeeded(currentPosition) + } + + // let the user know they can swipe between posts + if (adapter.count > 1 && !AppPrefs.isReaderSwipeToNavigateShown()) { + WPSwipeSnackbar.show(viewPager) + AppPrefs.setReaderSwipeToNavigateShown(true) + } + } + } + }.start() + } + + private fun hasCurrentTag(): Boolean { + return currentTag != null + } + + private val activePagerFragment: Fragment? + get() { + val adapter = pagerAdapter ?: return null + return adapter.activeFragment + } + + private val activeDetailFragment: ReaderPostDetailFragment? + get() = activePagerFragment as? ReaderPostDetailFragment + + private fun getPagerFragmentAtPosition(position: Int): Fragment? { + val adapter = pagerAdapter ?: return null + return adapter.getFragmentAtPosition(position) + } + + private fun getDetailFragmentAtPosition(position: Int): ReaderPostDetailFragment? { + return getPagerFragmentAtPosition(position) as? ReaderPostDetailFragment + } + + /* + * called when user scrolls towards the last posts - requests older posts with the + * current tag or in the current blog + */ + private fun requestMorePosts() { + if (isRequestingMorePosts) { + return + } + + AppLog.d(AppLog.T.READER, "reader pager > requesting older posts") + when (postListType) { + ReaderPostListType.TAG_PREVIEW, + ReaderPostListType.TAG_FOLLOWED -> + ReaderPostServiceStarter.startServiceForTag( + this, + currentTag, + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER + ) + + ReaderPostListType.BLOG_PREVIEW -> ReaderPostServiceStarter.startServiceForBlog( + this, + blogId, + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER + ) + + ReaderPostListType.SEARCH_RESULTS -> {} + + ReaderPostListType.TAGS_FEED -> {} + + else -> {} + } + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(event: UpdatePostsStarted?) { + if (isFinishing) { + return + } + + isRequestingMorePosts = true + progressBar!!.visibility = View.VISIBLE + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(event: UpdatePostsEnded) { + if (isFinishing) { + return + } + + val adapter = pagerAdapter ?: return + + isRequestingMorePosts = false + progressBar!!.visibility = View.GONE + + if (event.result == ReaderActions.UpdateResult.HAS_NEW) { + AppLog.d(AppLog.T.READER, "reader pager > older posts received") + // remember which post to keep active + val id = adapter.currentBlogIdPostId + val blogId = (id?.blogId ?: 0) + val postId = (id?.postId ?: 0) + loadPosts(blogId, postId) + } else { + AppLog.d(AppLog.T.READER, "reader pager > all posts loaded") + adapter.allPostsLoaded = true + } + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(event: DoSignIn?) { + if (isFinishing) { + return + } + + readerTracker.trackUri(AnalyticsTracker.Stat.READER_SIGN_IN_INITIATED, interceptedUri!!) + ActivityLauncher.loginWithoutMagicLink(this) + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + when (requestCode) { + RequestCodes.EDIT_POST -> { + if (resultCode != RESULT_OK || data == null || isFinishing) { + return + } + val localId = data.getIntExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, 0) + val site = data.extras?.let { + BundleCompat.getSerializable( + it, + WordPress.SITE, + SiteModel::class.java + ) + } + val post = postStore.getPostByLocalPostId(localId) + + if (checkToRestart(data)) { + ActivityLauncher.editPostOrPageForResult( + data, + this@ReaderPostPagerActivity, site, + data.getIntExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, 0) + ) + + // a restart will happen so, no need to continue here + return + } + + val snackbarAttachView = findViewById(R.id.coordinator) + if (site != null && post != null && snackbarAttachView != null) { + uploadUtilsWrapper.handleEditPostResultSnackbars( + this, + snackbarAttachView, + data, + post, + site, + uploadActionUseCase.getUploadAction(post), + { _: View? -> + UploadUtils.publishPost( + this@ReaderPostPagerActivity, + post, + site, + dispatcher + ) + }) + } + } + + RequestCodes.DO_LOGIN -> if (resultCode == RESULT_OK) { + backFromLogin = true + } + + RequestCodes.NO_REBLOG_SITE -> if (resultCode == RESULT_OK) { + finish() // Finish activity to make My Site page visible + } + } + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onPostUploaded(event: OnPostUploaded) { + val site = siteStore.getSiteByLocalId(selectedSiteRepository.getSelectedSiteLocalId()) + val snackbarAttachView = findViewById(R.id.coordinator) + if (site != null && event.post != null && snackbarAttachView != null) { + uploadUtilsWrapper.onPostUploadedSnackbarHandler( + this, + snackbarAttachView, + event.isError, + event.isFirstTimePublish, + event.post, + null, + site + ) + } + } + + /** + * pager adapter containing post detail fragments + */ + private inner class PostPagerAdapter @SuppressLint("WrongConstant") constructor( + fm: FragmentManager, + ids: ReaderBlogIdPostIdList + ) : + FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + private val idList = ids.clone() as ReaderBlogIdPostIdList + var allPostsLoaded: Boolean = false + + // this is used to retain created fragments so we can access them in + // getFragmentAtPosition() - necessary because the pager provides no + // built-in way to do this - note that destroyItem() removes fragments + // from this map when they're removed from the adapter, so this doesn't + // retain *every* fragment + private val fragmentMap = SparseArray() + + override fun restoreState(state: Parcelable?, loader: ClassLoader?) { + // work around "Fragement no longer exists for key" Android bug + // by catching the IllegalStateException + // https://code.google.com/p/android/issues/detail?id=42601 + try { + AppLog.d(AppLog.T.READER, "reader pager > adapter restoreState") + super.restoreState(state, loader) + } catch (e: IllegalStateException) { + AppLog.e(AppLog.T.READER, e) + } + } + + override fun saveState(): Parcelable? { + AppLog.d(AppLog.T.READER, "reader pager > adapter saveState") + return super.saveState() + } + + fun canRequestMostPosts(): Boolean { + return !allPostsLoaded && !isSinglePostView && (idList.size < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) + && NetworkUtils.isNetworkAvailable(this@ReaderPostPagerActivity) + } + + fun isValidPosition(position: Int): Boolean { + return (position in 0..