fix: カメラ権限の要求とWebViewの権限許可を見直す - #17
Conversation
起動時に無条件でカメラ権限を要求しており、拒否されると continueStartup() が 呼ばれないまま WebView が読み込まれず、復帰経路も無いため空白画面で固まっていた。 カメラを使わないページを表示するだけのキオスクでも権限が必須になっていた。 ページが実際にカメラを要求した時点で初めて権限を求めるよう変更する。 あわせて onPermissionRequest が request.grant(request.resources) で要求内容を問わず 許可していたのを、宣言済みのカメラのみを、権限が付与されている場合に限って 許可するよう変更する。判定は Android 非依存の WebPermissionPolicy に切り出した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @5ym's task in 2m 52s —— View job レビュー完了
総評起動時のカメラ権限強制要求と インラインで 2 点、動作を確認しておきたい箇所にコメントしました(いずれも「要確認」、致命的ではないと考えています)。
その他、セキュリティ面(宣言済みリソースのみ許可・端末権限が無ければ拒否)や設計(既存コードとの一貫性)については問題を見つけませんでした。 |
| // 応答待ちの要求が残っている場合は、古い方を取り下げて新しい方だけを扱う。 | ||
| pendingPermissionRequest?.deny() | ||
| pendingPermissionRequest = request | ||
| requestCameraPermission.launch(Manifest.permission.CAMERA) |
There was a problem hiding this comment.
要確認: ページが短時間に複数回カメラを要求した場合、古い要求は deny() しつつ requestCameraPermission.launch() を再度呼んでいますが、これは同一の ActivityResultLauncher に対して前回の呼び出しがまだ完了していない(システムの権限ダイアログ表示中)状態で launch() を重ねて呼ぶことになります。AndroxX の ActivityResultLauncher は同時並行の launch() を想定しておらず、環境によっては例外やダイアログの多重表示につながる可能性があります。発生頻度は低いと思われますが、念のため確認をお願いします。
There was a problem hiding this comment.
妥当な懸念なので、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() |
There was a problem hiding this comment.
要確認: 画面回転などの設定変更で Activity が再生成される場合も onDestroy() は呼ばれ、pendingPermissionRequest?.deny() が実行されます。しかしシステムのカメラ権限ダイアログ自体は再生成後の新しいインスタンスに引き継がれて表示され続けるため、ユーザーが「許可」を選んでも、ページ側にはすでに deny() 済みとして応答されてしまいます(新インスタンスの pendingPermissionRequest は null のため、後から届く requestCameraPermission のコールバックは何もせず無視されます)。
キオスク端末で画面回転が実質発生しない運用であれば実害は小さいですが、AndroidManifest.xml に向きの固定(android:screenOrientation など)が見当たらないため、回転可能な端末では「許可したのに使えない」という体験になり得ます。設定変更による再生成と本当にアプリを離れる場合(isFinishing/isChangingConfigurations)を区別するか、pendingPermissionRequest を引き継ぐ対応が要るかもしれません。
There was a problem hiding this comment.
設定変更で onDestroy() が走り deny() されるという指摘は、そのとおりです。ただ実害は無いと判断し、この点は変更していません。
理由は、同じ再生成によって PermissionRequest の送り主であるページ自体が失われるためです。
AndroidManifest.xml の MainActivity に android:configChanges の指定が無いため、回転時は Activity が破棄・再生成されます。WebView は activity_main.xml から findViewById で取得している通常のビューで、状態の保存・復元も行っていないため、再生成後は新しい WebView が作られ、startKiosk() から loadUrl() でページが読み込み直されます。
つまり再生成前の PermissionRequest は、対応するページがもう存在しないので、応答しても意味がありません。deny() せずに保持し続けても宙に浮くだけです。
ユーザー体験としても破綻しません。
- 回転前: ページがカメラを要求 → 権限ダイアログ表示
- 回転 →
Activity再生成、deny()、ページ再読み込み - ユーザーが「許可」を選択 →
ActivityResultLauncherのコールバックは復元された registry 経由で新インスタンスに届くが、pendingPermissionRequestは null なので何もしない - 読み込み直されたページが再度カメラを要求 →
isCameraGranted()が true になっているため、権限ダイアログ無しで即座に許可される
結果として、回転を挟んでもページはカメラを使えます。
なお android:configChanges を指定して回転時の再生成自体を避ける手もありますが、それはキオスク表示の挙動を変える別の判断になるため、本PRの範囲外としています。
「修正済み」と書いていたが、実際の修正は別ブランチの #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 finished @5ym's task in 1m 19s —— View job レビュー完了
総評前回のレビューで指摘した2点のうち、1点(応答待ちの要求に対して同一 もう1点(画面回転など設定変更による その他、今回の変更全体について:
|
|
|
||
| override fun onDestroy() { | ||
| pendingPermissionRequest?.deny() | ||
| pendingPermissionRequest = null |
There was a problem hiding this comment.
要確認(前回レビューの再掲・未対応): onDestroy() は画面回転などの設定変更による再生成時にも呼ばれます。AndroidManifest.xml に MainActivity の向き固定 (android:screenOrientation) も configChanges の指定も見当たらないため、回転可能な端末では設定変更のたびに再生成が起こり得ます。
その場合、システムのカメラ権限ダイアログ自体は継続表示されますが、pendingPermissionRequest?.deny() によりページには既に「拒否」として応答済みになります。ユーザーがダイアログで「許可」を選んでも、新しいインスタンスの pendingPermissionRequest は null のため requestCameraPermission のコールバックは何もせず無視され、結果的に「許可したのにカメラが使えない」という体験になります。
今回のコミット (008c99a) で launch() の重複呼び出しは解消されましたが、この設定変更時の経路は今回のPRの範囲では対応されていないようです。キオスク運用で回転が発生しないなら実害は小さいですが、isFinishing/isChangingConfigurations で区別する、または onRetainNonConfigurationInstance 等で pendingPermissionRequest を引き継ぐ対応の検討をお勧めします。
There was a problem hiding this comment.
この点は前回のスレッド(#17 (comment) )で回答済みです。意図的に変更していません。
要点だけ再掲します。同じ再生成によって PermissionRequest の送り主であるページ自体が失われるため、応答しても意味がありません。
MainActivityにandroid:configChangesの指定が無いので回転時は再生成されるWebViewは状態の保存・復元をしておらず、再生成後はstartKiosk()→loadUrl()でページが読み込み直される- したがって再生成前の
PermissionRequestは対象を失っており、deny()せず保持しても宙に浮くだけ
ユーザー体験も破綻しません。読み込み直されたページが再度カメラを要求した時点で isCameraGranted() が true になっているため、権限ダイアログ無しで即座に許可されます。
* 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>
概要
#16 のストア掲載調査で見つかった、ストアとは無関係に問題のある 2 箇所を修正する。
1. 起動時のカメラ権限要求(実質バグ)
MainActivity.onCreateが起動直後に無条件でCAMERAを要求し、拒否されるとcontinueStartup()が呼ばれないまま WebView が読み込まれなかった。復帰経路も無いため、空白画面のまま固まる。カメラは表示するページが使う場合にのみ必要なもので、中核機能ではない。カメラを使わないページを表示するだけのキオスクでもカメラが必須になっていた。
ページが
onPermissionRequestで実際にカメラを要求した時点で初めて権限を求めるよう変更した。2. WebView からの権限要求を無条件許可(セキュリティ)
読み込んだページからの権限要求を、内容を問わず許可していた。表示先が固定されていてもページが遷移すれば任意のサイトが対象になり得る。
マニフェストで宣言しているカメラのみを、端末の権限が付与されている場合に限って許可するよう変更した。音声取得や保護されたメディアなど宣言していないリソースは、要求されても通さない。
実装
判定ロジックは Android 非依存の
WebPermissionPolicyに切り出し、ローカルユニットテストを追加した(CONTRIBUTING.md の方針に沿う形)。Activity側は応答待ちのPermissionRequestを保持し、権限ダイアログの結果を受けてgrant()/deny()を返す。破棄時と、要求が重なった場合の古い要求はdeny()する。文言が実態と合わなくなったため
camera_permission_requiredをcamera_permission_deniedに差し替えた。やらなかったこと
課金は商用ライセンス直販に集中する方針となったため、Play 掲載に固有の対応(AAB、開示・同意 UI、プライバシーポリシー)は入れていない。
isAccessibilityTool="false"の明示も見送った。既定値がfalseで動作は変わらず、Play Console への申告が不要になった以上は意味を持たないため。🤖 Generated with Claude Code