BrowserTab と SystemNavigation のルートハンドラで未処理例外を捕捉しクラッシュを防止
Production 環境で BrowserTabRouteDecisionHandler と SystemNavigationRouteDecisionHandler が例外を捕捉せずにアプリがクラッシュする事象が報告されました。今回の PR では ActivityNotFoundException と SecurityException をハンドリングし、エラーログを出力したうえで Router.Decision.CANCEL を返すように修正しています。これにより、外部ブラウザや他アプリへの遷移が失敗してもホストアプリが停止しないようになります。
背景
未処理例外が原因でアプリがクラッシュするケースは、CustomTabsIntent.launchUrl が対応ブラウザを見つけられないと ActivityNotFoundException をスローすることや、ターゲットアクティビティが android:exported="false" などの理由でアクセス不可になると SecurityException が発生することです。スタックトレースは PR 本文でも示されており、どちらもルートハンドラ内部で例外が捕捉されずにスレッドが終了しています。これらの例外は外部環境に依存するため、ライブラリ側で安全に処理する必要があります。
このような例外は Router.RouteDecisionHandler が UI スレッド上で実行されるため、例外が伝搬するとアプリ全体が即座に停止します。従来は SystemNavigationRouteDecisionHandler が ActivityNotFoundException のみを捕捉しており、SecurityException については対策が取られていませんでした。結果として、特定デバイスや深いリンク先が制限されたアプリへの遷移でクラッシュが頻発していました。
技術的な変更
BrowserTabRouteDecisionHandler.kt では CustomTabsIntent.launchUrl を try ブロックで囲み、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 を返す 設計は、ホストアプリが例外処理を個別に実装しなくても済むようにする意図が伺えます。これにより、ホスト側のコードはシンプルに保たれ、将来的に新たな例外種別が追加された場合でもハンドラ内部で拡張できる柔軟性が確保されます。
まとめ
BrowserTabRouteDecisionHandler と SystemNavigationRouteDecisionHandler に未処理例外捕捉を追加し、エラーログを出力したうえで決定を CANCEL に統一したことで、外部ブラウザや他アプリへの遷移失敗時にアプリがクラッシュする問題が解消されました。これにより、デバイス構成やアプリのエクスポート設定に依存せず、安定したナビゲーション体験が提供されます。