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