Skip to content

fix: カメラ権限の要求とWebViewの権限許可を見直す - #17

Merged
5ym merged 2 commits into
masterfrom
fix/webview-permissions
Sep 16, 2026
Merged

5ym merged 2 commits into
masterfrom
fix/webview-permissions

Conversation

@5ym

@5ym 5ym commented Sep 16, 2026

Copy link
Copy Markdown
Owner

概要

#16 のストア掲載調査で見つかった、ストアとは無関係に問題のある 2 箇所を修正する。

1. 起動時のカメラ権限要求(実質バグ)

MainActivity.onCreate が起動直後に無条件で CAMERA を要求し、拒否されると continueStartup() が呼ばれないまま WebView が読み込まれなかった。復帰経路も無いため、空白画面のまま固まる

registerForActivityResult(...) { granted ->
    if (granted) { continueStartup() } else { Toast(...) }   // 拒否すると何も起きない
}

カメラは表示するページが使う場合にのみ必要なもので、中核機能ではない。カメラを使わないページを表示するだけのキオスクでもカメラが必須になっていた。

ページが onPermissionRequest で実際にカメラを要求した時点で初めて権限を求めるよう変更した。

2. WebView からの権限要求を無条件許可(セキュリティ)

override fun onPermissionRequest(request: PermissionRequest) {
    request.grant(request.resources)
}

読み込んだページからの権限要求を、内容を問わず許可していた。表示先が固定されていてもページが遷移すれば任意のサイトが対象になり得る。

マニフェストで宣言しているカメラのみを、端末の権限が付与されている場合に限って許可するよう変更した。音声取得や保護されたメディアなど宣言していないリソースは、要求されても通さない。

実装

判定ロジックは Android 非依存の WebPermissionPolicy に切り出し、ローカルユニットテストを追加した(CONTRIBUTING.md の方針に沿う形)。

Activity 側は応答待ちの PermissionRequest を保持し、権限ダイアログの結果を受けて grant() / deny() を返す。破棄時と、要求が重なった場合の古い要求は deny() する。

文言が実態と合わなくなったため camera_permission_requiredcamera_permission_denied に差し替えた。

やらなかったこと

課金は商用ライセンス直販に集中する方針となったため、Play 掲載に固有の対応(AAB、開示・同意 UI、プライバシーポリシー)は入れていない。

isAccessibilityTool="false" の明示も見送った。既定値が false で動作は変わらず、Play Console への申告が不要になった以上は意味を持たないため。

🤖 Generated with Claude Code

起動時に無条件でカメラ権限を要求しており、拒否されると continueStartup() が
呼ばれないまま WebView が読み込まれず、復帰経路も無いため空白画面で固まっていた。
カメラを使わないページを表示するだけのキオスクでも権限が必須になっていた。

ページが実際にカメラを要求した時点で初めて権限を求めるよう変更する。

あわせて onPermissionRequest が request.grant(request.resources) で要求内容を問わず
許可していたのを、宣言済みのカメラのみを、権限が付与されている場合に限って
許可するよう変更する。判定は Android 非依存の WebPermissionPolicy に切り出した。

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

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown

Claude finished @5ym's task in 2m 52s —— View job


レビュー完了

  • コンテキストの収集(PR差分、CLAUDE.md/CONTRIBUTING.md確認)
  • MainActivity.kt の変更内容を確認
  • WebPermissionPolicy.kt / テストの確認
  • strings.xml の確認
  • レビューコメントの投稿

総評

起動時のカメラ権限強制要求と onPermissionRequest の無条件許可という 2 つの問題に対する妥当な修正だと思います。判定ロジックを Android 非依存の WebPermissionPolicy に切り出し、CONTRIBUTING.md の方針通りローカルユニットテストを添えている点も良いです。テストはカメラ許可済み/未許可、宣言外リソース(audio/protectedMedia/midiSysex)、混在ケース、空リクエストまで網羅されており十分です。

インラインで 2 点、動作を確認しておきたい箇所にコメントしました(いずれも「要確認」、致命的ではないと考えています)。

  1. MainActivity.kt 105-108行目: 短時間に複数回カメラ権限要求が来た場合、前の要求を deny() しつつ同じ ActivityResultLauncher に対して launch() を重ねて呼ぶ形になります。前回の呼び出し(システムダイアログ表示中)が完了する前の再 launch() が安全かどうか未確認です。
  2. MainActivity.kt 297-303行目 (onDestroy): 画面回転などの設定変更による再生成時にも onDestroy() が呼ばれ、pendingPermissionRequestdeny() します。システム権限ダイアログ自体は継続表示されるため、ユーザーが後から「許可」を選んでもページ側にはすでに拒否として応答済みになります。マニフェストに向き固定の指定が見当たらないため、回転可能な端末では発生し得るケースです。

その他、セキュリティ面(宣言済みリソースのみ許可・端末権限が無ければ拒否)や設計(既存コードとの一貫性)については問題を見つけませんでした。

Comment on lines +105 to +108
// 応答待ちの要求が残っている場合は、古い方を取り下げて新しい方だけを扱う。
pendingPermissionRequest?.deny()
pendingPermissionRequest = request
requestCameraPermission.launch(Manifest.permission.CAMERA)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

要確認: ページが短時間に複数回カメラを要求した場合、古い要求は deny() しつつ requestCameraPermission.launch() を再度呼んでいますが、これは同一の ActivityResultLauncher に対して前回の呼び出しがまだ完了していない(システムの権限ダイアログ表示中)状態で launch() を重ねて呼ぶことになります。AndroxX の ActivityResultLauncher は同時並行の launch() を想定しておらず、環境によっては例外やダイアログの多重表示につながる可能性があります。発生頻度は低いと思われますが、念のため確認をお願いします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

妥当な懸念なので、launch() を重ねて呼ぶ形自体をやめました。

応答待ちの要求がある間は、新しい要求をその場で deny() して launch() を呼ばないようにしています。これで同一 launcher への多重 launch は発生しません。

// 権限ダイアログを重ねて出さないよう、応答待ちの間は新しい要求を取り下げる。
// 拒否されたページは、必要であれば改めて要求してくる。
if (pendingPermissionRequest != null) {
    request.deny()
    return
}
pendingPermissionRequest = request
requestCameraPermission.launch(Manifest.permission.CAMERA)

どちらの形でもいずれか一方の要求は拒否されますが、多重 launch の挙動に依存しない分こちらが確実だと判断しました。拒否されたページ側は、必要なら改めて getUserMedia() を呼んで再要求できます。

}

override fun onDestroy() {
pendingPermissionRequest?.deny()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

要確認: 画面回転などの設定変更で Activity が再生成される場合も onDestroy() は呼ばれ、pendingPermissionRequest?.deny() が実行されます。しかしシステムのカメラ権限ダイアログ自体は再生成後の新しいインスタンスに引き継がれて表示され続けるため、ユーザーが「許可」を選んでも、ページ側にはすでに deny() 済みとして応答されてしまいます(新インスタンスの pendingPermissionRequestnull のため、後から届く requestCameraPermission のコールバックは何もせず無視されます)。

キオスク端末で画面回転が実質発生しない運用であれば実害は小さいですが、AndroidManifest.xml に向きの固定(android:screenOrientation など)が見当たらないため、回転可能な端末では「許可したのに使えない」という体験になり得ます。設定変更による再生成と本当にアプリを離れる場合(isFinishing/isChangingConfigurations)を区別するか、pendingPermissionRequest を引き継ぐ対応が要るかもしれません。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

設定変更で onDestroy() が走り deny() されるという指摘は、そのとおりです。ただ実害は無いと判断し、この点は変更していません。

理由は、同じ再生成によって PermissionRequest の送り主であるページ自体が失われるためです。

AndroidManifest.xmlMainActivityandroid:configChanges の指定が無いため、回転時は Activity が破棄・再生成されます。WebViewactivity_main.xml から findViewById で取得している通常のビューで、状態の保存・復元も行っていないため、再生成後は新しい WebView が作られ、startKiosk() から loadUrl() でページが読み込み直されます。

つまり再生成前の PermissionRequest は、対応するページがもう存在しないので、応答しても意味がありません。deny() せずに保持し続けても宙に浮くだけです。

ユーザー体験としても破綻しません。

  1. 回転前: ページがカメラを要求 → 権限ダイアログ表示
  2. 回転 → Activity 再生成、deny()、ページ再読み込み
  3. ユーザーが「許可」を選択 → ActivityResultLauncher のコールバックは復元された registry 経由で新インスタンスに届くが、pendingPermissionRequest は null なので何もしない
  4. 読み込み直されたページが再度カメラを要求 → isCameraGranted() が true になっているため、権限ダイアログ無しで即座に許可される

結果として、回転を挟んでもページはカメラを使えます。

なお android:configChanges を指定して回転時の再生成自体を避ける手もありますが、それはキオスク表示の挙動を変える別の判断になるため、本PRの範囲外としています。

5ym added a commit that referenced this pull request Sep 16, 2026
「修正済み」と書いていたが、実際の修正は別ブランチの #17 にあり、この差分には
含まれていない。本文側の「(要修正)」という記載とも矛盾していた。
参照先を明示する形に統一する。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
応答待ちの要求を deny() しつつ同じ launcher へ launch() を重ねて呼ぶ形になっており、
システムダイアログ表示中の再 launch が安全か確証が無かった。
応答待ちの間は新しい要求をその場で取り下げ、launch を重ねない形にする。

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

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown

Claude finished @5ym's task in 1m 19s —— View job


レビュー完了

  • コンテキストの収集(CLAUDE.md / CONTRIBUTING.md 確認、PR差分確認)
  • MainActivity.kt の変更内容を確認
  • WebPermissionPolicy.kt / テストの確認
  • strings.xml の確認
  • レビューコメントの投稿

総評

前回のレビューで指摘した2点のうち、1点(応答待ちの要求に対して同一 ActivityResultLauncherlaunch() を重ねて呼んでいた箇所)はコミット 008c99a で解消されています。応答待ちの間は新しい要求をその場で deny() する形に変わり、launch() の重複呼び出しは無くなりました。良い対応だと思います。

もう1点(画面回転など設定変更による Activity 再生成時に onDestroy()pendingPermissionRequestdeny() してしまい、システムダイアログでの「許可」がページに反映されない経路)は今回のPRの範囲では未対応のようです。該当行にインラインコメントを付けました(要確認・致命的ではないと考えています)。

その他、今回の変更全体について:

  • 不具合: 上記のインラインコメント以外に、ロジックの誤りや境界値の問題は見当たりませんでした。
  • セキュリティ: onPermissionRequest がマニフェストで宣言済みのカメラのみを、端末の権限が付与されている場合に限り許可する形になっており、未宣言のリソース(音声取得・保護されたメディア等)は要求されても通さない点は妥当です。起動時にカメラ権限を無条件要求していた問題も、ページが実際に要求した時点まで遅延させることで解消されています。
  • 設計: 判定ロジックを Android 非依存の WebPermissionPolicy に切り出しており、CONTRIBUTING.md の方針(Android 非依存ロジックの切り出し+ローカルユニットテスト)に沿っています。既存コードとの一貫性も問題ありません。
  • テスト: WebPermissionPolicyTest はカメラ許可済み/未許可、宣言外リソース(audio/protectedMedia/midiSysex)、許可対象と非対象が混在するケース、空リクエストまで網羅されており十分です。


override fun onDestroy() {
pendingPermissionRequest?.deny()
pendingPermissionRequest = null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

要確認(前回レビューの再掲・未対応): onDestroy() は画面回転などの設定変更による再生成時にも呼ばれます。AndroidManifest.xmlMainActivity の向き固定 (android:screenOrientation) も configChanges の指定も見当たらないため、回転可能な端末では設定変更のたびに再生成が起こり得ます。

その場合、システムのカメラ権限ダイアログ自体は継続表示されますが、pendingPermissionRequest?.deny() によりページには既に「拒否」として応答済みになります。ユーザーがダイアログで「許可」を選んでも、新しいインスタンスの pendingPermissionRequestnull のため requestCameraPermission のコールバックは何もせず無視され、結果的に「許可したのにカメラが使えない」という体験になります。

今回のコミット (008c99a) で launch() の重複呼び出しは解消されましたが、この設定変更時の経路は今回のPRの範囲では対応されていないようです。キオスク運用で回転が発生しないなら実害は小さいですが、isFinishing/isChangingConfigurations で区別する、または onRetainNonConfigurationInstance 等で pendingPermissionRequest を引き継ぐ対応の検討をお勧めします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

この点は前回のスレッド(#17 (comment) )で回答済みです。意図的に変更していません。

要点だけ再掲します。同じ再生成によって PermissionRequest の送り主であるページ自体が失われるため、応答しても意味がありません。

  • MainActivityandroid:configChanges の指定が無いので回転時は再生成される
  • WebView は状態の保存・復元をしておらず、再生成後は startKiosk()loadUrl() でページが読み込み直される
  • したがって再生成前の PermissionRequest は対象を失っており、deny() せず保持しても宙に浮くだけ

ユーザー体験も破綻しません。読み込み直されたページが再度カメラを要求した時点で isCameraGranted() が true になっているため、権限ダイアログ無しで即座に許可されます。

@5ym
5ym merged commit 226c3dc into master Sep 16, 2026
3 checks passed
@5ym
5ym deleted the fix/webview-permissions branch September 16, 2026 03:57
5ym added a commit that referenced this pull request Sep 16, 2026
* docs: ストア掲載の可否調査と手順を追加

Google Play への掲載が規約上可能かを調査し、結果と手順を PUBLISHING.md にまとめる。

掲載自体は可能だが、AccessibilityService の非アクセシビリティ利用にあたるため、
アプリ内での開示・同意、Play Console での宣言、デモ動画の提出が必要になる。
あわせてカメラ権限の要求方法と WebView の権限要求の扱いに要修正箇所がある。

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

* docs: ストア掲載を見送る方針を追記

課金は商用ライセンスの直販に集中する方針となったため、Google Play への掲載は
当面見送る。調査記録は方針を変える場合の資料として残す。

あわせて、掲載と無関係に問題のある 2 箇所を修正済みである旨を反映する。

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

* docs: 2箇所の修正状況の記載を #17 への参照に直す

「修正済み」と書いていたが、実際の修正は別ブランチの #17 にあり、この差分には
含まれていない。本文側の「(要修正)」という記載とも矛盾していた。
参照先を明示する形に統一する。

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant