WebViewのメディアキャプチャ権限リクエストをフレームワーク側で処理

hotwired/hotwire-native-android

Webページが getUserMedia({ audio, video }) を呼び出した際のパーミッション処理が、ホストアプリ側の実装なしにフレームワーク側で完結するようになりました。新設された WebViewPermissionDelegateWebChromeClient.onPermissionRequest を透過的に処理します。

背景

これまで、Webページからカメラやマイクへのアクセスを要求する getUserMedia() を扱うには、ホストアプリが WebChromeClient.onPermissionRequest を独自にオーバーライドし、Androidのランタイムパーミッション取得からWebViewへの結果返却までを自前で実装する必要がありました。位置情報については既に GeolocationPermissionDelegate によって同様の抽象化が提供されていましたが、カメラ・マイクに相当する仕組みが欠けていました。

本PRはその非対称性を解消し、メディアキャプチャ権限の処理をフレームワーク側に集約します。

技術的な変更

WebViewPermissionDelegate の新設

WebViewPermissionDelegatecore/src/main/kotlin/dev/hotwire/core/files/delegates/WebViewPermissionDelegate.kt)が新設され、SessionwebViewPermissionDelegate プロパティとして保持されます。このクラスが処理するリソースは以下の2種類です。

  • RESOURCE_AUDIO_CAPTURE: RECORD_AUDIO(ランタイム権限)と MODIFY_AUDIO_SETTINGS(インストール時権限)の両方がマニフェストに宣言されていることが必要
  • RESOURCE_VIDEO_CAPTURE: CAMERA(ランタイム権限)が必要

MODIFY_AUDIO_SETTINGS がインストール時権限でありながら必須とされている理由は、Chromium の音声パイプラインが通信デバイスの選択に同権限を要求するためです。RECORD_AUDIO のみ付与された状態では Unable to select communication device! エラーが発生し、getUserMedia({ audio: true }) が失敗します。マニフェスト宣言が不足している場合はリクエスト全体を即座に拒否し、ページ側には NotAllowedError として伝達されます。

キャンセル安全性にも考慮されており、pendingRequest フィールドで保持中のリクエストを追跡します。新しいリクエストが来た際は既存の保留リクエストを先に拒否してから新しいものを保持し、孤立したリクエストが無応答になる状態を防いでいます。

HotwireWebChromeClient への統合

HotwireWebChromeClientonPermissionRequestonPermissionRequestCanceled のオーバーライドが追加されました。

override fun onPermissionRequest(request: PermissionRequest) {
    if (request.requestsMediaCapture()) {
        session.webViewPermissionDelegate.onRequest(request)
    } else {
        super.onPermissionRequest(request)
    }
}

override fun onPermissionRequestCanceled(request: PermissionRequest) {
    session.webViewPermissionDelegate.onCancel(request)
    super.onPermissionRequestCanceled(request)
}

private fun PermissionRequest.requestsMediaCapture(): Boolean {
    val resources = resources ?: return false
    return resources.isNotEmpty() && resources.all { resource ->
        resource == PermissionRequest.RESOURCE_AUDIO_CAPTURE ||
                resource == PermissionRequest.RESOURCE_VIDEO_CAPTURE
    }
}

メディアキャプチャ以外のリソース(RESOURCE_PROTECTED_MEDIA_ID 等)は super に委譲されるため、既存の動作を壊しません。キャンセル時は常にデリゲートへ転送し、デリゲート側で追跡中のリクエストに一致しない場合は no-op として扱われます。

複数権限ランチャーの追加

音声と映像を同時に要求するケースでは、RECORD_AUDIOCAMERA を一度のシステムダイアログで要求する必要があります。これに対応するため、VisitDestination インターフェースと HotwireDestination インターフェースに activityMultiplePermissionsResultLauncher メソッドが追加されました。

fun activityMultiplePermissionsResultLauncher(
    requestCode: Int
): ActivityResultLauncher<Array<String>>? = null

既存の activityPermissionResultLauncher(単一権限用)と対になる形で設計されており、デフォルト実装は null を返します。HotwireWebFragmentHotwireWebBottomSheetFragment の両フラグメントがこのメソッドをオーバーライドし、HOTWIRE_REQUEST_CODE_WEBVIEW_PERMISSION(値: 3738)に対して HotwireWebFragmentDelegate 内で登録された webViewPermissionResultLauncher を返します。

private fun registerWebViewPermissionLauncher(): ActivityResultLauncher<Array<String>> {
    return navDestination.fragment.registerForActivityResult(RequestMultiplePermissions()) { results ->
        session.webViewPermissionDelegate.onActivityResult(results)
    }
}

RequestMultiplePermissions コントラクトを使用することで、複数のランタイム権限を一括リクエストし、その結果を Map<String, Boolean> として受け取ってデリゲートに委譲します。

設計判断

既存の GeolocationPermissionDelegate と対称的な設計 が採用されています。Session にデリゲートを保持し、HotwireWebChromeClient がルーティングし、フラグメント側がランチャーを登録するという責務分担は、位置情報権限の実装と完全に対応しています。

リクエストコードの管理も既存パターンを踏襲しており、FileConstants.ktHOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION = 3737 に続く形で HOTWIRE_REQUEST_CODE_WEBVIEW_PERMISSION = 3738 が追加されました。単一権限と複数権限でランチャーのインターフェースを分けた点は、ActivityResultContracts.RequestPermissionRequestMultiplePermissions がそれぞれ異なる型シグネチャ(ActivityResultLauncher<String> vs ActivityResultLauncher<Array<String>>)を持つためで、型安全性を保つ上で合理的な判断です。

ホストアプリへの要求は最小限に抑えられており、マニフェストへの権限宣言のみで対応できます。既存の activityMultiplePermissionsResultLauncher のデフォルト実装が null を返すため、メディアキャプチャが不要なアプリへの影響はありません。

まとめ

この変更により、getUserMedia() を使用するWebページのサポートがホストアプリのボイラープレートなしに実現できるようになりました。GeolocationPermissionDelegate で確立されたパターンをメディアキャプチャへ一貫して適用することで、フレームワークのパーミッション処理の統一性が高まっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
ab813f27

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が明確に適用されており、非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```言語:ファイルパス)およびPR番号のリンク記法([#123](URL))が、ガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

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

専門用語(getUserMedia, WebChromeClientなど)を前提知識として使用しており、専門知識を持つエンジニアという対象読者に適合した内容になっています。

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

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

各セクションが総論→各論の構成で、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されている3つのコードブロックは、提供されたDiff内のコードとファイルパスを含めて完全に一致しており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`PermissionRequest`, `RESOURCE_AUDIO_CAPTURE`, `ActivityResultLauncher`などの技術用語が、文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

`MODIFY_AUDIO_SETTINGS`権限が必要な理由や、キャンセル安全性の仕組みなど、技術的な説明がPR情報とDiffに基づいており、論理的かつ正確です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのTitle, Description, Diff内容で裏付けられており、推測や憶測に基づくハルシネーションは見られません。

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

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

PR番号(#199)やリクエストコード(HOTWIRE_REQUEST_CODE_WEBVIEW_PERMISSION = 3738)などの数値・固有名詞が、PR情報と一致しており正確です。

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

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

記事タイトル「WebViewのメディアキャプチャ権限リクエストをフレームワーク側で処理」は、PRの主題「Handle WebView media-capture permission requests」を的確に表現しています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPR情報に限定されており、バージョンのサポート状況やリリース日程といったPR外の知識の追記や捏造はありません。

時間表現の正確性 ✓ PASS

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

「これまで...必要でした」「...するようになりました」といった時間表現が、変更の前後関係を正確に反映しており、PRの内容と一致しています。