From 205bc6ebca3653e67f932d582eef760b6007c155 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 13:15:13 +0100 Subject: [PATCH 01/10] Supporting read and discovery deeplinks --- WordPress/src/main/AndroidManifest.xml | 20 +++++++ .../android/ui/ActivityLauncher.java | 7 +++ .../android/ui/deeplinks/DeepLinkNavigator.kt | 3 + .../deeplinks/handlers/ReaderLinkHandler.kt | 48 ++++++++++++---- .../android/ui/main/WPMainActivity.java | 4 ++ .../android/ui/reader/ReaderFragment.kt | 7 +++ .../ui/reader/viewmodels/ReaderViewModel.kt | 6 ++ .../handlers/ReaderLinkHandlerTest.kt | 55 +++++++++++++++++++ 8 files changed, 139 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index f6bdfdc55b21..0cf7db3572b1 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -581,6 +581,26 @@ android:pathPattern="/site-monitoring/.*" android:scheme="http" /> + + + + + + + + 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..ab48b0a287d1 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,13 @@ 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 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..86deb44a5a7e 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 @@ -19,6 +19,7 @@ import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenP 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.OpenReader +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenReaderDiscover 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 +79,7 @@ class DeepLinkNavigator ) OpenReader -> ActivityLauncher.viewReaderInNewStack(activity) + OpenReaderDiscover -> ActivityLauncher.viewReaderDiscoverInNewStack(activity) is OpenInReader -> ActivityLauncher.viewPostDeeplinkInNewStack(activity, navigateAction.uri.uri) is ViewPostInReader -> ActivityLauncher.viewReaderPostDetailInNewStack( activity, @@ -123,6 +125,7 @@ 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() 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..689f651eeb53 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 @@ -8,6 +8,7 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_VIEWPOST_INT import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction 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.ViewPostInReader import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.APPLINK_SCHEME import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.HOST_WORDPRESS_COM @@ -34,18 +35,33 @@ 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) || + 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 } 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 +72,8 @@ class ReaderLinkHandler OpenReader } } + isWordPressComReaderUrl(uri) -> OpenReader + isWordPressComDiscoverUrl(uri) -> OpenReaderDiscover else -> OpenInReader(uri) } } @@ -64,15 +82,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 +112,16 @@ class ReaderLinkHandler } } } + isWordPressComReaderUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_READ" + isWordPressComDiscoverUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_DISCOVER" 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") { + val firstSegment = segments.firstOrNull() + if (firstSegment == PATH_READ || firstSegment == "reader") { appendReadPath(segments) } else if (segments.size > DATE_URL_SEGMENTS) { append("/YYYY/MM/DD/$POST_ID") @@ -136,6 +160,8 @@ class ReaderLinkHandler companion object { private const val DEEP_LINK_HOST_READ = "read" private const val DEEP_LINK_HOST_VIEWPOST = "viewpost" + private const val PATH_READ = "read" + private const val PATH_DISCOVER = "discover" private const val BLOG_ID = "blogId" private const val POST_ID = "postId" private const val FEED_ID = "feedId" 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..f10a5bdb749c 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"; @@ -904,6 +905,9 @@ private void handleOpenPageIntent(@NonNull Intent intent) { if (intent.getBooleanExtra(ARG_READER_BOOKMARK_TAB, false) && mBottomNav != null && mBottomNav .getActiveFragment() instanceof ReaderFragment) { ((ReaderFragment) mBottomNav.getActiveFragment()).requestBookmarkTab(); + } else if (intent.getBooleanExtra(ARG_READER_DISCOVER_TAB, false) && mBottomNav != null + && mBottomNav.getActiveFragment() instanceof ReaderFragment) { + ((ReaderFragment) mBottomNav.getActiveFragment()).requestDiscoverTab(); } else { if (mBottomNav != null) mBottomNav.setCurrentSelectedPage(PageType.READER); } 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/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 28cc9cf5600b..c96610727408 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 @@ -209,6 +209,12 @@ class ReaderViewModel @Inject constructor( } } + fun discoverTabRequested() { + readerTagsList.find { it.isDiscover }?.let { + updateSelectedContent(it) + } + } + @Suppress("UseCheckOrError") fun onSearchActionClicked() { if (isSearchSupported()) { 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..e519277a35b5 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 @@ -11,6 +11,7 @@ import org.wordpress.android.BaseUnitTest import org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_VIEWPOST_INTERCEPTED 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.ViewPostInReader import org.wordpress.android.ui.deeplinks.buildUri import org.wordpress.android.ui.reader.ReaderConstants @@ -224,4 +225,58 @@ 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") + } } From ba8e9ddf3ec3eb9e7926708fcf02ff2e5b3ad128 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 13:27:02 +0100 Subject: [PATCH 02/10] Fixing discovery issue --- .../android/ui/main/WPMainActivity.java | 3 +- .../ui/reader/viewmodels/ReaderViewModel.kt | 32 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) 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 f10a5bdb749c..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 @@ -902,14 +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 (intent.getBooleanExtra(ARG_READER_DISCOVER_TAB, false) && mBottomNav != null && mBottomNav.getActiveFragment() instanceof ReaderFragment) { ((ReaderFragment) mBottomNav.getActiveFragment()).requestDiscoverTab(); - } else { - if (mBottomNav != null) mBottomNav.setCurrentSelectedPage(PageType.READER); } break; case ARG_EDITOR: 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 c96610727408..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,14 +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() { - readerTagsList.find { it.isDiscover }?.let { - updateSelectedContent(it) + val tag = readerTagsList.find { it.isDiscover } + if (tag != null) { + updateSelectedContent(tag) + } else { + pendingTabRequest = PendingTabRequest.DISCOVER } } @@ -580,3 +599,8 @@ class ReaderViewModel @Inject constructor( } data class TabNavigation(val position: Int, val smoothAnimation: Boolean) + +enum class PendingTabRequest { + BOOKMARK, + DISCOVER +} From a13caf89246287afbb7e921ce702df2d7145b2d2 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 14:29:41 +0100 Subject: [PATCH 03/10] Handling feed links --- WordPress/src/main/AndroidManifest.xml | 32 ++++++++----- .../android/ui/ActivityLauncher.java | 10 ++++ .../android/ui/deeplinks/DeepLinkNavigator.kt | 3 ++ .../deeplinks/handlers/ReaderLinkHandler.kt | 32 +++++++++++++ .../ui/reader/ReaderActivityLauncher.kt | 13 ++++++ .../ui/reader/ReaderPostListActivity.kt | 2 +- .../reader/repository/ReaderPostRepository.kt | 29 ++++++------ .../handlers/ReaderLinkHandlerTest.kt | 46 +++++++++++++++++++ 8 files changed, 141 insertions(+), 26 deletions(-) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 0cf7db3572b1..c51ebca1aba1 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -601,6 +601,26 @@ android:path="/discover" android:scheme="http" /> + + + + + + + + @@ -641,18 +661,6 @@ android:scheme="http" > - - - - - - ActivityLauncher.viewReaderInNewStack(activity) OpenReaderDiscover -> ActivityLauncher.viewReaderDiscoverInNewStack(activity) + is OpenFeedInReader -> ActivityLauncher.viewReaderFeedInNewStack(activity, navigateAction.feedId) is OpenInReader -> ActivityLauncher.viewPostDeeplinkInNewStack(activity, navigateAction.uri.uri) is ViewPostInReader -> ActivityLauncher.viewReaderPostDetailInNewStack( activity, @@ -126,6 +128,7 @@ class DeepLinkNavigator data class OpenEditorForSite(val site: SiteModel) : NavigateAction() object OpenReader : NavigateAction() object OpenReaderDiscover : NavigateAction() + data class OpenFeedInReader(val feedId: Long) : 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 689f651eeb53..cc4948608e20 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,6 +6,7 @@ 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 @@ -43,6 +44,7 @@ class ReaderLinkHandler DEEP_LINK_HOST_VIEWPOST == uri.host || isWordPressComReaderUrl(uri) || isWordPressComDiscoverUrl(uri) || + isWordPressComFeedUrl(uri) || intentUtils.canResolveWith(ReaderConstants.ACTION_VIEW_POST, uri) } @@ -58,6 +60,22 @@ class ReaderLinkHandler 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 && + (segments.firstOrNull() == PATH_READ || segments.firstOrNull() == PATH_READER) && + segments.getOrNull(BLOGS_FEEDS_PATH_POSITION) == PATH_FEEDS + } + + private fun extractFeedId(uri: UriWrapper): Long? { + return uri.pathSegments.getOrNull(FEED_ID_POSITION)?.toLongOrNull() + } + override fun buildNavigateAction(uri: UriWrapper): NavigateAction { return when { uri.host == DEEP_LINK_HOST_READ -> OpenReader @@ -74,6 +92,15 @@ class ReaderLinkHandler } isWordPressComReaderUrl(uri) -> OpenReader isWordPressComDiscoverUrl(uri) -> OpenReaderDiscover + isWordPressComFeedUrl(uri) -> { + val feedId = extractFeedId(uri) + if (feedId != null) { + OpenFeedInReader(feedId) + } else { + _toast.value = Event(R.string.error_generic) + OpenReader + } + } else -> OpenInReader(uri) } } @@ -114,6 +141,7 @@ class ReaderLinkHandler } isWordPressComReaderUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_READ" isWordPressComDiscoverUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_DISCOVER" + isWordPressComFeedUrl(uri) -> "$HOST_WORDPRESS_COM/$PATH_READ/$PATH_FEEDS/$FEED_ID" else -> { buildString { val segments = uri.pathSegments @@ -161,13 +189,17 @@ class ReaderLinkHandler private const val DEEP_LINK_HOST_READ = "read" private const val DEEP_LINK_HOST_VIEWPOST = "viewpost" 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 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 FEED_ID_POSITION = 2 private const val POSTS_PATH_POSITION = 3 private const val DATE_URL_SEGMENTS = 3 + private const val FEED_URL_SEGMENTS = 3 } } 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..51c84d44deb3 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 @@ -194,6 +194,19 @@ 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) + } + } + /* * show a list of posts with a specific tag */ 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/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt index e519277a35b5..54abba9a82b2 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,6 +9,7 @@ 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 @@ -279,4 +280,49 @@ class ReaderLinkHandlerTest : BaseUnitTest() { 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") + } } From f1aa2b49e3232a3588319276c5cbd446ee8939ca Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 14:53:45 +0100 Subject: [PATCH 04/10] Handling search and tags --- WordPress/src/main/AndroidManifest.xml | 20 +++++++ .../android/ui/ActivityLauncher.java | 20 +++++++ .../android/ui/deeplinks/DeepLinkNavigator.kt | 6 ++ .../deeplinks/handlers/ReaderLinkHandler.kt | 53 +++++++++++++++++- .../ui/reader/ReaderActivityLauncher.kt | 11 ++++ .../handlers/ReaderLinkHandlerTest.kt | 56 +++++++++++++++++++ 6 files changed, 163 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index c51ebca1aba1..556d030af767 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -621,6 +621,26 @@ android:pathPattern="/reader/feeds/.*" android:scheme="http" /> + + + + + + + + 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 fb7794724c21..4a5265f97d40 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -355,6 +355,26 @@ public static void viewReaderFeedInNewStack(Context context, long feedId) { .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(Context context, 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 2000ab7da3b7..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 @@ -21,6 +21,8 @@ import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenQ 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 @@ -81,7 +83,9 @@ 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, @@ -128,7 +132,9 @@ class DeepLinkNavigator 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 cc4948608e20..27622cda719a 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 @@ -10,6 +10,8 @@ import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenF 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 @@ -45,6 +47,8 @@ class ReaderLinkHandler isWordPressComReaderUrl(uri) || isWordPressComDiscoverUrl(uri) || isWordPressComFeedUrl(uri) || + isWordPressComReaderSearchUrl(uri) || + isWordPressComTagUrl(uri) || intentUtils.canResolveWith(ReaderConstants.ACTION_VIEW_POST, uri) } @@ -69,13 +73,38 @@ class ReaderLinkHandler return uri.host == HOST_WORDPRESS_COM && segments.size == FEED_URL_SEGMENTS && (segments.firstOrNull() == PATH_READ || segments.firstOrNull() == PATH_READER) && - segments.getOrNull(BLOGS_FEEDS_PATH_POSITION) == PATH_FEEDS + segments.getOrNull(SECOND_PATH_POSITION) == PATH_FEEDS + } + + /** + * Checks if this is a reader search URL like wordpress.com/read/search + */ + private fun isWordPressComReaderSearchUrl(uri: UriWrapper): Boolean { + val segments = uri.pathSegments + return uri.host == HOST_WORDPRESS_COM && + segments.size == SEARCH_URL_SEGMENTS && + segments.firstOrNull() == PATH_READ && + segments.getOrNull(SECOND_PATH_POSITION) == PATH_SEARCH + } + + /** + * 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 @@ -92,6 +121,7 @@ class ReaderLinkHandler } isWordPressComReaderUrl(uri) -> OpenReader isWordPressComDiscoverUrl(uri) -> OpenReaderDiscover + isWordPressComReaderSearchUrl(uri) -> OpenReaderSearch isWordPressComFeedUrl(uri) -> { val feedId = extractFeedId(uri) if (feedId != null) { @@ -101,6 +131,15 @@ class ReaderLinkHandler OpenReader } } + isWordPressComTagUrl(uri) -> { + val tagSlug = extractTagSlug(uri) + if (!tagSlug.isNullOrBlank()) { + OpenTagInReader(tagSlug) + } else { + _toast.value = Event(R.string.error_generic) + OpenReader + } + } else -> OpenInReader(uri) } } @@ -141,7 +180,9 @@ 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 @@ -172,7 +213,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") } @@ -192,14 +233,20 @@ class ReaderLinkHandler 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" private const val BLOG_ID = "blogId" private const val POST_ID = "postId" private const val FEED_ID = "feedId" + private const val TAG_SLUG = "tagSlug" private const val CUSTOM_DOMAIN_POSITION = 3 - private const val BLOGS_FEEDS_PATH_POSITION = 1 + 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 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/reader/ReaderActivityLauncher.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.kt index 51c84d44deb3..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 @@ -207,6 +208,15 @@ object ReaderActivityLauncher { } } + /** + * 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 */ @@ -241,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/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/ReaderLinkHandlerTest.kt index 54abba9a82b2..58800cf2d798 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 @@ -13,6 +13,8 @@ import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenF 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 @@ -325,4 +327,58 @@ class ReaderLinkHandlerTest : BaseUnitTest() { 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 tag path`() { + val uri = buildUri("wordpress.com", "tag", "dogs") + + val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) + + assertThat(isReaderUri).isTrue() + } + + @Test + fun `wordpress com tag opens tag in reader`() { + val uri = buildUri("wordpress.com", "tag", "dogs") + + val navigateAction = readerLinkHandler.buildNavigateAction(uri) + + assertThat(navigateAction).isEqualTo(OpenTagInReader("dogs")) + } + + @Test + fun `correctly strips wordpress com tag URI`() { + val uri = buildUri("wordpress.com", "tag", "dogs") + + val strippedUrl = readerLinkHandler.stripUrl(uri) + + assertThat(strippedUrl).isEqualTo("wordpress.com/tag/tagSlug") + } } From 4b98e77a69155936e63a3e89697c0d24b185fc97 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 15:01:05 +0100 Subject: [PATCH 05/10] Some improvements --- WordPress/src/main/AndroidManifest.xml | 10 +++++++ .../deeplinks/handlers/ReaderLinkHandler.kt | 22 ++++++++++----- .../handlers/ReaderLinkHandlerTest.kt | 27 ++++++++++++++++--- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 556d030af767..202fc466ad85 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -631,6 +631,16 @@ android:path="/read/search" android:scheme="http" /> + + + + DATE_URL_SEGMENTS) { append("/YYYY/MM/DD/$POST_ID") @@ -227,23 +228,32 @@ 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 TAG_SLUG = "tagSlug" - private const val CUSTOM_DOMAIN_POSITION = 3 + + // 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 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 58800cf2d798..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 @@ -32,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() { @@ -355,9 +356,27 @@ class ReaderLinkHandlerTest : BaseUnitTest() { 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", "dogs") + val uri = buildUri("wordpress.com", "tag", tagSlug) val isReaderUri = readerLinkHandler.shouldHandleUrl(uri) @@ -366,16 +385,16 @@ class ReaderLinkHandlerTest : BaseUnitTest() { @Test fun `wordpress com tag opens tag in reader`() { - val uri = buildUri("wordpress.com", "tag", "dogs") + val uri = buildUri("wordpress.com", "tag", tagSlug) val navigateAction = readerLinkHandler.buildNavigateAction(uri) - assertThat(navigateAction).isEqualTo(OpenTagInReader("dogs")) + assertThat(navigateAction).isEqualTo(OpenTagInReader(tagSlug)) } @Test fun `correctly strips wordpress com tag URI`() { - val uri = buildUri("wordpress.com", "tag", "dogs") + val uri = buildUri("wordpress.com", "tag", tagSlug) val strippedUrl = readerLinkHandler.stripUrl(uri) From 53e19b9d9c5faf26daef31cdf8e039f2164653c2 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 15:09:44 +0100 Subject: [PATCH 06/10] Re-adding deleted code --- WordPress/src/main/AndroidManifest.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 202fc466ad85..52e78aa53d4b 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -691,6 +691,18 @@ android:scheme="http" > + + + + + + Date: Fri, 28 Nov 2025 16:55:40 +0100 Subject: [PATCH 07/10] Lint fixes --- WordPress/src/main/AndroidManifest.xml | 56 +++++++++++++++++++------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 52e78aa53d4b..e77c58834043 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -624,32 +624,38 @@ + 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" /> @@ -682,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" > + + + + + + From 43c3cef62a21c6326b7ee1016898cf6ad8ffce9d Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 17:57:15 +0100 Subject: [PATCH 08/10] Adding tests for ReaderActivityLauncher --- WordPress/build.gradle | 2 + .../ui/reader/ReaderActivityLauncherTest.kt | 95 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderActivityLauncherTest.kt 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/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) + } +} From 2625667e816a74995ead5b512412363ae9159101 Mon Sep 17 00:00:00 2001 From: Adalberto Plaza Date: Fri, 28 Nov 2025 17:28:10 +0100 Subject: [PATCH 09/10] Update WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../main/java/org/wordpress/android/ui/ActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4a5265f97d40..c038964765b5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -365,7 +365,7 @@ public static void viewReaderSearchInNewStack(Context context) { .startActivities(); } - public static void viewReaderTagInNewStack(Context context, String tagSlug) { + 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"); From 9eb3e36b971158d89bd88dddb39cabb8bc6d2ca0 Mon Sep 17 00:00:00 2001 From: adalpari Date: Mon, 1 Dec 2025 12:11:30 +0100 Subject: [PATCH 10/10] chore: trigger CI