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"?>                                                                          

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 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 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? _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 requestedResources, Action> 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());        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 requiredPermissions, Action 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 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 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"

                                     

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(#34;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 CheckPermissionsCamera();        Task TakePhoto();        Task CheckPermissionsLocation();        Task GetCachedLocation();        Task GetCurrentLocation();        Task 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 #34;{name},{version}.{build}";        }        public void ShowSettingsUI()        {            //显示应用设置            AppInfo.Current.ShowSettingsUI();        }        public async Task CheckPermissionsCamera()        {            //检查权限的当前状态            PermissionStatus status = await Permissions.CheckStatusAsync();            //请求权限            if (status != PermissionStatus.Granted)            {                status = await Permissions.RequestAsync();            }            return status.ToString();        }        public async Task CheckPermissionsLocation()        {            //检查权限的当前状态            PermissionStatus status = await Permissions.CheckStatusAsync();            //请求权限            if (status != PermissionStatus.Granted)            {                status = await Permissions.RequestAsync();            }            return status.ToString();        }        ///         /// 拍照        /// CapturePhotoAsync调用该方法以打开相机,让用户拍照。 如果用户拍照,该方法的返回值将是非 null 值。        /// 以下代码示例使用媒体选取器拍摄照片并将其保存到缓存目录:        ///         public async Task 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;        }        ///         /// 获取最后一个已知位置, 设备可能已缓存设备的最新位置。        /// 使用此方法 GetLastKnownLocationAsync 访问缓存的位置(如果可用)。        /// 这通常比执行完整位置查询更快,但可能不太准确。        /// 如果不存在缓存位置,此方法将 null返回 。        ///         ///         public async Task GetCachedLocation()        {            await CheckPermissionsLocation();            string result = null;            try            {                Location location = await Geolocation.Default.GetLastKnownLocationAsync();                if (location != null)                {                    result = #34;Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}";                    Console.WriteLine(result);                    return result;                }            }            catch (FeatureNotSupportedException fnsEx)            {                // Handle not supported on device exception                result = #34;not supported on device, {fnsEx.Message}";            }            catch (FeatureNotEnabledException fneEx)            {                // Handle not enabled on device exception                result = #34;not enabled on device, {fneEx.Message}";            }            catch (PermissionException pEx)            {                // Handle permission exception                result = #34;permission, {pEx.Message}";            }            catch (Exception ex)            {                // Unable to get location                result = #34;Unable to get location, {ex.Message}";            }            return result ?? "None";        }        private CancellationTokenSource _cancelTokenSource;        private bool _isCheckingLocation;        ///         /// 获取当前位置        /// 虽然检查设备 的最后已知位置 可能更快,但它可能不准确。        /// 使用该方法 GetLocationAsync 查询设备的当前位置。        /// 可以配置查询的准确性和超时。        /// 最好是使用 GeolocationRequest 和 CancellationToken 参数的方法重载,        /// 因为可能需要一些时间才能获取设备的位置。        ///         ///         public async Task 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 = #34;Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}";                    Console.WriteLine(result);                    return result;                }            }            catch (FeatureNotSupportedException fnsEx)            {                // Handle not supported on device exception                result = #34;not supported on device, {fnsEx.Message}";            }            catch (FeatureNotEnabledException fneEx)            {                // Handle not enabled on device exception                result = #34;not enabled on device, {fneEx.Message}";            }            catch (PermissionException pEx)            {                // Handle permission exception                result = #34;permission, {pEx.Message}";            }            catch (Exception ex)            {                // Unable to get location                result = #34;Unable to get location, {ex.Message}";            }            finally            {                _isCheckingLocation = false;            }            return result ?? "None";        }    }}

MauiProgram.cs文件注入

builder.Services.AddSingleton();

razor

        

最终效果


项目地址

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 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章