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下获取定位,使用相机等功能,新版释出后反而权限获取不到了,定位页面出现如下错误

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

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

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 ANDROID using AndroidX.Activity; #endif  namespace 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 WINDOWS using Windows.Storage; #endif #if ANDROID using Android.Webkit; #endif using 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(); } 

最终效果

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

项目地址

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 QQ群:4336577、8578575、52508226

BA & Blazor QQ群:795206915、675147445

知识共享许可协议

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

AlexChow

今日头条 | 博客园 | 知乎 | Gitee | GitHub

发表评论

评论已关闭。

相关文章