Skip to content

Conversation

@adalpari
Copy link
Contributor

@adalpari adalpari commented Nov 28, 2025

Description

This PR adds support for handling various Reader-related deeplinks, allowing users to open specific Reader content directly from URLs.

More info here: https://github.com/wordpress-mobile/wordpress-mobile-specs/blob/trunk/specs/URL%20Handling.md

https://wordpress.com/read - Reader main feed
https://wordpress.com/discover - Discover stream
https://wordpress.com/read/search - Reader search
https://wordpress.com/tag/dogs - Tag stream
https://wordpress.com/read/feeds/138734090/posts/5879194632 - Specific feed post
https://wordpress.com/reader/feeds/138734090/posts/5879194632
https://wordpress.com/read/feeds/138734090
https://wordpress.com/reader/feeds/138734090 (edited)

Note: This one is missing, but looking for an example to test:
https://wordpress.com/read/blogs/{blogId}/posts/{postId} - Specific blog post

Testing instructions

Note: logged out cases are not tested here as I'll work in a different PR for that scenario

  1. Test all the deeplinks provided in the description
  • Verify they are handled by the app and work as expected

Bonus: try corrupted deeplinks. They shoudl directy open the browser, or show an error inside the app
https://wordpress.com/rad
https://wordpress.com/dicover
https://wordpress.com/read/sarch
https://wordpress.com/tas/dogs
https://wordpress.com/read/feeds/13734090/posts/587194632
https://wordpress.com/reader/feeds/13874090/posts/587194632
https://wordpress.com/read/feeds/1387340
https://wordpress.com/reader/feeds/1387390

@dangermattic
Copy link
Collaborator

dangermattic commented Nov 28, 2025

2 Warnings
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Nov 28, 2025

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: wordpressVanillaRelease):

--- ./build/reports/diff_manifest/WordPress/wordpressVanillaRelease/base_manifest.txt	2025-11-28 17:03:09.375987331 +0000
+++ ./build/reports/diff_manifest/WordPress/wordpressVanillaRelease/head_manifest.txt	2025-11-28 17:03:18.556028842 +0000
@@ -596,6 +596,62 @@
                     android:host="wordpress.com"
                     android:pathPattern="/site-monitoring/.*"
                     android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/discover"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/discover"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/read/feeds/.*"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/read/feeds/.*"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/reader/feeds/.*"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/reader/feeds/.*"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read/search"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read/search"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/reader/search"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/reader/search"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/tag/.*"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/tag/.*"
+                    android:scheme="http" />
             </intent-filter>
         </activity-alias> <!-- Custom Wordpress URI Scheme Deep Linking Activity Alias -->
         <activity-alias

Go to https://buildkite.com/automattic/wordpress-android/builds/24019/canvas?sid=019acb66-a166-45eb-b181-b334bcee8e52, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Nov 28, 2025

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: jetpackVanillaRelease):

--- ./build/reports/diff_manifest/WordPress/jetpackVanillaRelease/base_manifest.txt	2025-11-28 17:03:40.907057743 +0000
+++ ./build/reports/diff_manifest/WordPress/jetpackVanillaRelease/head_manifest.txt	2025-11-28 17:03:52.367112562 +0000
@@ -233,6 +233,62 @@
                     android:host="wordpress.com"
                     android:pathPattern="/site-monitoring/.*"
                     android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/discover"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/discover"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/read/feeds/.*"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/read/feeds/.*"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/reader/feeds/.*"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/reader/feeds/.*"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read/search"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/read/search"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/reader/search"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:path="/reader/search"
+                    android:scheme="http" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/tag/.*"
+                    android:scheme="https" />
+                <data
+                    android:host="wordpress.com"
+                    android:pathPattern="/tag/.*"
+                    android:scheme="http" />
             </intent-filter>
         </activity-alias> <!-- Custom Wordpress URI Scheme Deep Linking Activity Alias -->
         <activity-alias

Go to https://buildkite.com/automattic/wordpress-android/builds/24019/canvas?sid=019acb66-a167-4617-b186-c2e1d9e51d39, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Nov 28, 2025

App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
FlavorJalapeno
Build TypeDebug
Versionpr22374-529cf47
Commit529cf47
Direct Downloadjetpack-prototype-build-pr22374-529cf47.apk
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Nov 28, 2025

App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
FlavorJalapeno
Build TypeDebug
Versionpr22374-529cf47
Commit529cf47
Direct Downloadwordpress-prototype-build-pr22374-529cf47.apk
Note: Google Login is not supported on these builds.

context.startActivity(intent);
}

public static void viewReaderDiscoverInNewStack(Context context) {

Check notice

Code scanning / Android Lint

Nullable/NonNull annotation missing on method parameter Note

Missing null annotation
@codecov
Copy link

codecov bot commented Nov 28, 2025

Codecov Report

❌ Patch coverage is 51.20000% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.06%. Comparing base (06a7f4e) to head (529cf47).

Files with missing lines Patch % Lines
...ava/org/wordpress/android/ui/ActivityLauncher.java 0.00% 25 Missing ⚠️
...ss/android/ui/reader/viewmodels/ReaderViewModel.kt 5.55% 16 Missing and 1 partial ⚠️
...android/ui/deeplinks/handlers/ReaderLinkHandler.kt 86.44% 4 Missing and 4 partials ⚠️
...droid/ui/reader/repository/ReaderPostRepository.kt 0.00% 7 Missing ⚠️
...ordpress/android/ui/deeplinks/DeepLinkNavigator.kt 50.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22374      +/-   ##
==========================================
+ Coverage   39.02%   39.06%   +0.03%     
==========================================
  Files        2203     2203              
  Lines      106342   106442     +100     
  Branches    15061    15106      +45     
==========================================
+ Hits        41501    41580      +79     
- Misses      61350    61365      +15     
- Partials     3491     3497       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adalpari adalpari marked this pull request as ready for review November 28, 2025 16:22
@adalpari
Copy link
Contributor Author

@claude

@claude
Copy link

claude bot commented Nov 28, 2025

Claude finished @adalpari's task —— View job


Code Review Complete ✓

I've completed a comprehensive review of this PR that adds Reader deeplink support. Overall, the implementation is solid with good test coverage, but there are a few areas that need attention.


Summary

This PR successfully implements deeplink handling for various Reader-related URLs including /read, /discover, /read/search, /tag/{slug}, and /read/feeds/{feedId}. The implementation follows established patterns in the codebase and includes comprehensive unit tests.

Strengths:

  • ✅ Well-structured code with clear separation of concerns
  • ✅ Comprehensive unit test coverage (176 test cases added)
  • ✅ Good error handling with fallback to OpenReader action
  • ✅ Proper analytics tracking for viewpost intercepts
  • ✅ Consistent with existing deeplink patterns

Areas for Improvement:

  • ⚠️ Security: Missing null annotations flagged by scanner
  • ⚠️ Code coverage: 44.8% patch coverage with 69 lines missing coverage
  • ⚠️ Input validation: Could be more robust for edge cases

Critical Issues

1. Missing Null Annotations (Security Scanner Findings)

Location: ActivityLauncher.java:368

  • The security scanner flagged missing @NonNull annotations on method parameters
  • This is required for consistency with codebase standards
  • Action Required: Add annotations to new methods

2. Low Test Coverage for New Code

Affected Files:

  • ActivityLauncher.java: 0% coverage (25 lines missing)
  • ReaderViewModel.kt: 5.55% coverage (17 lines missing)
  • ReaderActivityLauncher.kt: 0% coverage (8 lines missing)

Recommendation: While the core logic in ReaderLinkHandler.kt has excellent test coverage (86.44%), the integration points need tests. Consider adding integration tests or at least basic unit tests for the new launcher methods.


Security Considerations

1. URL Validation ✓ Generally Good

The implementation properly validates URLs through multiple checks:

  • Host validation (wordpress.com only)
  • Path segment validation (exact matches)
  • Safe parsing with toLongOrNull() for numeric values

2. Potential Improvements

Input Validation for Feed IDs (ReaderLinkHandler.kt:103)

private fun extractFeedId(uri: UriWrapper): Long? {
    return uri.pathSegments.getOrNull(FEED_ID_POSITION)?.toLongOrNull()?.takeIf { it > 0 }
}

Consider validating that feed IDs are positive values.

Tag Slug Sanitization (ReaderLinkHandler.kt:138)
Currently only checks for null/blank. Consider validating character set if tag slugs have restrictions (verify requirements first).

3. AndroidManifest Patterns

The pathPattern /read/feeds/.* is very broad. While this is likely intentional for your use case, be aware it matches any path starting with that prefix. The security scanner warned about this, but it's probably acceptable given the URL validation in ReaderLinkHandler.


Code Quality Issues

1. Line Length Violations

⚠️ IMPORTANT: The CLAUDE.md specifies a 120 character line limit. Review the following for compliance:

  • Check all changed files for lines exceeding 120 characters
  • Break long lines according to the style guide

2. Error Handling

Good use of fallback to OpenReader when parsing fails. However, consider:

  • Logging parse failures for debugging (especially in production)
  • More specific error messages for different failure modes

3. Code Organization

The ReaderLinkHandler class is growing in complexity. Consider:

  • Extracting URL validation logic to a separate validator class
  • Creating a sealed class hierarchy for different URL types

Testing Assessment

Strengths

  • ✅ Comprehensive unit tests for ReaderLinkHandler
  • ✅ Good coverage of happy paths and error cases
  • ✅ Tests for URL stripping (analytics)
  • ✅ Tests for both read and reader path variants

Gaps

  1. Missing Integration Tests: No tests for the full deeplink flow
  2. Missing Edge Cases:
    • What happens with extremely long feed IDs?
    • What happens with special characters in tag slugs (emoji, unicode)?
    • What happens with mixed case paths (/Read, /DISCOVER)?
  3. No tests for new ActivityLauncher methods

Recommendations

// Add tests for edge cases:
@Test
fun `handles tag with special characters`() { ... }

@Test
fun `handles extremely long feed id`() { ... }

@Test
fun `handles case sensitivity in paths`() { ... }

Performance Considerations

No significant performance concerns. The URL parsing is lightweight and happens on the UI thread appropriately.


Documentation

Good

  • Clear KDoc comments for URL format checks
  • Well-documented URL handling in comments

Could Improve

  • Add KDoc for public methods in ActivityLauncher.java
  • Document the expected behavior for malformed URLs more explicitly
  • Consider adding a README or design doc link for the deeplink URL scheme

Specific File Reviews

ReaderLinkHandler.kt ⭐ (Excellent)

  • Clean implementation with good separation of concerns
  • Excellent test coverage
  • Clear validation logic
  • Minor suggestions in inline comments

ActivityLauncher.java ⚠️ (Needs Work)

  • Missing null annotations (flagged by scanner)
  • Zero test coverage
  • Otherwise follows established patterns well

AndroidManifest.xml ✓ (Acceptable)

  • Properly declares all intent filters
  • Uses tools:ignore appropriately for known warnings
  • Supports both HTTP and HTTPS (good for testing)

ReaderViewModel.kt ⚠️ (Low Coverage)

  • Changes look correct but lack test coverage
  • The bookmarkTabRequested() and discoverTabRequested() pattern is good

Recommendations

High Priority

  1. Add missing @NonNull annotations to fix security scanner findings
  2. Add basic tests for ActivityLauncher methods
  3. Verify line length compliance (120 character limit)

Medium Priority

  1. Add input validation for feed IDs (positive values)
  2. Consider more robust tag slug validation
  3. Add integration tests for the full deeplink flow

Low Priority

  1. Extract URL validation to separate class for better testability
  2. Add more edge case tests
  3. Improve KDoc coverage

Testing Instructions Verification

The PR description mentions testing "corrupted deeplinks". From code review:

  • ✅ Invalid feed IDs will fallback to Reader main feed
  • ✅ Invalid tag slugs will fallback to Reader main feed
  • ✅ Unknown paths will open in browser (via DeepLinkingIntentReceiverViewModel)
  • ⚠️ Suggestion: Document the expected behavior explicitly in testing instructions

Conclusion

This is a solid PR that implements the required functionality well. The core deeplink handling logic is robust and well-tested. The main concerns are:

  1. Address security scanner findings (missing annotations)
  2. Improve test coverage for integration points
  3. Verify line length compliance

The PR is close to merge-ready after addressing the high-priority items. Great job on the comprehensive test suite for the core logic!

Action Items

  • Add @NonNull annotations to ActivityLauncher.java:341 and :368
  • Consider adding tests for new ActivityLauncher methods
  • Verify all lines are ≤120 characters
  • (Optional) Add validation for positive feed IDs
  • (Optional) Add more edge case tests

Comment on lines -4921 to -4942
<issue
id="IntentFilterUniqueDataAttributes"
message="Consider splitting data tag into multiple tags with individual attributes to avoid confusion"
errorLine1=" &lt;data"
errorLine2=" ^">
<location
file="src/main/AndroidManifest.xml"
line="660"
column="17"/>
</issue>

<issue
id="IntentFilterUniqueDataAttributes"
message="Consider splitting data tag into multiple tags with individual attributes to avoid confusion"
errorLine1=" &lt;data"
errorLine2=" ^">
<location
file="src/main/AndroidManifest.xml"
line="666"
column="17"/>
</issue>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines were useless in the lint ignore rules


<data
android:host="wordpress.com"
android:pathPattern="/read/feeds/.*"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Overly broad pathPattern: The pattern /read/feeds/.* will match ANY path starting with /read/feeds/, including potentially malicious paths like /read/feeds/../../../etc/passwd. While Android's intent filtering doesn't typically allow path traversal attacks, consider using more specific patterns.

The security scanner flagged these data tags as having non-unique attributes. While this is a warning, it might be cleaner to be more explicit with your intent filters. However, this is likely acceptable for your use case.

@Inject constructor(private val activityNavigator: ActivityNavigator) {
@Suppress("ComplexMethod", "LongMethod")
fun handleNavigationAction(navigateAction: NavigateAction, activity: AppCompatActivity) {
when (navigateAction) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is hard to achieve, since these are the cases the app is handling. Maybe we should review all cases in a separate PR to clean it up.

android:pathPattern="/read/feeds/.*/posts/.*"
android:scheme="https" >
android:scheme="https"
tools:ignore="IntentFilterUniqueDataAttributes" >
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the ignore suppression here instead of having a huuuge lint/baseline file

@adalpari
Copy link
Contributor Author

I've now achieved some of the clade suggestions

@sonarqubecloud
Copy link

@adalpari adalpari requested a review from jkmassel November 28, 2025 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants