From b8159e3e35276e7843f29093fd2648d7f3158c85 Mon Sep 17 00:00:00 2001 From: Booker Date: Tue, 26 May 2026 20:28:11 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E7=9A=84=E5=AA=92=E4=BD=93=E6=9D=83=E9=99=90=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=92=8C=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=9B=E4=BC=98=E5=8C=96=20WebView=20=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=99=A8=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 277 +------------------------------------------------- 1 file changed, 1 insertion(+), 276 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 183290d..e5da3a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,11 +3,8 @@ 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'; -import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import 'config/app_config.dart'; @@ -56,100 +53,6 @@ const _inspectH5SnapshotScript = r''' }); })(); '''; -const _shellMediaPermissionBridgeScript = r''' -(() => { - if (window.__openimShellMediaBridgeInstalled) { - return; - } - - const channel = window.OpenIMFlutterShell; - const mediaDevices = navigator.mediaDevices; - if (!channel || typeof channel.postMessage !== 'function') { - return; - } - if (!mediaDevices || typeof mediaDevices.getUserMedia !== 'function') { - return; - } - - const originalGetUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); - const resultEventName = 'openim-shell-media-permission-result'; - - const createRequestId = () => { - if (window.crypto && typeof window.crypto.randomUUID === 'function') { - return window.crypto.randomUUID(); - } - return `${Date.now()}-${Math.random().toString(36).slice(2)}`; - }; - - const createPermissionError = () => { - try { - return new DOMException('Permission denied', 'NotAllowedError'); - } catch (_) { - const error = new Error('Permission denied'); - error.name = 'NotAllowedError'; - return error; - } - }; - - const requestNativePermissions = (constraints) => - new Promise((resolve) => { - const requestId = createRequestId(); - let timer; - - const cleanup = () => { - if (timer) { - window.clearTimeout(timer); - } - window.removeEventListener(resultEventName, listener); - }; - - const listener = (event) => { - const detail = event.detail; - if (!detail || detail.requestId !== requestId) { - return; - } - cleanup(); - resolve(detail); - }; - - timer = window.setTimeout(() => { - cleanup(); - resolve(undefined); - }, 30000); - - window.addEventListener(resultEventName, listener); - channel.postMessage( - JSON.stringify({ - type: 'requestMediaPermissions', - requestId, - audio: Boolean(constraints && constraints.audio), - video: Boolean(constraints && constraints.video), - }), - ); - }); - - const wrappedGetUserMedia = async (constraints) => { - const result = await requestNativePermissions(constraints || {}); - if (result && result.granted) { - return originalGetUserMedia(constraints); - } - throw createPermissionError(); - }; - - try { - Object.defineProperty(mediaDevices, 'getUserMedia', { - configurable: true, - writable: true, - value: wrappedGetUserMedia, - }); - } catch (_) { - mediaDevices.getUserMedia = wrappedGetUserMedia; - } - - window.__openimShellMediaBridgeInstalled = true; - window.dispatchEvent(new CustomEvent('openim-shell-media-bridge-ready')); -})(); -'''; const _stopWebMediaScript = r''' (() => { try { @@ -279,30 +182,8 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { } WebViewController _buildController() { - PlatformWebViewControllerCreationParams params = - const PlatformWebViewControllerCreationParams(); - - if (WebViewPlatform.instance is WebKitWebViewPlatform) { - params = WebKitWebViewControllerCreationParams( - allowsInlineMediaPlayback: true, - mediaTypesRequiringUserAction: const {}, - ); - } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { - params = AndroidWebViewControllerCreationParams - .fromPlatformWebViewControllerCreationParams(params); - } - - final controller = WebViewController.fromPlatformCreationParams( - params, - onPermissionRequest: (request) { - unawaited(_handleWebViewPermissionRequest(request)); - }, - ) + return WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..addJavaScriptChannel( - 'OpenIMFlutterShell', - onMessageReceived: _handleShellBridgeMessage, - ) ..setBackgroundColor(_shellBackground) ..setNavigationDelegate( NavigationDelegate( @@ -339,90 +220,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { onNavigationRequest: _handleNavigationRequest, ), ); - - final platformController = controller.platform; - if (platformController is AndroidWebViewController) { - assert(() { - AndroidWebViewController.enableDebugging(true); - return true; - }()); - unawaited(platformController.setMediaPlaybackRequiresUserGesture(false)); - unawaited(platformController.setGeolocationEnabled(true)); - unawaited( - platformController.setGeolocationPermissionsPromptCallbacks( - onShowPrompt: (_) async { - final allowed = - await _requestPermission(Permission.locationWhenInUse); - return GeolocationPermissionsResponse( - allow: allowed, - retain: allowed, - ); - }, - ), - ); - } - - return controller; - } - - void _handleShellBridgeMessage(JavaScriptMessage message) { - Map? payload; - String? type; - try { - final decoded = jsonDecode(message.message); - if (decoded is Map) { - payload = Map.from(decoded); - type = payload['type'] as String?; - } - } catch (_) { - type = message.message; - } - - if (type == 'openAppSettings') { - unawaited(openAppSettings()); - } - if (type == 'requestMediaPermissions' && payload != null) { - unawaited(_handleMediaPermissionBridgeRequest(payload)); - } - } - - Future _handleMediaPermissionBridgeRequest( - Map payload, - ) async { - final requestId = payload['requestId'] as String?; - if (requestId == null || requestId.isEmpty) { - return; - } - - final permissions = {}; - if (payload['audio'] == true) { - permissions.add(Permission.microphone); - } - if (payload['video'] == true) { - permissions.add(Permission.camera); - } - - final statusesByPermission = await _requestPermissions(permissions); - final statuses = statusesByPermission.values; - final granted = statuses.every( - _isPermissionAllowed, - ); - final permanentlyDenied = - statuses.any((status) => status.isPermanentlyDenied); - final restricted = statuses.any((status) => status.isRestricted); - - final detail = jsonEncode({ - 'requestId': requestId, - 'granted': granted, - 'permanentlyDenied': permanentlyDenied, - 'restricted': restricted, - }); - final script = ''' -(() => { - window.dispatchEvent(new CustomEvent('openim-shell-media-permission-result', { detail: $detail })); -})(); -'''; - await _runJavaScriptSafely(script); } void _showTransientShellCover() { @@ -451,7 +248,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { Future _handlePageFinished() async { await _syncShellBranding(); - unawaited(_injectShellMediaPermissionBridge()); unawaited(_logLoadedH5Snapshot()); if (mounted) { setState(() { @@ -478,10 +274,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { return _runJavaScriptSafely(script); } - Future _injectShellMediaPermissionBridge() { - return _runJavaScriptSafely(_shellMediaPermissionBridgeScript); - } - Future _stopWebMedia() { return _runJavaScriptSafely(_stopWebMediaScript); } @@ -556,73 +348,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { return NavigationDecision.prevent; } - Future _handleWebViewPermissionRequest( - WebViewPermissionRequest request, - ) async { - final permissions = {}; - if (request.types.contains(WebViewPermissionResourceType.camera)) { - permissions.add(Permission.camera); - } - if (request.types.contains(WebViewPermissionResourceType.microphone)) { - permissions.add(Permission.microphone); - } - - debugPrint( - '[H5Shell] WebView media permission request: ' - '${request.types.map((type) => type.name).join(', ')}', - ); - - final statusesByPermission = await _requestPermissions(permissions); - final allowed = statusesByPermission.values.every(_isPermissionAllowed); - debugPrint( - '[H5Shell] WebView media permission result: ' - '${statusesByPermission.map((permission, status) => MapEntry(permission.toString(), status.toString()))}', - ); - - if (allowed) { - await request.grant(); - } else { - await request.deny(); - } - } - - bool _isPermissionAllowed(PermissionStatus status) { - return status.isGranted || status.isLimited; - } - - Future> _requestPermissions( - Set permissions, - ) async { - if (permissions.isEmpty) { - return const {}; - } - - final pending = []; - final statuses = {}; - - for (final permission in permissions) { - final status = await permission.status; - if (_isPermissionAllowed(status) || - status.isPermanentlyDenied || - status.isRestricted) { - statuses[permission] = status; - } else { - pending.add(permission); - } - } - - if (pending.isNotEmpty) { - statuses.addAll(await pending.request()); - } - - return statuses; - } - - Future _requestPermission(Permission permission) async { - final statuses = await _requestPermissions({permission}); - return statuses.values.every(_isPermissionAllowed); - } - Future _handleBackNavigation() async { await _stopWebMedia(); if (await _controller.canGoBack()) {