diff --git a/lib/main.dart b/lib/main.dart index 59f379f..9afbde4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -119,12 +120,21 @@ class _H5ShellPageState extends State { } WebViewController _buildController(int lineIndex) { - return WebViewController() + return WebViewController( + onPermissionRequest: (request) { + unawaited(_handleWebViewPermissionRequest(request)); + }, + ) ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( 'OpenIMShell', onMessageReceived: (message) => _handleShellMessage(lineIndex, message), ) + ..addJavaScriptChannel( + 'OpenIMFlutterShell', + onMessageReceived: (message) => + _handleFlutterShellMessage(lineIndex, message), + ) ..setBackgroundColor(_shellBackground) ..setNavigationDelegate( NavigationDelegate( @@ -211,6 +221,35 @@ class _H5ShellPageState extends State { } } + void _handleFlutterShellMessage(int lineIndex, JavaScriptMessage message) { + try { + final decoded = jsonDecode(message.message); + if (decoded is! Map) { + return; + } + + switch (decoded['type']) { + case 'requestMediaPermissions': + final requestId = decoded['requestId']?.toString(); + if (requestId == null || requestId.isEmpty) { + return; + } + unawaited( + _handleShellMediaPermissionRequest( + lineIndex: lineIndex, + requestId: requestId, + audio: decoded['audio'] == true, + video: decoded['video'] == true, + ), + ); + case 'openAppSettings': + unawaited(openAppSettings()); + } + } catch (_) { + // Ignore malformed shell messages from web content. + } + } + void _scheduleShellCoverFallback(int lineIndex) { final slot = _lineSlots[lineIndex]; slot.shellCoverFallbackTimer?.cancel(); @@ -309,6 +348,88 @@ class _H5ShellPageState extends State { return _runJavaScriptSafely(lineIndex, script); } + Future _handleShellMediaPermissionRequest({ + required int lineIndex, + required String requestId, + required bool audio, + required bool video, + }) async { + final result = await _requestNativeMediaPermissions( + audio: audio, + video: video, + ); + final payload = jsonEncode({ + 'requestId': requestId, + 'granted': result.granted, + 'permanentlyDenied': result.permanentlyDenied, + 'restricted': result.restricted, + }); + final script = ''' +(() => { + try { + window.dispatchEvent(new CustomEvent('openim-shell-media-permission-result', { detail: $payload })); + } catch (_) {} +})(); +'''; + await _runJavaScriptSafely(lineIndex, script); + } + + Future _handleWebViewPermissionRequest( + WebViewPermissionRequest request, + ) async { + final requestsOnlySupportedMediaTypes = request.types.every( + (type) => + type == WebViewPermissionResourceType.camera || + type == WebViewPermissionResourceType.microphone, + ); + if (!requestsOnlySupportedMediaTypes) { + await request.deny(); + return; + } + + final result = await _requestNativeMediaPermissions( + audio: request.types.contains(WebViewPermissionResourceType.microphone), + video: request.types.contains(WebViewPermissionResourceType.camera), + ); + + if (result.granted) { + await request.grant(); + } else { + await request.deny(); + } + } + + Future<_NativeMediaPermissionResult> _requestNativeMediaPermissions({ + required bool audio, + required bool video, + }) async { + final permissions = [ + if (audio) Permission.microphone, + if (video) Permission.camera, + ]; + + if (permissions.isEmpty) { + return const _NativeMediaPermissionResult(granted: true); + } + + var granted = true; + var permanentlyDenied = false; + var restricted = false; + + for (final permission in permissions) { + final status = await permission.request(); + granted = granted && status.isGranted; + permanentlyDenied = permanentlyDenied || status.isPermanentlyDenied; + restricted = restricted || status.isRestricted; + } + + return _NativeMediaPermissionResult( + granted: granted, + permanentlyDenied: permanentlyDenied, + restricted: restricted, + ); + } + void _updateSlotUrl(int lineIndex, String? url) { if (url == null || lineIndex < 0 || lineIndex >= _lineSlots.length) { return; @@ -499,6 +620,18 @@ class _H5ShellPageState extends State { } } +class _NativeMediaPermissionResult { + const _NativeMediaPermissionResult({ + required this.granted, + this.permanentlyDenied = false, + this.restricted = false, + }); + + final bool granted; + final bool permanentlyDenied; + final bool restricted; +} + class _H5LineWebViewSlot { _H5LineWebViewSlot({ required this.line,