Skip to content

Fix crash when a deep link opens with no browser available#23121

Merged
adalpari merged 3 commits into
trunkfrom
fix/deeplink-browser-crash
Jul 21, 2026
Merged

Fix crash when a deep link opens with no browser available#23121
adalpari merged 3 commits into
trunkfrom
fix/deeplink-browser-crash

Conversation

@adalpari

@adalpari adalpari commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes an ActivityNotFoundException crash in DeepLinkingIntentReceiverActivity:

android.content.ActivityNotFoundException: No Activity found to handle Intent
{ act=android.intent.action.VIEW dat=https://www.wordpress.com/... }
    at org.wordpress.android.ui.deeplinks.DeepLinkNavigator.handleNavigationAction(DeepLinkNavigator.kt:58)

How it happens. A WordPress.com email link (https://public-api.wordpress.com/mbar/...) is caught by the
app's intent filter. DeepLinkingIntentReceiverViewModel unwraps its redirect_to parameter, finds no in-app
handler for the target, and emits OpenInBrowser. DeepLinkNavigator then fired an implicit ACTION_VIEW
intent with no guard, so on a device where nothing can handle a web link — no browser installed, browser
disabled, or restricted by a work profile — the deep link receiver crashed before it could tell the user
anything.

The fix. The OpenInBrowser branch moves into a small openInBrowser() helper that catches
ActivityNotFoundException and reports it with a toast plus an AppLog entry. This matches how the rest of
the app already opens external URLs (ActivityLauncher.openUrlExternal, ReaderActivityLauncher,
StatsNavigator, ActivityNavigator) — this call site was the outlier.

The helper also calls finish() on the failure path. The receiver activity has no UI of its own: it uses an
opaque theme (WordPress.NoActionBar) and never calls setContentView, and nothing on the OpenInBrowser
path finishes it — the view model only emits finish for links it could not build a navigation action for.
Without the finish() call the user tapping a link from an email would get the toast and then be parked on a
blank screen until they pressed back. The toast is unaffected, since it does not depend on the activity
staying alive.

No tests added: there is no existing DeepLinkNavigatorTest, and handleNavigationAction takes a raw
AppCompatActivity, so covering this would need a wrapper refactor beyond the scope of a crash fix.

Follow-up (not in this PR)

While tracing the crash I found a related gap worth its own issue: the failing URL was on www.wordpress.com,
but every link handler compares the host with exact equality against HOST_WORDPRESS_COM = "wordpress.com"
(StatsLinkHandler, PagesLinkHandler, ReaderLinkHandler, DeepLinkUriUtils.isWpLoginUrl), and the
manifest web-link filters only declare android:host="wordpress.com". So email links redirecting to the www.
host bounce out to the browser instead of opening in-app, even on devices that have a browser. Fixing that
means normalising the host across the handlers, plus assetlinks.json verification for the www. hostname if
the app should intercept those links directly.

Testing instructions

Both checks use the same deep link. Trigger it either via adb:

adb shell am start -n <applicationId>/org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverActivity \
  -a android.intent.action.VIEW \
  -d "https://public-api.wordpress.com/mbar/?redirect_to=https%3A%2F%2Fwww.wordpress.com%2Fsupport%2F"

...or by tapping the same link from an external app (an email or a notes app).

1. Normal hand-off (device with a browser)

  1. Install this branch on a device or emulator that has a browser.
  2. Trigger the deep link.
  • Verify the app opens and handles the deep link (no crash).
  • Verify it then hands off to the browser, which loads the redirect target.

2. Crash scenario (device with no browser available)

  1. Disable every app that can handle a web link. List the handlers:
    adb shell cmd package query-activities -a android.intent.action.VIEW \
      -c android.intent.category.BROWSABLE -d "https://www.wordpress.com/support/" --brief
    
    Disable each one it reports (adb shell pm disable-user --user 0 <package>), then run the query
    again. Repeat until it prints No activities found — the query surfaces the preferred handler first,
    so a second browser can stay hidden behind the default one until that one is disabled.
  2. Trigger the deep link again.
  • Verify the app does not crash (before this change it did).
  • Verify an "Unable to open the link" toast is shown.
  • Verify the app closes itself instead of leaving a blank screen on top — you should land back on whatever
    you triggered the link from (the launcher, or the email/notes app).
  1. Re-enable each browser afterwards (adb shell pm enable <package>). If you disabled the default
    browser, Android reassigns the browser role to another app, so check and restore it:
    adb shell cmd role get-role-holders --user 0 android.app.role.BROWSER
    adb shell cmd role add-role-holder --user 0 android.app.role.BROWSER <package>
    

3. Regression check on a deep link the app handles itself

  1. Run adb shell am start -a android.intent.action.VIEW -d "wordpress://stats".
  • Verify it opens Stats in the app as before, without going to the browser.

Why the explicit component (-n) in the deep link command: on a debug build the public-api.wordpress.com
host is not App Links–verified (adb shell pm get-app-links <applicationId> reports it unverified), so
Android 12+ routes the implicit intent straight to the default browser and the app is never launched. Naming
the component bypasses that and exercises the real deep link code. Tapping the link from an external app on a
production build works without this, since the host is verified there.

A tracking link whose redirect target the app can't handle is passed to
an external browser with an implicit ACTION_VIEW intent. On devices with
no app able to handle a web link the intent fails to resolve and the deep
link receiver dies before it can tell the user anything. Report the
missing handler with a toast instead, matching how the rest of the app
opens external URLs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dangermattic

dangermattic commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@wpmobilebot

wpmobilebot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr23121-ef54054
Build Number1498
Application IDorg.wordpress.android.prealpha
Commitef54054
Installation URL0g46luu7ssfr8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr23121-ef54054
Build Number1498
Application IDcom.jetpack.android.prealpha
Commitef54054
Installation URL4epqi69htins0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.68%. Comparing base (0ff05d5) to head (ef54054).

Files with missing lines Patch % Lines
...ordpress/android/ui/deeplinks/DeepLinkNavigator.kt 0.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #23121      +/-   ##
==========================================
- Coverage   37.69%   37.68%   -0.01%     
==========================================
  Files        2343     2343              
  Lines      127339   127342       +3     
  Branches    17658    17658              
==========================================
  Hits        47995    47995              
- Misses      75411    75414       +3     
  Partials     3933     3933              

☔ View full report in Codecov by Harness.
📢 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.

The receiver activity has no UI of its own and nothing on the OpenInBrowser
path finishes it, so after the toast the user was left on an empty screen
until they pressed back. Verified on device with every web link handler
disabled: the toast shows and the launcher comes back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adalpari
adalpari marked this pull request as ready for review July 21, 2026 10:15
@nbradbury

Copy link
Copy Markdown
Contributor

@adalpari This fix is good but can't we simply reuse ActivityLauncher.openUrlExternal here?

@adalpari

adalpari commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@adalpari This fix is good but can't we simply reuse ActivityLauncher.openUrlExternal here?

That was also my thought after Claude made the changes. But when I asked, it turned out to be not exactly what we are looking for:

  1. Unwanted side effect — it calls WPActivityUtils.disableReaderDeeplinks() first, a persistent setComponentEnabledSetting only undone on next app start. Reader deep links would silently switch off every time an email link bounces to the browser. That guard exists for in-app WP URLs; here the VM already knows the URL isn't ours.
  2. Swallows the exception, returns void — the caller can't tell the hand-off failed, so we lose the finish() that keeps the user off a blank screen.
  3. Adds FLAG_ACTIVITY_NEW_TASK — an untested back-stack change riding along on a crash fix.

I'm not sure to understand the purpose after the explanined behaviour in 1, but yes, it's there:

// ActivityLauncher.java:1677-1681
// Disable deeplinking activity so to not catch WP URLs.
// We'll re-enable them later - see callers of WPActivityUtils#enableReaderDeeplinks.
WPActivityUtils.disableReaderDeeplinks(context);
context.startActivity(intent);

@nbradbury

Copy link
Copy Markdown
Contributor

That was also my thought after Claude made the changes. But when I asked, it turned out to be not exactly what we are looking for:

Oof, that's unfortunate - that existing function should just be a safe way to open a URL, but that deep link disabling corrupts it. At any rate, this PR is good and I'll approve it.

@nbradbury nbradbury left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:shipit:

@adalpari
adalpari merged commit 8abbdf1 into trunk Jul 21, 2026
24 checks passed
@adalpari
adalpari deleted the fix/deeplink-browser-crash branch July 21, 2026 11:03
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