From 962b2978d2c2248f249932eaca79e658476ded57 Mon Sep 17 00:00:00 2001 From: Piotr Szydelko Date: Fri, 3 Jul 2026 12:22:51 +0200 Subject: [PATCH] [Security.PrivacyPrivilegeManager] CheckPermissionsAsync(), RequestPermissionsAsync() --- ...en.Security.PrivacyPrivilegeManager.csproj | 1 + .../PermissionDeniedException.cs | 83 +++++++ .../Tizen.Security/PermissionResult.cs | 58 +++++ .../Tizen.Security/PermissionStatus.cs | 102 +++++++++ .../Tizen.Security/PrivacyPrivilegeManager.cs | 211 +++++++++++++++++- 5 files changed, 447 insertions(+), 8 deletions(-) create mode 100644 src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionDeniedException.cs create mode 100644 src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionResult.cs create mode 100644 src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionStatus.cs diff --git a/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security.PrivacyPrivilegeManager.csproj b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security.PrivacyPrivilegeManager.csproj index 8d0fdb53af9..e46ad76814a 100644 --- a/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security.PrivacyPrivilegeManager.csproj +++ b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security.PrivacyPrivilegeManager.csproj @@ -6,5 +6,6 @@ + diff --git a/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionDeniedException.cs b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionDeniedException.cs new file mode 100644 index 00000000000..c63ee6a3fd0 --- /dev/null +++ b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionDeniedException.cs @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Tizen.Security +{ + /// + /// Exception thrown when one or more required permissions are denied. + /// + /// 14 + /// + /// + /// + /// + /// + public class PermissionDeniedException : Exception + { + private readonly IDictionary _map; + + /// + /// Gets the list of denied privilege names. + /// + /// 14 + public IEnumerable DeniedPrivileges => _map.Keys; + + /// + /// Initializes a new instance of the PermissionDeniedException class with denied privileges and their statuses. + /// + /// A dictionary mapping denied privilege names to their statuses. + /// 14 + public PermissionDeniedException(IDictionary denied) + : base("Required permissions denied: " + string.Join(", ", denied.Keys)) + { + _map = new Dictionary(denied); + } + + /// + /// Gets the permission status for a specific denied privilege. + /// + /// The privilege name. + /// The permission status of the denied privilege. + /// Thrown when the privilege is not found in the denied list. + /// 14 + public PermissionStatus GetStatus(string privilege) + { + if (!_map.TryGetValue(privilege, out var status)) + { + throw new ArgumentException($"Privilege '{privilege}' is not in the denied list.", nameof(privilege)); + } + return status; + } + } +} diff --git a/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionResult.cs b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionResult.cs new file mode 100644 index 00000000000..32907bc1831 --- /dev/null +++ b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionResult.cs @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Tizen.Security +{ + /// + /// Represents the result of permission checks, implementing IDictionary<string, PermissionState>. + /// Provides methods to check if privileges are granted. + /// + /// 14 + /// + /// + /// + public class PermissionResult : Dictionary + { + /// + /// Checks if a specific privilege is granted. + /// Returns false for unknown privileges without throwing an exception. + /// + /// The privilege to check. + /// True if the privilege is granted; otherwise, false. + /// 14 + public bool IsGranted(string privilege) => + TryGetValue(privilege, out var status) && status.IsGranted(); + + /// + /// Checks if all privileges in this result are granted. + /// + /// True if all privileges are granted; otherwise, false. + /// 14 + public bool AreAllGranted() => Values.All(status => status.IsGranted()); + } +} diff --git a/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionStatus.cs b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionStatus.cs new file mode 100644 index 00000000000..56c5ae0d314 --- /dev/null +++ b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PermissionStatus.cs @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Tizen.Security +{ + /// + /// Enumeration for the status of a permission. + /// + /// 14 + public enum PermissionStatus + { + /// + /// The privilege is granted. + /// + Granted = 0, + /// + /// The privilege is denied. + /// + Denied = 1, + /// + /// The permission is denied until user grants it. Use RequestPermissionAsync() to ask the user for the permission. + /// + Ask = 2, + /// + /// The privilege is granted for the current session only. + /// + GrantedSession = 3, + /// + /// The privilege is denied for the current session only. + /// + DeniedSession = 4, + /// + /// The privilege is granted only while the application is in foreground. + /// + GrantedInUse = 5, + /// + /// The privilege is denied once - user did not allow the permission this time and chose to be asked again later. + /// + Deferred = 90, + } + + /// + /// Extension methods for PermissionState. + /// + /// 14 + public static class PermissionStateExtensions + { + /// + /// Checks if the permission state indicates that the privilege is granted. + /// + /// The permission state to check. + /// True if the privilege is granted (Granted, GrantedSession, or GrantedInUse); otherwise, false. + /// 14 + public static bool IsGranted(this PermissionStatus state) + { + return state == PermissionStatus.Granted || + state == PermissionStatus.GrantedSession || + state == PermissionStatus.GrantedInUse; + } + } + + /// + /// Internal helper class for converting between interop enums and PermissionStatus. + /// + internal static class PermissionStatusConverter + { + /// + /// Converts Interop.CheckResult to PermissionState. + /// + internal static PermissionStatus ToPermissionStatus(this Interop.PrivacyPrivilegeManager.CheckResult checkResult) + { + // Values match directly for all cases. + return (PermissionStatus)checkResult; + } + + /// + /// Converts RequestResult to PermissionState. + /// + internal static PermissionStatus ToPermissionStatus(this RequestResult requestResult) + { + // Value DenyOnce maps to Deferred (90). Others match directly. + if (requestResult == RequestResult.DenyOnce) + { + return PermissionStatus.Deferred; + } + return (PermissionStatus)requestResult; + } + } +} diff --git a/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PrivacyPrivilegeManager.cs b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PrivacyPrivilegeManager.cs index a541412e41d..bdbf26c2dd5 100644 --- a/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PrivacyPrivilegeManager.cs +++ b/src/Tizen.Security.PrivacyPrivilegeManager/Tizen.Security/PrivacyPrivilegeManager.cs @@ -19,13 +19,14 @@ using System.Linq; using System.Threading.Tasks; +#pragma warning disable 618 + namespace Tizen.Security { /// /// The PrivacyPrivilegeManager provides the properties or methods to check and request a permission for privacy privilege. /// /// 4 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] public static class PrivacyPrivilegeManager { private const string LogTag = "Tizen.Privilege"; @@ -113,7 +114,7 @@ private static string[] CheckPrivilegesArgument(IEnumerable privileges, /// /// /// 4 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] + [Obsolete("Deprecated in API11, will be removed in API13. Use CheckPermissionsAsync() instead.")] public static CheckResult CheckPermission(string privilege) { Interop.PrivacyPrivilegeManager.CheckResult result; @@ -160,7 +161,7 @@ public static CheckResult CheckPermission(string privilege) /// /// /// 6 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] + [Obsolete("Deprecated in API11, will be removed in API13. Use CheckPermissionsAsync() instead.")] public static IEnumerable CheckPermissions(IEnumerable privileges) { string[] privilegesArray = CheckPrivilegesArgument(privileges, "CheckPermissions"); @@ -208,7 +209,7 @@ public static IEnumerable CheckPermissions(IEnumerable priv /// /// /// 4 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] + [Obsolete("Deprecated in API11, will be removed in API13. Use RequestPermissionsAsync() instead.")] public static void RequestPermission(string privilege) { if (!s_PrivilegesInProgress.Add(privilege)) @@ -261,7 +262,7 @@ public static void RequestPermission(string privilege) /// /// /// 6 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] + [Obsolete("Deprecated in API11, will be removed in API13. Use RequestPermissionsAsync() instead.")] public static Task RequestPermissions(IEnumerable privileges) { string[] privilegesArray = CheckPrivilegesArgument(privileges, "RequestPermissions"); @@ -360,7 +361,7 @@ public static Task RequestPermissions(IEnumera /// /// /// 4 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] + [Obsolete("Deprecated in API11, will be removed in API13. Use RequestPermissionsAsync() which handles context internally.")] public static WeakReference GetResponseContext(string privilege) { if (!(s_responseWeakMap.TryGetValue(privilege, out WeakReference weakRef) && weakRef.TryGetTarget(out ResponseContext context))) @@ -403,12 +404,206 @@ private static void MultipleRequestHandler(Interop.PrivacyPrivilegeManager.CallC s_multipleRequestMap.Remove(requestId); } + /// + /// Gets the status of privacy privileges permission asynchronously. + /// + /// The privacy privileges to be checked. + /// A task that returns a permission status of the requested privileges. + /// Thrown when an invalid parameter is passed. + /// Thrown when a memory error occurred. + /// Thrown when the method failed due to an internal I/O error. + /// + /// + /// + /// + /// + /// + /// Check if all privileges from manifest are granted. + /// + /// manifestPrivileges = await PrivacyPrivilegeManager.LoadPrivilegesFromManifestAsync(); + /// PermissionResult states = await PrivacyPrivilegeManager.CheckPermissionsAsync(manifestPrivileges); + /// bool allGranted = states.AllGranted(); + /// if (allGranted) + /// { + /// Console.WriteLine("All permissions are granted."); + /// } + /// ]]> + /// + /// + /// 14 + public static async Task CheckPermissionsAsync(IEnumerable privileges) + { + string[] privilegesArray = CheckPrivilegesArgument(privileges, "CheckPermissionsAsync"); + + return await Task.Run(() => + { + Interop.PrivacyPrivilegeManager.CheckResult[] results = new Interop.PrivacyPrivilegeManager.CheckResult[privilegesArray.Length]; + int ret = (int)Interop.PrivacyPrivilegeManager.CheckPermissions(privilegesArray, (uint)privilegesArray.Length, results); + if (ret != (int)Interop.PrivacyPrivilegeManager.ErrorCode.None) + { + Log.Error(LogTag, "Failed to check permissions " + string.Join(", ", privileges)); + throw PrivacyPrivilegeManagerErrorFactory.GetException(ret); + } + + var permissionStates = new PermissionResult(); + for (int iterator = 0; iterator < results.Length; ++iterator) + { + permissionStates[privilegesArray[iterator]] = results[iterator].ToPermissionStatus(); + } + return permissionStates; + }); + } + + /// + /// Triggers the permissions request for a user asynchronously. + /// + /// The required privacy privileges to be requested. + /// The optional, non-critical privacy privileges to be requested. + /// Privileges listed in both required and optional are treated as optional. + /// A task that returns a permission status of the requested privileges. + /// Thrown when an invalid parameter is passed. + /// Thrown when one or more required permissions are denied. + /// Thrown when a memory error occurred. + /// Thrown when the method failed due to an internal I/O error. + /// + /// + /// + /// + /// + /// 14 + public static async Task RequestPermissionsAsync(IEnumerable required = null, IEnumerable optional = null) + { + // Build the set of privileges to request + // Privileges in both required and optional are treated as optional + var optionalSet = optional != null ? new HashSet(optional) : new HashSet(); + var requiredList = required != null ? new List(required) : new List(); + + // Remove from required any privileges that are also in optional (they become optional) + var actualRequired = requiredList.Where(p => !optionalSet.Contains(p)).ToList(); + + // Combine all privileges to request (required + optional) + var allPrivileges = actualRequired.Concat(optionalSet).ToList(); + + var states = new PermissionResult(); + if (!allPrivileges.Any()) + { + return states; + } + + var result = await RequestPermissions(allPrivileges); + + // Build dictionary of results + foreach (var response in result.Responses) + { + states[response.Privilege] = response.Result.ToPermissionStatus(); + } + + // Check if any required permissions were denied + var deniedMap = actualRequired + .Where(p => states.ContainsKey(p) && !states[p].IsGranted()) + .ToDictionary(p => p, p => states[p]); + + if (deniedMap.Any()) + { + throw new PermissionDeniedException(deniedMap); + } + + return states; + } + + /// + /// Loads privileges from the application manifest file (tizen-manifest.xml) asynchronously. + /// + /// A task that returns a list of privilege strings declared in the manifest. + /// Thrown when the manifest file is not found. + /// Thrown when the manifest file contains invalid XML. + /// + /// + /// privileges = await PrivacyPrivilegeManager.LoadPrivilegesFromManifestAsync(); + /// foreach (var privilege in privileges) + /// { + /// Console.WriteLine($"Privilege: {privilege}"); + /// } + /// ]]> + /// + /// + /// 14 + public static async Task> LoadPrivilegesFromManifestAsync() + { + return await Task.Run(() => + { + var manifestPath = System.IO.Path.GetDirectoryName( + System.IO.Path.GetDirectoryName( + Tizen.Applications.Application.Current.ApplicationInfo.ExecutablePath) + ) + "/tizen-manifest.xml"; + if (!System.IO.File.Exists(manifestPath)) + { + Log.Error(LogTag, "Manifest file not found: " + manifestPath); + throw new System.IO.FileNotFoundException("Manifest file not found: " + manifestPath); + } + + var privileges = new List(); + var doc = System.Xml.Linq.XDocument.Load(manifestPath); + var ns = doc.Root?.Name.Namespace ?? System.Xml.Linq.XNamespace.None; + + var privilegesElement = doc.Root?.Element(ns + "privileges"); + if (privilegesElement != null) + { + foreach (var privilegeElement in privilegesElement.Elements(ns + "privilege")) + { + if (!string.IsNullOrEmpty(privilegeElement.Value)) + { + privileges.Add(privilegeElement.Value.Trim()); + } + } + } + + return privileges; + }); + } + /// /// This class manages event handlers of the privilege permission requests. /// This class enables having event handlers for an individual privilege. /// /// 4 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] + [Obsolete("Deprecated in API11, will be removed in API13. Use RequestPermissionsAsync() which handles context internally.")] public class ResponseContext { private string _privilege; @@ -422,7 +617,7 @@ internal ResponseContext(string privilege) /// /// Thrown when the bundle instance has been disposed. /// 4 - [Obsolete("Deprecated in API11, will be removed in API13. This API will be removed without any alternatives.")] + [Obsolete("Deprecated in API11, will be removed in API13. Use RequestPermissionsAsync() which handles context internally.")] public event EventHandler ResponseFetched { add