feat: 移除多余的媒体权限处理逻辑和相关代码;优化 WebView 控制器创建
This commit is contained in:
277
lib/main.dart
277
lib/main.dart
@@ -3,11 +3,8 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.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';
|
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'''
|
const _stopWebMediaScript = r'''
|
||||||
(() => {
|
(() => {
|
||||||
try {
|
try {
|
||||||
@@ -279,30 +182,8 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WebViewController _buildController() {
|
WebViewController _buildController() {
|
||||||
PlatformWebViewControllerCreationParams params =
|
return WebViewController()
|
||||||
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));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
..addJavaScriptChannel(
|
|
||||||
'OpenIMFlutterShell',
|
|
||||||
onMessageReceived: _handleShellBridgeMessage,
|
|
||||||
)
|
|
||||||
..setBackgroundColor(_shellBackground)
|
..setBackgroundColor(_shellBackground)
|
||||||
..setNavigationDelegate(
|
..setNavigationDelegate(
|
||||||
NavigationDelegate(
|
NavigationDelegate(
|
||||||
@@ -339,90 +220,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
onNavigationRequest: _handleNavigationRequest,
|
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() {
|
void _showTransientShellCover() {
|
||||||
@@ -451,7 +248,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
Future<void> _handlePageFinished() async {
|
Future<void> _handlePageFinished() async {
|
||||||
await _syncShellBranding();
|
await _syncShellBranding();
|
||||||
unawaited(_injectShellMediaPermissionBridge());
|
|
||||||
unawaited(_logLoadedH5Snapshot());
|
unawaited(_logLoadedH5Snapshot());
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -478,10 +274,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
return _runJavaScriptSafely(script);
|
return _runJavaScriptSafely(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _injectShellMediaPermissionBridge() {
|
|
||||||
return _runJavaScriptSafely(_shellMediaPermissionBridgeScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _stopWebMedia() {
|
Future<void> _stopWebMedia() {
|
||||||
return _runJavaScriptSafely(_stopWebMediaScript);
|
return _runJavaScriptSafely(_stopWebMediaScript);
|
||||||
}
|
}
|
||||||
@@ -556,73 +348,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
return NavigationDecision.prevent;
|
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 {
|
Future<void> _handleBackNavigation() async {
|
||||||
await _stopWebMedia();
|
await _stopWebMedia();
|
||||||
if (await _controller.canGoBack()) {
|
if (await _controller.canGoBack()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user