网络知识 娱乐 MAUI Blazor 权限经验分享 (定位,使用相机)

MAUI Blazor 权限经验分享 (定位,使用相机)

入门文章

Blazor Hybrid / MAUI 简介和实战
https://www.cnblogs.com/densen2014/p/16240966.html

在 Mac 上开发 .NET MAUI
https://www.cnblogs.com/densen2014/p/16057571.html

在 Windows 上开发 .NET MAUI
https://docs.microsoft.com/zh-cn/dotnet/maui/get-started/installation

之前的工程已经能正常使用blazor的webview下获取定位,使用相机等功能,新版释出后反而权限获取不到了,定位页面出现如下错误

由于这个问题主要出现在安卓系统,下面只选了安卓的步骤分享

Android

应用所需的权限和功能在 AndroidManifest.xml 中定义。请参阅 官方文档 了解 Android App Manifest。

某些 Android 设备权限需要在运行时显示提示,以便用户可以授予或拒绝该权限。 Android 有一个推荐的 workflow 用于在运行时请求权限,此工作流必须由应用手动实现。 WebView 的 WebChromeClient 负责对权限请求做出反应,因此该项目提供了一个 PermissionManagingBlazorWebChromeClient 将 Webkit 资源映射到 Android 权限并执行推荐的权限请求工作流。

在向 AndroidManifest.xml 添加其他权限后,请务必更新 PermissionManagingBlazorWebChromeClient.cs 以包含该权限的“基本原理字符串”,解释应用程序需要它的原因。可能还需要在 权限请求类型 和 Android Manifest 权限之间定义其他映射。

1. 应用所需的权限Platforms/Android/AndroidManifest.xml

以下是我所有的测试权限列表,各位看官按需自由组合.

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-feature android:name="android.hardware.location" android:required="false" /> <uses-feature android:name="android.hardware.location.gps" android:required="false" /> <uses-feature android:name="android.hardware.location.network" android:required="false" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.ModifyAudioSettings" /> <uses-permission android:name="android.permission.FLASHLIGHT" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" /> <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" /> <uses-permission android:name="android.permission.CaptureSecureVideoOutput" /> <uses-permission android:name="android.permission.CaptureVideoOutput" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.BATTERY_STATS" /> <queries> <intent> <action android:name="android.media.action.IMAGE_CAPTURE" /> </intent> </queries></manifest>

2. 添加文件Platforms/Android/PermissionManagingBlazorWebChromeClient.cs

using Android;using Android.App;using Android.Content.PM;using Android.Graphics;using Android.OS;using Android.Views;using Android.Webkit;using AndroidX.Activity;using AndroidX.Activity.Result;using AndroidX.Activity.Result.Contract;using AndroidX.Core.Content;using Java.Interop;using System;using System.Collections.Generic;using View = Android.Views.View;using WebView = Android.Webkit.WebView;namespace BlazorMaui;internal class PermissionManagingBlazorWebChromeClient : WebChromeClient, IActivityResultCallback{ // This class implements a permission requesting workflow that matches workflow recommended // by the official Android developer documentation. // See: https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions // The current implementation supports location, camera, and microphone permissions. To add your own, // update the s_rationalesByPermission dictionary to include your rationale for requiring the permission. // If necessary, you may need to also update s_requiredPermissionsByWebkitResource to define how a specific // Webkit resource maps to an Android permission. // In a real app, you would probably use more convincing rationales tailored toward what your app does. private const string CameraAccessRationale = "This app requires access to your camera. Please grant access to your camera when requested."; private const string LocationAccessRationale = "This app requires access to your location. Please grant access to your precise location when requested."; private const string MicrophoneAccessRationale = "This app requires access to your microphone. Please grant access to your microphone when requested."; private static readonly Dictionary<string, string> s_rationalesByPermission = new() { [Manifest.Permission.Camera] = CameraAccessRationale, [Manifest.Permission.AccessFineLocation] = LocationAccessRationale, [Manifest.Permission.RecordAudio] = MicrophoneAccessRationale, // Add more rationales as you add more supported permissions. }; private static readonly Dictionary<string, string[]> s_requiredPermissionsByWebkitResource = new() { [PermissionRequest.ResourceVideoCapture] = new[] { Manifest.Permission.Camera }, [PermissionRequest.ResourceAudioCapture] = new[] { Manifest.Permission.ModifyAudioSettings, Manifest.Permission.RecordAudio }, // Add more Webkit resource -> Android permission mappings as needed. }; private readonly WebChromeClient _blazorWebChromeClient; private readonly ComponentActivity _activity; private readonly ActivityResultLauncher _requestPermissionLauncher; private Action<bool>? _pendingPermissionRequestCallback; public PermissionManagingBlazorWebChromeClient(WebChromeClient blazorWebChromeClient, ComponentActivity activity) { _blazorWebChromeClient = blazorWebChromeClient; _activity = activity; _requestPermissionLauncher = _activity.RegisterForActivityResult(new ActivityResultContracts.RequestPermission(), this); } public override void OnCloseWindow(Android.Webkit.WebView? window) { _blazorWebChromeClient.OnCloseWindow(window); _requestPermissionLauncher.Unregister(); } public override void OnGeolocationPermissionsShowPrompt(string? origin, GeolocationPermissions.ICallback? callback) { ArgumentNullException.ThrowIfNull(callback, nameof(callback)); RequestPermission(Manifest.Permission.AccessFineLocation, isGranted => callback.Invoke(origin, isGranted, false)); } public override void OnPermissionRequest(PermissionRequest? request) { ArgumentNullException.ThrowIfNull(request, nameof(request)); if (request.GetResources() is not { } requestedResources) { request.Deny(); return; } RequestAllResources(requestedResources, grantedResources => { if (grantedResources.Count == 0) { request.Deny(); } else { request.Grant(grantedResources.ToArray()); } }); } private void RequestAllResources(Memory<string> requestedResources, Action<List<string>> callback) { if (requestedResources.Length == 0) { // No resources to request - invoke the callback with an empty list. callback(new()); return; } var currentResource = requestedResources.Span[0]; var requiredPermissions = s_requiredPermissionsByWebkitResource.GetValueOrDefault(currentResource, Array.Empty<string>()); RequestAllPermissions(requiredPermissions, isGranted => { // Recurse with the remaining resources. If the first resource was granted, use a modified callback // that adds the first resource to the granted resources list. RequestAllResources(requestedResources[1..], !isGranted ? callback : grantedResources => { grantedResources.Add(currentResource); callback(grantedResources); }); }); } private void RequestAllPermissions(Memory<string> requiredPermissions, Action<bool> callback) { if (requiredPermissions.Length == 0) { // No permissions left to request - success! callback(true); return; } RequestPermission(requiredPermissions.Span[0], isGranted => { if (isGranted) { // Recurse with the remaining permissions. RequestAllPermissions(requiredPermissions[1..], callback); } else { // The first required permission was not granted. Fail now and don't attempt to grant // the remaining permissions. callback(false); } }); } private void RequestPermission(string permission, Action<bool> callback) { // This method implements the workflow described here: // https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions if (ContextCompat.CheckSelfPermission(_activity, permission) == Permission.Granted) { callback.Invoke(true); } else if (_activity.ShouldShowRequestPermissionRationale(permission) && s_rationalesByPermission.TryGetValue(permission, out var rationale)) { new AlertDialog.Builder(_activity) .SetTitle("Enable app permissions")! .SetMessage(rationale)! .SetNegativeButton("No thanks", (_, _) => callback(false))! .SetPositiveButton("Continue", (_, _) => LaunchPermissionRequestActivity(permission, callback))! .Show(); } else { LaunchPermissionRequestActivity(permission, callback); } } private void LaunchPermissionRequestActivity(string permission, Action<bool> callback) { if (_pendingPermissionRequestCallback is not null) { throw new InvalidOperationException("Cannot perform multiple permission requests simultaneously."); } _pendingPermissionRequestCallback = callback; _requestPermissionLauncher.Launch(permission); } void IActivityResultCallback.OnActivityResult(Java.Lang.Object isGranted) { var callback = _pendingPermissionRequestCallback; _pendingPermissionRequestCallback = null; callback?.Invoke((bool)isGranted); } #region Unremarkable overrides // See: https://github.com/dotnet/maui/issues/6565 public override JniPeerMembers JniPeerMembers => _blazorWebChromeClient.JniPeerMembers; public override Bitmap? DefaultVideoPoster => _blazorWebChromeClient.DefaultVideoPoster; public override Android.Views.View? VideoLoadingProgressView => _blazorWebChromeClient.VideoLoadingProgressView; public override void GetVisitedHistory(IValueCallback? callback) => _blazorWebChromeClient.GetVisitedHistory(callback); public override bool OnConsoleMessage(ConsoleMessage? consoleMessage) => _blazorWebChromeClient.OnConsoleMessage(consoleMessage); public override bool OnCreateWindow(WebView? view, bool isDialog, bool isUserGesture, Message? resultMsg) => _blazorWebChromeClient.OnCreateWindow(view, isDialog, isUserGesture, resultMsg); public override void OnGeolocationPermissionsHidePrompt() => _blazorWebChromeClient.OnGeolocationPermissionsHidePrompt(); public override void OnHideCustomView() => _blazorWebChromeClient.OnHideCustomView(); public override bool OnJsAlert(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsAlert(view, url, message, result); public override bool OnJsBeforeUnload(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsBeforeUnload(view, url, message, result); public override bool OnJsConfirm(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsConfirm(view, url, message, result); public override bool OnJsPrompt(WebView? view, string? url, string? message, string? defaultValue, JsPromptResult? result) => _blazorWebChromeClient.OnJsPrompt(view, url, message, defaultValue, result); public override void OnPermissionRequestCanceled(PermissionRequest? request) => _blazorWebChromeClient.OnPermissionRequestCanceled(request); public override void OnProgressChanged(WebView? view, int newProgress) => _blazorWebChromeClient.OnProgressChanged(view, newProgress); public override void OnReceivedIcon(WebView? view, Bitmap? icon) => _blazorWebChromeClient.OnReceivedIcon(view, icon); public override void OnReceivedTitle(WebView? view, string? title) => _blazorWebChromeClient.OnReceivedTitle(view, title); public override void OnReceivedTouchIconUrl(WebView? view, string? url, bool precomposed) => _blazorWebChromeClient.OnReceivedTouchIconUrl(view, url, precomposed); public override void OnRequestFocus(WebView? view) => _blazorWebChromeClient.OnRequestFocus(view); public override void OnShowCustomView(View? view, ICustomViewCallback? callback) => _blazorWebChromeClient.OnShowCustomView(view, callback); public override bool OnShowFileChooser(WebView? webView, IValueCallback? filePathCallback, FileChooserParams? fileChooserParams) => _blazorWebChromeClient.OnShowFileChooser(webView, filePathCallback, fileChooserParams); #endregion}

3. 文件MainPage.xaml

添加 x:Name="_blazorWebView"

<BlazorWebView x:Name="_blazorWebView" HostPage="wwwroot/index.html"> <BlazorWebView.RootComponents> <RootComponent Selector="#app" ComponentType="{x:Type shared:App}" /> </BlazorWebView.RootComponents> </BlazorWebView>

4. 文件MainPage.xaml.cs

添加
_blazorWebView.BlazorWebViewInitialized += BlazorWebViewInitialized; _blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing;

完整代码:

using LibraryShared;using Microsoft.AspNetCore.Components.WebView;using Microsoft.Maui.Controls;using Microsoft.Maui.Platform;using System;using static Microsoft.Maui.ApplicationModel.Permissions;#if ANDROIDusing AndroidX.Activity;#endifnamespace BlazorMaui{ public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); _blazorWebView.BlazorWebViewInitialized += BlazorWebViewInitialized; _blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing; } private void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e) {#if ANDROID if (e.WebView.Context?.GetActivity() is not ComponentActivity activity) { throw new InvalidOperationException($"The permission-managing WebChromeClient requires that the current activity be a '{nameof(ComponentActivity)}'."); } e.WebView.Settings.JavaScriptEnabled = true; e.WebView.Settings.AllowFileAccess = true; e.WebView.Settings.MediaPlaybackRequiresUserGesture = false; e.WebView.Settings.SetGeolocationEnabled(true); e.WebView.Settings.SetGeolocationDatabasePath(e.WebView.Context?.FilesDir?.Path); e.WebView.SetWebChromeClient(new PermissionManagingBlazorWebChromeClient(e.WebView.WebChromeClient!, activity));#endif } private void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e) {#if IOS || MACCATALYST e.Configuration.AllowsInlineMediaPlayback = true; e.Configuration.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;#endif } }}

4. 其他更改

由于工程是一个共享库给多端用,先定义了一个接口用于注入服务到页面调用演示功能

public interface ITools { Task<string> CheckPermissionsCamera(); Task<string> TakePhoto(); Task<string> CheckPermissionsLocation(); Task<string> GetCachedLocation(); Task<string> GetCurrentLocation(); Task<string> CheckMock(); double DistanceBetweenTwoLocations(); void ShowSettingsUI(); string GetAppInfo(); }

调用MAUI的API功能 BlazorMaui/Services/TestService.cs

#if WINDOWSusing Windows.Storage;#endif#if ANDROIDusing Android.Webkit;#endifusing BlazorShared.Services;using System.Security.Permissions;namespace LibraryShared{ public class TestService : ITools { public string GetAppInfo() { //读取应用信息 string name = AppInfo.Current.Name; string package = AppInfo.Current.PackageName; string version = AppInfo.Current.VersionString; string build = AppInfo.Current.BuildString; return $"{name},{version}.{build}"; } public void ShowSettingsUI() { //显示应用设置 AppInfo.Current.ShowSettingsUI(); } public async Task<string> CheckPermissionsCamera() { //检查权限的当前状态 PermissionStatus status = await Permissions.CheckStatusAsync<Permissions.Camera>(); //请求权限 if (status != PermissionStatus.Granted) { status = await Permissions.RequestAsync<Permissions.Camera>(); } return status.ToString(); } public async Task<string> CheckPermissionsLocation() { //检查权限的当前状态 PermissionStatus status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>(); //请求权限 if (status != PermissionStatus.Granted) { status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>(); } return status.ToString(); } /// <summary> /// 拍照 /// CapturePhotoAsync调用该方法以打开相机,让用户拍照。 如果用户拍照,该方法的返回值将是非 null 值。 /// 以下代码示例使用媒体选取器拍摄照片并将其保存到缓存目录: /// </summary> public async Task<string> TakePhoto() { await CheckPermissionsCamera(); if (MediaPicker.Default.IsCaptureSupported) { FileResult photo = await MediaPicker.Default.CapturePhotoAsync(); if (photo != null) { // save the file into local storage string localFilePath = Path.Combine(FileSystem.CacheDirectory, photo.FileName); using Stream sourceStream = await photo.OpenReadAsync(); using FileStream localFileStream = File.OpenWrite(localFilePath); await sourceStream.CopyToAsync(localFileStream); return localFilePath; } return "photo null"; } return null; } /// <summary> /// 获取最后一个已知位置, 设备可能已缓存设备的最新位置。 /// 使用此方法 GetLastKnownLocationAsync 访问缓存的位置(如果可用)。 /// 这通常比执行完整位置查询更快,但可能不太准确。 /// 如果不存在缓存位置,此方法将 null返回 。 /// </summary> /// <returns></returns> public async Task<string> GetCachedLocation() { await CheckPermissionsLocation(); string result = null; try { Location location = await Geolocation.Default.GetLastKnownLocationAsync(); if (location != null) { result = $"Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}"; Console.WriteLine(result); return result; } } catch (FeatureNotSupportedException fnsEx) { // Handle not supported on device exception result = $"not supported on device, {fnsEx.Message}"; } catch (FeatureNotEnabledException fneEx) { // Handle not enabled on device exception result = $"not enabled on device, {fneEx.Message}"; } catch (PermissionException pEx) { // Handle permission exception result = $"permission, {pEx.Message}"; } catch (Exception ex) { // Unable to get location result = $"Unable to get location, {ex.Message}"; } return result ?? "None"; } private CancellationTokenSource _cancelTokenSource; private bool _isCheckingLocation; /// <summary> /// 获取当前位置 /// 虽然检查设备 的最后已知位置 可能更快,但它可能不准确。 /// 使用该方法 GetLocationAsync 查询设备的当前位置。 /// 可以配置查询的准确性和超时。 /// 最好是使用 GeolocationRequest 和 CancellationToken 参数的方法重载, /// 因为可能需要一些时间才能获取设备的位置。 /// </summary> /// <returns></returns> public async Task<string> GetCurrentLocation() { await CheckPermissionsLocation(); string result = null; try { _isCheckingLocation = true; GeolocationRequest request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10)); _cancelTokenSource = new CancellationTokenSource();#if IOS //从 iOS 14 开始,用户可能会限制应用检测完全准确的位置。 //该 Location.ReducedAccuracy 属性指示位置是否使用降低的准确性。 //若要请求完全准确性,请将 GeolocationRequest.RequestFullAccuracy 属性设置为 true request.RequestFullAccuracy = true;#endif Location location = await Geolocation.Default.GetLocationAsync(request, _cancelTokenSource.Token); if (location != null) { result = $"Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}"; Console.WriteLine(result); return result; } } catch (FeatureNotSupportedException fnsEx) { // Handle not supported on device exception result = $"not supported on device, {fnsEx.Message}"; } catch (FeatureNotEnabledException fneEx) { // Handle not enabled on device exception result = $"not enabled on device, {fneEx.Message}"; } catch (PermissionException pEx) { // Handle permission exception result = $"permission, {pEx.Message}"; } catch (Exception ex) { // Unable to get location result = $"Unable to get location, {ex.Message}"; } finally { _isCheckingLocation = false; } return result ?? "None"; } }}

MauiProgram.cs文件注入

builder.Services.AddSingleton<ITools, TestService>();

razor

<Button Text="定位权限" OnClick="检查定位权限" /> <span>@定位权限</span><br/><br/> <Button Text="摄像机权限" OnClick="检查摄像机权限" /> <span>@摄像机权限</span><br /><br /> <Button Text="定位" OnClick="获取定位" /> <span>@Locations</span><br /><br /> <Button Text="TakePhoto" OnClick="TakePhoto" /> <span>@PhotoFilename</span><br /><br /> <Button Text="ShowSettings" OnClick="ShowSettingsUI" /> <span>@version</span><br /><br />@code{ [Inject] protected ITools Tools { get; set; } private string Locations; private string PhotoFilename; private string version; private string 定位权限; private string 摄像机权限; async Task 获取定位() => Locations = await Tools.GetCurrentLocation(); async Task TakePhoto() => PhotoFilename = await Tools.TakePhoto(); async Task 检查定位权限() => 定位权限 = await Tools.CheckPermissionsLocation(); async Task 检查摄像机权限() => 摄像机权限 = await Tools.CheckPermissionsCamera(); void ShowSettingsUI() => Tools.ShowSettingsUI();}

最终效果


项目地址

https://github.com/densen2014/BlazorMaui

https://gitee.com/densen2014/BlazorMaui

参考资料

Permissions
https://docs.microsoft.com/en-us/dotnet/maui/platform-integration/appmodel/permissions?tabs=android

Geolocation
https://docs.microsoft.com/en-us/dotnet/maui/platform-integration/device/geolocation?tabs=windows

MauiBlazorPermissionsExample
https://github.com/MackinnonBuck/MauiBlazorPermissionsExample

关联项目

FreeSql

BA & Blazor

知识共享许可协议

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名AlexChow(包含链接: https://github.com/densen2014 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。