diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 904035c3af15..286731545494 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -493,6 +493,8 @@ dependencies { testImplementation(libs.assertj.core) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.turbine) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.test.core) androidTestImplementation project(path:':libs:mocks') diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index f6bdfdc55b21..e77c58834043 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -581,6 +581,82 @@ android:pathPattern="/site-monitoring/.*" android:scheme="http" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -612,49 +688,71 @@ + android:scheme="https" + tools:ignore="IntentFilterUniqueDataAttributes" > + android:scheme="http" + tools:ignore="IntentFilterUniqueDataAttributes" > + android:scheme="https" + tools:ignore="IntentFilterUniqueDataAttributes" > + android:scheme="http" + tools:ignore="IntentFilterUniqueDataAttributes" > + android:scheme="https" + tools:ignore="IntentFilterUniqueDataAttributes" > + android:scheme="http" + tools:ignore="IntentFilterUniqueDataAttributes" > + android:scheme="https" + tools:ignore="IntentFilterUniqueDataAttributes" > + android:scheme="http" + tools:ignore="IntentFilterUniqueDataAttributes" > + + + + + + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index 084adad57ae0..c038964765b5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -338,6 +338,43 @@ public static void viewReaderInNewStack(Context context) { context.startActivity(intent); } + public static void viewReaderDiscoverInNewStack(Context context) { + Intent intent = getMainActivityInNewStack(context); + intent.putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_READER); + intent.putExtra(WPMainActivity.ARG_READER_DISCOVER_TAB, true); + context.startActivity(intent); + } + + public static void viewReaderFeedInNewStack(Context context, long feedId) { + Intent mainActivityIntent = getMainActivityInNewStack(context) + .putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_READER); + Intent feedIntent = ReaderActivityLauncher.buildReaderFeedIntent(context, feedId, "deeplink"); + TaskStackBuilder.create(context) + .addNextIntent(mainActivityIntent) + .addNextIntent(feedIntent) + .startActivities(); + } + + public static void viewReaderSearchInNewStack(Context context) { + Intent mainActivityIntent = getMainActivityInNewStack(context) + .putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_READER); + Intent searchIntent = ReaderActivityLauncher.createReaderSearchIntent(context); + TaskStackBuilder.create(context) + .addNextIntent(mainActivityIntent) + .addNextIntent(searchIntent) + .startActivities(); + } + + public static void viewReaderTagInNewStack(@NonNull Context context, @NonNull String tagSlug) { + Intent mainActivityIntent = getMainActivityInNewStack(context) + .putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_READER); + Intent tagIntent = ReaderActivityLauncher.buildReaderTagIntent(context, tagSlug, "deeplink"); + TaskStackBuilder.create(context) + .addNextIntent(mainActivityIntent) + .addNextIntent(tagIntent) + .startActivities(); + } + public static void viewPostDeeplinkInNewStack(Context context, Uri uri) { Intent mainActivityIntent = getMainActivityInNewStack(context) .putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_READER); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt index 8613afa59d1d..eeffd41f9225 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt @@ -18,7 +18,11 @@ import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenN import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenPages import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenPagesForSite import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenQRCodeAuthFlow +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenFeedInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReader +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReaderDiscover +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReaderSearch +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenTagInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStats import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForSite import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForSiteAndTimeframe @@ -78,6 +82,10 @@ class DeepLinkNavigator ) OpenReader -> ActivityLauncher.viewReaderInNewStack(activity) + OpenReaderDiscover -> ActivityLauncher.viewReaderDiscoverInNewStack(activity) + OpenReaderSearch -> ActivityLauncher.viewReaderSearchInNewStack(activity) + is OpenFeedInReader -> ActivityLauncher.viewReaderFeedInNewStack(activity, navigateAction.feedId) + is OpenTagInReader -> ActivityLauncher.viewReaderTagInNewStack(activity, navigateAction.tagSlug) is OpenInReader -> ActivityLauncher.viewPostDeeplinkInNewStack(activity, navigateAction.uri.uri) is ViewPostInReader -> ActivityLauncher.viewReaderPostDetailInNewStack( activity, @@ -123,6 +131,10 @@ class DeepLinkNavigator data class OpenEditorForPost(val site: SiteModel, val postId: Int) : NavigateAction() data class OpenEditorForSite(val site: SiteModel) : NavigateAction() object OpenReader : NavigateAction() + object OpenReaderDiscover : NavigateAction() + object OpenReaderSearch : NavigateAction() + data class OpenFeedInReader(val feedId: Long) : NavigateAction() + data class OpenTagInReader(val tagSlug: String) : NavigateAction() data class OpenInReader(val uri: UriWrapper) : NavigateAction() data class ViewPostInReader(val blogId: Long, val postId: Long, val uri: UriWrapper) : NavigateAction() object OpenEditor : NavigateAction() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandler.kt index 24b57ced8616..2538e191560a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandler.kt @@ -6,8 +6,12 @@ import androidx.lifecycle.MutableLiveData import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_VIEWPOST_INTERCEPTED import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenFeedInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReader +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReaderDiscover +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReaderSearch +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenTagInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.ViewPostInReader import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.APPLINK_SCHEME import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.HOST_WORDPRESS_COM @@ -34,18 +38,79 @@ class ReaderLinkHandler * Other deeplinks handled: * `wordpress://read` * `wordpress://viewpost?blogId={blogId}&postId={postId}` + * `wordpress.com/read` + * `wordpress.com/discover` */ override fun shouldHandleUrl(uri: UriWrapper): Boolean { - return DEEP_LINK_HOST_READ == uri.host || DEEP_LINK_HOST_VIEWPOST == uri.host || intentUtils.canResolveWith( - ReaderConstants.ACTION_VIEW_POST, - uri - ) + return DEEP_LINK_HOST_READ == uri.host || + DEEP_LINK_HOST_VIEWPOST == uri.host || + isWordPressComReaderUrl(uri) || + isWordPressComDiscoverUrl(uri) || + isWordPressComFeedUrl(uri) || + isWordPressComReaderSearchUrl(uri) || + isWordPressComTagUrl(uri) || + intentUtils.canResolveWith(ReaderConstants.ACTION_VIEW_POST, uri) + } + + private fun isWordPressComReaderUrl(uri: UriWrapper): Boolean { + return uri.host == HOST_WORDPRESS_COM && + uri.pathSegments.size == 1 && + uri.pathSegments.firstOrNull() == PATH_READ + } + + private fun isWordPressComDiscoverUrl(uri: UriWrapper): Boolean { + return uri.host == HOST_WORDPRESS_COM && + uri.pathSegments.size == 1 && + uri.pathSegments.firstOrNull() == PATH_DISCOVER + } + + /** + * Checks if this is a feed URL like wordpress.com/read/feeds/{feedId} or wordpress.com/reader/feeds/{feedId} + * but NOT a post URL like wordpress.com/read/feeds/{feedId}/posts/{postId} + */ + private fun isWordPressComFeedUrl(uri: UriWrapper): Boolean { + val segments = uri.pathSegments + return uri.host == HOST_WORDPRESS_COM && + segments.size == FEED_URL_SEGMENTS && + isReadOrReaderPath(segments.firstOrNull()) && + segments.getOrNull(SECOND_PATH_POSITION) == PATH_FEEDS + } + + /** + * Checks if this is a reader search URL like wordpress.com/read/search or wordpress.com/reader/search + */ + private fun isWordPressComReaderSearchUrl(uri: UriWrapper): Boolean { + val segments = uri.pathSegments + return uri.host == HOST_WORDPRESS_COM && + segments.size == SEARCH_URL_SEGMENTS && + isReadOrReaderPath(segments.firstOrNull()) && + segments.getOrNull(SECOND_PATH_POSITION) == PATH_SEARCH + } + + private fun isReadOrReaderPath(segment: String?) = segment == PATH_READ || segment == PATH_READER + + /** + * Checks if this is a tag URL like wordpress.com/tag/{tagSlug} + */ + private fun isWordPressComTagUrl(uri: UriWrapper): Boolean { + val segments = uri.pathSegments + return uri.host == HOST_WORDPRESS_COM && + segments.size == TAG_URL_SEGMENTS && + segments.firstOrNull() == PATH_TAG + } + + private fun extractFeedId(uri: UriWrapper): Long? { + return uri.pathSegments.getOrNull(FEED_ID_POSITION)?.toLongOrNull() + } + + private fun extractTagSlug(uri: UriWrapper): String? { + return uri.pathSegments.getOrNull(TAG_SLUG_POSITION) } override fun buildNavigateAction(uri: UriWrapper): NavigateAction { - return when (uri.host) { - DEEP_LINK_HOST_READ -> OpenReader - DEEP_LINK_HOST_VIEWPOST -> { + return when { + uri.host == DEEP_LINK_HOST_READ -> OpenReader + uri.host == DEEP_LINK_HOST_VIEWPOST -> { val blogId = uri.getQueryParameter(BLOG_ID)?.toLongOrNull() val postId = uri.getQueryParameter(POST_ID)?.toLongOrNull() if (blogId != null && postId != null) { @@ -56,6 +121,27 @@ class ReaderLinkHandler OpenReader } } + isWordPressComReaderUrl(uri) -> OpenReader + isWordPressComDiscoverUrl(uri) -> OpenReaderDiscover + isWordPressComReaderSearchUrl(uri) -> OpenReaderSearch + isWordPressComFeedUrl(uri) -> { + val feedId = extractFeedId(uri) + if (feedId != null) { + OpenFeedInReader(feedId) + } else { + _toast.value = Event(R.string.error_generic) + OpenReader + } + } + isWordPressComTagUrl(uri) -> { + val tagSlug = extractTagSlug(uri) + if (!tagSlug.isNullOrBlank()) { + OpenTagInReader(tagSlug) + } else { + _toast.value = Event(R.string.error_generic) + OpenReader + } + } else -> OpenInReader(uri) } } @@ -64,15 +150,18 @@ class ReaderLinkHandler * URLs handled here * `wordpress://read` * `wordpress://viewpost?blogId={blogId}&postId={postId}` + * wordpress.com/read * wordpress.com/read/feeds/feedId/posts/feedItemId * wordpress.com/read/blogs/feedId/posts/feedItemId + * wordpress.com/reader/feeds/feedId/posts/feedItemId + * wordpress.com/discover * domain.wordpress.com/2.../../../postId * domain.wordpress.com/19../../../postId */ override fun stripUrl(uri: UriWrapper): String { - return when (uri.host) { - DEEP_LINK_HOST_READ -> "$APPLINK_SCHEME$DEEP_LINK_HOST_READ" - DEEP_LINK_HOST_VIEWPOST -> { + return when { + uri.host == DEEP_LINK_HOST_READ -> "$APPLINK_SCHEME$DEEP_LINK_HOST_READ" + uri.host == DEEP_LINK_HOST_VIEWPOST -> { val hasBlogId = uri.getQueryParameter(BLOG_ID) != null val hasPostId = uri.getQueryParameter(POST_ID) != null buildString { @@ -91,13 +180,18 @@ class ReaderLinkHandler } } } + isWordPressComReaderUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_READ" + isWordPressComDiscoverUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_DISCOVER" + isWordPressComReaderSearchUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_READ/$PATH_SEARCH" + isWordPressComFeedUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_READ/$PATH_FEEDS/$FEED_ID" + isWordPressComTagUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_TAG/$TAG_SLUG" else -> { buildString { val segments = uri.pathSegments // Handled URLs look like this: http[s]://wordpress.com/read/feeds/{feedId}/posts/{feedItemId} // with the first segment being 'read'. append(stripHost(uri)) - if (segments.firstOrNull() == "read") { + if (isReadOrReaderPath(segments.firstOrNull())) { appendReadPath(segments) } else if (segments.size > DATE_URL_SEGMENTS) { append("/YYYY/MM/DD/$POST_ID") @@ -120,7 +214,7 @@ class ReaderLinkHandler private fun StringBuilder.appendReadPath(segments: List) { append("/read") - when (segments.getOrNull(BLOGS_FEEDS_PATH_POSITION)) { + when (segments.getOrNull(SECOND_PATH_POSITION)) { "blogs" -> { append("/blogs/$FEED_ID") } @@ -134,14 +228,35 @@ class ReaderLinkHandler } companion object { + // Applink hosts (wordpress://read, wordpress://viewpost) private const val DEEP_LINK_HOST_READ = "read" private const val DEEP_LINK_HOST_VIEWPOST = "viewpost" + + // URL path segments + private const val PATH_READ = "read" + private const val PATH_READER = "reader" + private const val PATH_DISCOVER = "discover" + private const val PATH_FEEDS = "feeds" + private const val PATH_SEARCH = "search" + private const val PATH_TAG = "tag" + + // Query and path parameter names (used for analytics stripping) private const val BLOG_ID = "blogId" private const val POST_ID = "postId" private const val FEED_ID = "feedId" - private const val CUSTOM_DOMAIN_POSITION = 3 - private const val BLOGS_FEEDS_PATH_POSITION = 1 + private const val TAG_SLUG = "tagSlug" + + // URL segment positions + private const val SECOND_PATH_POSITION = 1 + private const val FEED_ID_POSITION = 2 + private const val TAG_SLUG_POSITION = 1 private const val POSTS_PATH_POSITION = 3 + private const val CUSTOM_DOMAIN_POSITION = 3 + + // Expected URL segment counts private const val DATE_URL_SEGMENTS = 3 + private const val FEED_URL_SEGMENTS = 3 + private const val SEARCH_URL_SEGMENTS = 2 + private const val TAG_URL_SEGMENTS = 2 } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 0b0f51460805..0a5715e58030 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -231,6 +231,7 @@ public class WPMainActivity extends BaseAppCompatActivity implements public static final String ARG_NOTIFICATIONS = "show_notifications"; public static final String ARG_READER = "show_reader"; public static final String ARG_READER_BOOKMARK_TAB = "show_reader_bookmark_tab"; + public static final String ARG_READER_DISCOVER_TAB = "show_reader_discover_tab"; public static final String ARG_EDITOR = "show_editor"; public static final String ARG_SHOW_ZENDESK_NOTIFICATIONS = "show_zendesk_notifications"; public static final String ARG_STATS = "show_stats"; @@ -901,11 +902,13 @@ private void handleOpenPageIntent(@NonNull Intent intent) { showJetpackFeatureOverlayAccessedInCorrectly(trackingProperties); break; } + if (mBottomNav != null) mBottomNav.setCurrentSelectedPage(PageType.READER); if (intent.getBooleanExtra(ARG_READER_BOOKMARK_TAB, false) && mBottomNav != null && mBottomNav .getActiveFragment() instanceof ReaderFragment) { ((ReaderFragment) mBottomNav.getActiveFragment()).requestBookmarkTab(); - } else { - if (mBottomNav != null) mBottomNav.setCurrentSelectedPage(PageType.READER); + } else if (intent.getBooleanExtra(ARG_READER_DISCOVER_TAB, false) && mBottomNav != null + && mBottomNav.getActiveFragment() instanceof ReaderFragment) { + ((ReaderFragment) mBottomNav.getActiveFragment()).requestDiscoverTab(); } break; case ARG_EDITOR: diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.kt index 92c9c87ef372..a2a712802c52 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.kt @@ -14,6 +14,7 @@ import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.RequestCodes import org.wordpress.android.ui.WPWebViewActivity @@ -194,6 +195,28 @@ object ReaderActivityLauncher { ) } + /** + * Build an intent to show posts from a specific feed (for deeplinks) + */ + @JvmStatic + fun buildReaderFeedIntent(context: Context, feedId: Long, source: String): Intent { + return Intent(context, ReaderPostListActivity::class.java).apply { + putExtra(ReaderConstants.ARG_SOURCE, source) + putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW) + putExtra(ReaderConstants.ARG_FEED_ID, feedId) + putExtra(ReaderConstants.ARG_IS_FEED, true) + } + } + + /** + * Build an intent to show posts with a specific tag (for deeplinks) + */ + @JvmStatic + fun buildReaderTagIntent(context: Context, tagSlug: String, source: String): Intent { + val tag = ReaderUtils.createTagFromTagName(tagSlug, ReaderTagType.FOLLOWED) + return createReaderTagPreviewIntent(context, tag, source) + } + /* * show a list of posts with a specific tag */ @@ -228,6 +251,7 @@ object ReaderActivityLauncher { context.startActivity(createReaderSearchIntent(context)) } + @JvmStatic fun createReaderSearchIntent(context: Context): Intent { return Intent(context, ReaderSearchActivity::class.java) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 18d51e63fdd3..d4d70c4c1a57 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -402,6 +402,13 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView viewModel.bookmarkTabRequested() } + fun requestDiscoverTab() { + if (!::viewModel.isInitialized) { + viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory)[ReaderViewModel::class.java] + } + viewModel.discoverTabRequested() + } + private fun showReaderInterests() { val readerInterestsFragment = childFragmentManager.findFragmentByTag(ReaderInterestsFragment.TAG) if (readerInterestsFragment == null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.kt index c832c0be9d84..e9f5c65ac5a1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.kt @@ -98,12 +98,12 @@ class ReaderPostListActivity : BaseAppCompatActivity() { if (postListType == ReaderPostListType.BLOG_PREVIEW) { setTitle(R.string.reader_activity_title_blog_preview) if (savedInstanceState == null) { - val blogId = intent.getLongExtra(ReaderConstants.ARG_BLOG_ID, 0) val feedId = intent.getLongExtra(ReaderConstants.ARG_FEED_ID, 0) if (feedId != 0L) { showListFragmentForFeed(feedId) siteId = feedId } else { + val blogId = intent.getLongExtra(ReaderConstants.ARG_BLOG_ID, 0) showListFragmentForBlog(blogId) siteId = blogId } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index 3b6364737d9b..ff41710b4eab 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -128,12 +128,7 @@ class ReaderPostRepository @Inject constructor( } } val listener = RestRequest.Listener { jsonObject -> - handleUpdatePostsResponse( - null, - jsonObject, - updateAction, - resultListener - ) + handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener) } val errorListener = RestRequest.ErrorListener { volleyError -> AppLog.e(AppLog.T.READER, volleyError) @@ -156,12 +151,7 @@ class ReaderPostRepository @Inject constructor( } } val listener = RestRequest.Listener { jsonObject -> - handleUpdatePostsResponse( - null, - jsonObject, - updateAction, - resultListener - ) + handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener, feedId) } val errorListener = RestRequest.ErrorListener { volleyError -> AppLog.e(AppLog.T.READER, volleyError) @@ -173,12 +163,17 @@ class ReaderPostRepository @Inject constructor( /** * called after requesting posts with a specific tag or in a specific blog/feed + * + * @param requestedFeedId If provided, ensures all posts have this feedId set. This is needed + * because the API response may not include feed_ID for external feeds, but we need it + * to properly query posts later. */ private fun handleUpdatePostsResponse( tag: ReaderTag?, jsonObject: JSONObject?, updateAction: ReaderPostServiceStarter.UpdateAction, - resultListener: UpdateResultListener + resultListener: UpdateResultListener, + requestedFeedId: Long? = null ) { if (jsonObject == null) { resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED) @@ -190,6 +185,14 @@ class ReaderPostRepository @Inject constructor( object : Thread() { override fun run() { val serverPosts = ReaderPostList.fromJson(jsonObject) + // For feed requests, always set the feedId on all posts to the requested feedId. + // The API response may not include feed_ID, or may include a different value, + // but we need the feedId to match what we'll query for later. + if (requestedFeedId != null && requestedFeedId != 0L) { + serverPosts.forEach { post -> + post.feedId = requestedFeedId + } + } val updateResult = localSource.saveUpdatedPosts(serverPosts, updateAction, tag) resultListener.onUpdateResult(updateResult) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 28cc9cf5600b..e8ac0d171079 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -85,6 +85,7 @@ class ReaderViewModel @Inject constructor( private var wasPaused: Boolean = false private var trackReaderTabJob: Job? = null private var isQuickStartPromptShown: Boolean = false + private var pendingTabRequest: PendingTabRequest? = null private val _uiState = MutableLiveData() val uiState: LiveData = _uiState.distinct() @@ -151,10 +152,22 @@ class ReaderViewModel @Inject constructor( if (!initialized) { initialized = true } + applyPendingTabRequest() } } } + private fun applyPendingTabRequest() { + pendingTabRequest?.let { request -> + val tag = when (request) { + PendingTabRequest.BOOKMARK -> readerTagsList.find { it.isBookmarked } + PendingTabRequest.DISCOVER -> readerTagsList.find { it.isDiscover } + } + tag?.let { updateSelectedContent(it) } + pendingTabRequest = null + } + } + fun onTagChanged(selectedTag: ReaderTag?) { selectedTag?.let { trackReaderTabShownIfNecessary(it) @@ -204,8 +217,20 @@ class ReaderViewModel @Inject constructor( } fun bookmarkTabRequested() { - readerTagsList.find { it.isBookmarked }?.let { - updateSelectedContent(it) + val tag = readerTagsList.find { it.isBookmarked } + if (tag != null) { + updateSelectedContent(tag) + } else { + pendingTabRequest = PendingTabRequest.BOOKMARK + } + } + + fun discoverTabRequested() { + val tag = readerTagsList.find { it.isDiscover } + if (tag != null) { + updateSelectedContent(tag) + } else { + pendingTabRequest = PendingTabRequest.DISCOVER } } @@ -574,3 +599,8 @@ class ReaderViewModel @Inject constructor( } data class TabNavigation(val position: Int, val smoothAnimation: Boolean) + +enum class PendingTabRequest { + BOOKMARK, + DISCOVER +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt index fe7936a1c266..49442eeef735 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt @@ -9,8 +9,12 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_VIEWPOST_INTERCEPTED +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenFeedInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReader +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReaderDiscover +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReaderSearch +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenTagInReader import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.ViewPostInReader import org.wordpress.android.ui.deeplinks.buildUri import org.wordpress.android.ui.reader.ReaderConstants @@ -28,6 +32,7 @@ class ReaderLinkHandlerTest : BaseUnitTest() { val blogId: Long = 111 val postId: Long = 222 val feedId: Long = 333 + val tagSlug: String = "dogs" @Before fun setUp() { @@ -224,4 +229,175 @@ class ReaderLinkHandlerTest : BaseUnitTest() { assertThat(strippedUrl).isEqualTo("www.wordpress.com/read") } + + @Test + fun `handles wordpress com read path`() { + val uri = buildUri("wordpress.com", "read") + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `handles wordpress com discover path`() { + val uri = buildUri("wordpress.com", "discover") + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `wordpress com read opens reader`() { + val uri = buildUri("wordpress.com", "read") + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenReader) + } + + @Test + fun `wordpress com discover opens reader discover`() { + val uri = buildUri("wordpress.com", "discover") + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenReaderDiscover) + } + + @Test + fun `correctly strips wordpress com read URI`() { + val uri = buildUri("wordpress.com", "read") + + val strippedUrl = readerLinkHandler.stripUrl(uri) + + assertThat(strippedUrl).isEqualTo("wordpress.com/read") + } + + @Test + fun `correctly strips wordpress com discover URI`() { + val uri = buildUri("wordpress.com", "discover") + + val strippedUrl = readerLinkHandler.stripUrl(uri) + + assertThat(strippedUrl).isEqualTo("wordpress.com/discover") + } + + @Test + fun `handles wordpress com read feeds path`() { + val uri = buildUri("wordpress.com", "read", "feeds", feedId.toString()) + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `handles wordpress com reader feeds path`() { + val uri = buildUri("wordpress.com", "reader", "feeds", feedId.toString()) + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `wordpress com read feeds opens feed in reader`() { + val uri = buildUri("wordpress.com", "read", "feeds", feedId.toString()) + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenFeedInReader(feedId)) + } + + @Test + fun `wordpress com reader feeds opens feed in reader`() { + val uri = buildUri("wordpress.com", "reader", "feeds", feedId.toString()) + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenFeedInReader(feedId)) + } + + @Test + fun `correctly strips wordpress com feed URI`() { + val uri = buildUri("wordpress.com", "read", "feeds", feedId.toString()) + + val strippedUrl = readerLinkHandler.stripUrl(uri) + + assertThat(strippedUrl).isEqualTo("wordpress.com/read/feeds/feedId") + } + + @Test + fun `handles wordpress com read search path`() { + val uri = buildUri("wordpress.com", "read", "search") + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `wordpress com read search opens reader search`() { + val uri = buildUri("wordpress.com", "read", "search") + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenReaderSearch) + } + + @Test + fun `correctly strips wordpress com read search URI`() { + val uri = buildUri("wordpress.com", "read", "search") + + val strippedUrl = readerLinkHandler.stripUrl(uri) + + assertThat(strippedUrl).isEqualTo("wordpress.com/read/search") + } + + @Test + fun `handles wordpress com reader search path`() { + val uri = buildUri("wordpress.com", "reader", "search") + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `wordpress com reader search opens reader search`() { + val uri = buildUri("wordpress.com", "reader", "search") + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenReaderSearch) + } + + @Test + fun `handles wordpress com tag path`() { + val uri = buildUri("wordpress.com", "tag", tagSlug) + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `wordpress com tag opens tag in reader`() { + val uri = buildUri("wordpress.com", "tag", tagSlug) + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenTagInReader(tagSlug)) + } + + @Test + fun `correctly strips wordpress com tag URI`() { + val uri = buildUri("wordpress.com", "tag", tagSlug) + + val strippedUrl = readerLinkHandler.stripUrl(uri) + + assertThat(strippedUrl).isEqualTo("wordpress.com/tag/tagSlug") + } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderActivityLauncherTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderActivityLauncherTest.kt new file mode 100644 index 000000000000..e0752d4bad9e --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderActivityLauncherTest.kt @@ -0,0 +1,95 @@ +package org.wordpress.android.ui.reader + +import androidx.test.core.app.ApplicationProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType + +@RunWith(RobolectricTestRunner::class) +@Config(application = android.app.Application::class) +class ReaderActivityLauncherTest { + private val context = ApplicationProvider.getApplicationContext() + private val feedId = 12345L + private val tagSlug = "dogs" + private val source = "deeplink" + + @Test + fun `buildReaderFeedIntent creates intent with correct feed id`() { + val intent = ReaderActivityLauncher.buildReaderFeedIntent(context, feedId, source) + + assertThat(intent.getLongExtra(ReaderConstants.ARG_FEED_ID, 0L)).isEqualTo(feedId) + } + + @Test + fun `buildReaderFeedIntent creates intent with is feed flag set to true`() { + val intent = ReaderActivityLauncher.buildReaderFeedIntent(context, feedId, source) + + assertThat(intent.getBooleanExtra(ReaderConstants.ARG_IS_FEED, false)).isTrue() + } + + @Test + fun `buildReaderFeedIntent creates intent with correct source`() { + val intent = ReaderActivityLauncher.buildReaderFeedIntent(context, feedId, source) + + assertThat(intent.getStringExtra(ReaderConstants.ARG_SOURCE)).isEqualTo(source) + } + + @Test + fun `buildReaderFeedIntent creates intent with blog preview list type`() { + val intent = ReaderActivityLauncher.buildReaderFeedIntent(context, feedId, source) + + @Suppress("DEPRECATION") + val listType = intent.getSerializableExtra(ReaderConstants.ARG_POST_LIST_TYPE) as ReaderPostListType + assertThat(listType).isEqualTo(ReaderPostListType.BLOG_PREVIEW) + } + + @Test + fun `buildReaderFeedIntent creates intent for ReaderPostListActivity`() { + val intent = ReaderActivityLauncher.buildReaderFeedIntent(context, feedId, source) + + assertThat(intent.component?.className).isEqualTo(ReaderPostListActivity::class.java.name) + } + + @Test + fun `buildReaderTagIntent creates intent with correct tag slug`() { + val intent = ReaderActivityLauncher.buildReaderTagIntent(context, tagSlug, source) + + @Suppress("DEPRECATION") + val tag = intent.getSerializableExtra(ReaderConstants.ARG_TAG) as ReaderTag + assertThat(tag.tagSlug).isEqualTo(tagSlug) + } + + @Test + fun `buildReaderTagIntent creates intent with correct source`() { + val intent = ReaderActivityLauncher.buildReaderTagIntent(context, tagSlug, source) + + assertThat(intent.getStringExtra(ReaderConstants.ARG_SOURCE)).isEqualTo(source) + } + + @Test + fun `buildReaderTagIntent creates intent with tag preview list type`() { + val intent = ReaderActivityLauncher.buildReaderTagIntent(context, tagSlug, source) + + @Suppress("DEPRECATION") + val listType = intent.getSerializableExtra(ReaderConstants.ARG_POST_LIST_TYPE) as ReaderPostListType + assertThat(listType).isEqualTo(ReaderPostListType.TAG_PREVIEW) + } + + @Test + fun `buildReaderTagIntent creates intent for ReaderPostListActivity`() { + val intent = ReaderActivityLauncher.buildReaderTagIntent(context, tagSlug, source) + + assertThat(intent.component?.className).isEqualTo(ReaderPostListActivity::class.java.name) + } + + @Test + fun `createReaderSearchIntent creates intent for ReaderSearchActivity`() { + val intent = ReaderActivityLauncher.createReaderSearchIntent(context) + + assertThat(intent.component?.className).isEqualTo(ReaderSearchActivity::class.java.name) + } +}