BrowserTab と SystemNavigation のルートハンドラで未処理例外を捕捉しクラッシュを防止

hotwired/hotwire-native-android

Production 環境で BrowserTabRouteDecisionHandlerSystemNavigationRouteDecisionHandler が例外を捕捉せずにアプリがクラッシュする事象が報告されました。今回の PR では ActivityNotFoundExceptionSecurityException をハンドリングし、エラーログを出力したうえで Router.Decision.CANCEL を返すように修正しています。これにより、外部ブラウザや他アプリへの遷移が失敗してもホストアプリが停止しないようになります。

背景

未処理例外が原因でアプリがクラッシュするケースは、CustomTabsIntent.launchUrl が対応ブラウザを見つけられないと ActivityNotFoundException をスローすることや、ターゲットアクティビティが android:exported="false" などの理由でアクセス不可になると SecurityException が発生することです。スタックトレースは PR 本文でも示されており、どちらもルートハンドラ内部で例外が捕捉されずにスレッドが終了しています。これらの例外は外部環境に依存するため、ライブラリ側で安全に処理する必要があります。

このような例外は Router.RouteDecisionHandler が UI スレッド上で実行されるため、例外が伝搬するとアプリ全体が即座に停止します。従来は SystemNavigationRouteDecisionHandlerActivityNotFoundException のみを捕捉しており、SecurityException については対策が取られていませんでした。結果として、特定デバイスや深いリンク先が制限されたアプリへの遷移でクラッシュが頻発していました。

技術的な変更

BrowserTabRouteDecisionHandler.kt では CustomTabsIntent.launchUrltry ブロックで囲み、ActivityNotFoundException を捕捉して logError に送る処理が追加されました。変更前後のコードは以下の通りです。

@@ -1,10 +1,12 @@
 package dev.hotwire.navigation.routing

+import android.content.ActivityNotFoundException
 import androidx.browser.customtabs.CustomTabColorSchemeParams
 import androidx.browser.customtabs.CustomTabsIntent
 import androidx.core.net.toUri
 import com.google.android.material.R
 import dev.hotwire.navigation.activities.HotwireActivity
+import dev.hotwire.navigation.logging.logError
 import dev.hotwire.navigation.navigator.NavigatorConfiguration
 import dev.hotwire.navigation.util.colorFromThemeAttr
@@ -39,13 +41,17 @@ class BrowserTabRouteDecisionHandler : Router.RouteDecisionHandler {
             .setNavigationBarColor(color)
             .build()

-        CustomTabsIntent.Builder()
-            .setShowTitle(true)
-            .setShareState(CustomTabsIntent.SHARE_STATE_ON)
-            .setUrlBarHidingEnabled(false)
-            .setDefaultColorSchemeParams(colorParams)
-            .build()
-            .launchUrl(activity, location.toUri())
+        try {
+            CustomTabsIntent.Builder()
+                .setShowTitle(true)
+                .setShareState(CustomTabsIntent.SHARE_STATE_ON)
+                .setUrlBarHidingEnabled(false)
+                .setDefaultColorSchemeParams(colorParams)
+                .build()
+                .launchUrl(activity, location.toUri())
+        } catch (e: ActivityNotFoundException) {
+            logError("BrowserTabRouteDecisionHandler", e)
+        }

         return Router.Decision.CANCEL
     }

この変更により、カスタムタブが利用できない環境でも例外が捕捉され、アプリは CANCEL を返して安全に処理を終了します。エラーログは開発者が原因を把握できるように残ります。

SystemNavigationRouteDecisionHandler.kt にも同様の例外捕捉が拡張され、SecurityException が追加でハンドリングされます。コード差分は次のとおりです。

@@ -31,6 +31,8 @@ class SystemNavigationRouteDecisionHandler : Router.RouteDecisionHandler {
             activity.startActivity(intent)
         } catch (e: ActivityNotFoundException) {
             logError("SystemNavigationRouteDecisionHandler", e)
+        } catch (e: SecurityException) {
+            logError("SystemNavigationRouteDecisionHandler", e)
         }

         return Router.Decision.CANCEL

既に存在していた ActivityNotFoundException の捕捉に加えて SecurityException を捕捉することで、エクスポート制限や権限不足による起動失敗も安全に処理されます。結果として、どちらのハンドラでも例外が発生した場合は一貫して CANCEL が返され、アプリ全体の安定性が向上します。

設計判断

例外を 捕捉してログに残す という方針は、外部アクティビティ起動という不確実な操作に対する防御的プログラミングの実装例です。例外がスローされた場合にスタックトレースを残すことで、開発者は問題の再現性や影響範囲を分析しやすくなります。一方でユーザー体験への直接的な影響は最小化され、アプリは安全にフローを中断します。

ライブラリ側が例外を 吸収して CANCEL を返す 設計は、ホストアプリが例外処理を個別に実装しなくても済むようにする意図が伺えます。これにより、ホスト側のコードはシンプルに保たれ、将来的に新たな例外種別が追加された場合でもハンドラ内部で拡張できる柔軟性が確保されます。

まとめ

BrowserTabRouteDecisionHandlerSystemNavigationRouteDecisionHandler に未処理例外捕捉を追加し、エラーログを出力したうえで決定を CANCEL に統一したことで、外部ブラウザや他アプリへの遷移失敗時にアプリがクラッシュする問題が解消されました。これにより、デバイス構成やアプリのエクスポート設定に依存せず、安定したナビゲーション体験が提供されます。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
cc68a834

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文・背景・技術的な変更・設計判断・まとめの5セクションが揃っており、総論→各論→結論の構成が明確です。まとめはリード文の単なる繰り返しではなく、変更の意義と影響を述べています。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは正しいdiff形式で記載されていますが、PRリンクの記法が「[PR #191](…)」となっており、仕様の「[#191](URL)」形式と完全に一致していません。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

対象はAndroid開発者向けで、専門用語や実装詳細が中心です。初心者向けの過度な解説はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論パラグラフ→具体的事実・コード→結論パラグラフの流れになっており、トピックセンテンスで始まり、段落は1トピックに絞られ、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のdiffブロックは提供されたDiffと完全に一致し、ファイル名・変更箇所・追加コードが正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

ActivityNotFoundException、SecurityException、Router.Decision.CANCEL、logError などの用語はPRで使用されているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

例外ハンドリングを追加しCANCELを返すという主張はPRの説明とコード変更と整合しています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事のすべての主張はPRタイトル、説明、提供されたDiffで裏付けられており、推測や捏造はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #191 が正しく記載されており、他の数値や定数の誤りは見当たりません。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの内容(例外処理の修正)を日本語で的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

LTSやリリース日程等、PRに記載がない外部情報は一切含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の記述はなく、PRと矛盾する表現もありません。