feat: 移除多余的媒体权限处理逻辑和相关代码;优化 WebView 控制器创建

This commit is contained in:
Booker
2026-05-26 20:28:11 +07:00
parent 1e19f5f5f8
commit b8159e3e35

View File

@@ -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<H5ShellPage> with WidgetsBindingObserver {
}
WebViewController _buildController() {
PlatformWebViewControllerCreationParams params =
const PlatformWebViewControllerCreationParams();
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} 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<H5ShellPage> 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<String, dynamic>? payload;
String? type;
try {
final decoded = jsonDecode(message.message);
if (decoded is Map) {
payload = Map<String, dynamic>.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<void> _handleMediaPermissionBridgeRequest(
Map<String, dynamic> payload,
) async {
final requestId = payload['requestId'] as String?;
if (requestId == null || requestId.isEmpty) {
return;
}
final permissions = <Permission>{};
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<H5ShellPage> with WidgetsBindingObserver {
Future<void> _handlePageFinished() async {
await _syncShellBranding();
unawaited(_injectShellMediaPermissionBridge());
unawaited(_logLoadedH5Snapshot());
if (mounted) {
setState(() {
@@ -478,10 +274,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
return _runJavaScriptSafely(script);
}
Future<void> _injectShellMediaPermissionBridge() {
return _runJavaScriptSafely(_shellMediaPermissionBridgeScript);
}
Future<void> _stopWebMedia() {
return _runJavaScriptSafely(_stopWebMediaScript);
}
@@ -556,73 +348,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
return NavigationDecision.prevent;
}
Future<void> _handleWebViewPermissionRequest(
WebViewPermissionRequest request,
) async {
final permissions = <Permission>{};
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<Map<Permission, PermissionStatus>> _requestPermissions(
Set<Permission> permissions,
) async {
if (permissions.isEmpty) {
return const {};
}
final pending = <Permission>[];
final statuses = <Permission, PermissionStatus>{};
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<bool> _requestPermission(Permission permission) async {
final statuses = await _requestPermissions({permission});
return statuses.values.every(_isPermissionAllowed);
}
Future<void> _handleBackNavigation() async {
await _stopWebMedia();
if (await _controller.canGoBack()) {