You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

181 line
7.0 KiB

  1. using Android;
  2. using Android.App;
  3. using Android.Content.PM;
  4. using Android.Graphics;
  5. using Android.OS;
  6. using Android.Views;
  7. using Android.Webkit;
  8. using AndroidX.Activity;
  9. using AndroidX.Activity.Result;
  10. using AndroidX.Activity.Result.Contract;
  11. using AndroidX.Core.Content;
  12. using Java.Interop;
  13. using System;
  14. using System.Collections.Generic;
  15. namespace MauiBlazorPermissionsExample.Platforms.Android;
  16. internal class PermissionManagingBlazorWebChromeClient : WebChromeClient, IActivityResultCallback
  17. {
  18. // This class implements a permission requesting workflow that matches workflow recommended
  19. // by the official Android developer documentation.
  20. // See: https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions
  21. // The current implementation supports location, camera, and microphone permissions. To add your own,
  22. // update the s_rationalesByPermission dictionary to include your rationale for requiring the permission.
  23. // If necessary, you may need to also update s_requiredPermissionsByWebkitResource to define how a specific
  24. // Webkit resource maps to an Android permission.
  25. // In a real app, you would probably use more convincing rationales tailored toward what your app does.
  26. private const string CameraAccessRationale = "This app requires access to your camera. Please grant access to your camera when requested.";
  27. private const string LocationAccessRationale = "This app requires access to your location. Please grant access to your precise location when requested.";
  28. private const string MicrophoneAccessRationale = "This app requires access to your microphone. Please grant access to your microphone when requested.";
  29. private static readonly Dictionary<string, string> s_rationalesByPermission = new()
  30. {
  31. [Manifest.Permission.Camera] = CameraAccessRationale,
  32. [Manifest.Permission.AccessFineLocation] = LocationAccessRationale,
  33. [Manifest.Permission.RecordAudio] = MicrophoneAccessRationale,
  34. // Add more rationales as you add more supported permissions.
  35. };
  36. private static readonly Dictionary<string, string[]> s_requiredPermissionsByWebkitResource = new()
  37. {
  38. [PermissionRequest.ResourceVideoCapture] = new[] { Manifest.Permission.Camera },
  39. [PermissionRequest.ResourceAudioCapture] = new[] { Manifest.Permission.ModifyAudioSettings, Manifest.Permission.RecordAudio },
  40. // Add more Webkit resource -> Android permission mappings as needed.
  41. };
  42. private readonly WebChromeClient _blazorWebChromeClient;
  43. private readonly ComponentActivity _activity;
  44. private readonly ActivityResultLauncher _requestPermissionLauncher;
  45. private Action<bool>? _pendingPermissionRequestCallback;
  46. public PermissionManagingBlazorWebChromeClient(WebChromeClient blazorWebChromeClient, ComponentActivity activity)
  47. {
  48. _blazorWebChromeClient = blazorWebChromeClient;
  49. _activity = activity;
  50. _requestPermissionLauncher = _activity.RegisterForActivityResult(new ActivityResultContracts.RequestPermission(), this);
  51. }
  52. public override void OnPermissionRequest(PermissionRequest? request)
  53. {
  54. ArgumentNullException.ThrowIfNull(request, nameof(request));
  55. if (request.GetResources() is not { } requestedResources)
  56. {
  57. request.Deny();
  58. return;
  59. }
  60. RequestAllResources(requestedResources, grantedResources =>
  61. {
  62. if (grantedResources.Count == 0)
  63. {
  64. request.Deny();
  65. }
  66. else
  67. {
  68. request.Grant(grantedResources.ToArray());
  69. }
  70. });
  71. }
  72. private void RequestAllResources(Memory<string> requestedResources, Action<List<string>> callback)
  73. {
  74. if (requestedResources.Length == 0)
  75. {
  76. // No resources to request - invoke the callback with an empty list.
  77. callback(new());
  78. return;
  79. }
  80. var currentResource = requestedResources.Span[0];
  81. var requiredPermissions = s_requiredPermissionsByWebkitResource.GetValueOrDefault(currentResource, Array.Empty<string>());
  82. RequestAllPermissions(requiredPermissions, isGranted =>
  83. {
  84. // Recurse with the remaining resources. If the first resource was granted, use a modified callback
  85. // that adds the first resource to the granted resources list.
  86. RequestAllResources(requestedResources[1..], !isGranted ? callback : grantedResources =>
  87. {
  88. grantedResources.Add(currentResource);
  89. callback(grantedResources);
  90. });
  91. });
  92. }
  93. private void RequestAllPermissions(Memory<string> requiredPermissions, Action<bool> callback)
  94. {
  95. if (requiredPermissions.Length == 0)
  96. {
  97. // No permissions left to request - success!
  98. callback(true);
  99. return;
  100. }
  101. RequestPermission(requiredPermissions.Span[0], isGranted =>
  102. {
  103. if (isGranted)
  104. {
  105. // Recurse with the remaining permissions.
  106. RequestAllPermissions(requiredPermissions[1..], callback);
  107. }
  108. else
  109. {
  110. // The first required permission was not granted. Fail now and don't attempt to grant
  111. // the remaining permissions.
  112. callback(false);
  113. }
  114. });
  115. }
  116. private void RequestPermission(string permission, Action<bool> callback)
  117. {
  118. // This method implements the workflow described here:
  119. // https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions
  120. if (ContextCompat.CheckSelfPermission(_activity, permission) == Permission.Granted)
  121. {
  122. callback.Invoke(true);
  123. }
  124. else if (_activity.ShouldShowRequestPermissionRationale(permission) && s_rationalesByPermission.TryGetValue(permission, out var rationale))
  125. {
  126. new AlertDialog.Builder(_activity)
  127. .SetTitle("Enable app permissions")!
  128. .SetMessage(rationale)!
  129. .SetNegativeButton("No thanks", (_, _) => callback(false))!
  130. .SetPositiveButton("Continue", (_, _) => LaunchPermissionRequestActivity(permission, callback))!
  131. .Show();
  132. }
  133. else
  134. {
  135. LaunchPermissionRequestActivity(permission, callback);
  136. }
  137. }
  138. private void LaunchPermissionRequestActivity(string permission, Action<bool> callback)
  139. {
  140. if (_pendingPermissionRequestCallback is not null)
  141. {
  142. throw new InvalidOperationException("Cannot perform multiple permission requests simultaneously.");
  143. }
  144. _pendingPermissionRequestCallback = callback;
  145. _requestPermissionLauncher.Launch(permission);
  146. }
  147. void IActivityResultCallback.OnActivityResult(Java.Lang.Object isGranted)
  148. {
  149. var callback = _pendingPermissionRequestCallback;
  150. _pendingPermissionRequestCallback = null;
  151. callback?.Invoke((bool)isGranted);
  152. }
  153. }