Fix issues from review (#2)
[platform/core/csapi/tizenfx.git] / src / Tizen.System.Session / Session / Session.cs
1 /*
2  * Copyright (c) 2023 Samsung Electronics Co., Ltd All Rights Reserved
3  *
4  * Licensed under the Apache License, Version 2.0 (the License);
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an AS IS BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 using System;
18 using System.Collections.Concurrent;
19 using System.Collections.Generic;
20 using System.ComponentModel;
21 using System.IO;
22 using System.Runtime.InteropServices;
23 using System.Text;
24 using System.Threading.Tasks;
25
26 namespace Tizen.System
27 {
28     /// <summary>
29     /// Provides methods to manage subsession users. Allows to register for events triggered by operations on subsession users.
30     /// </summary>
31     [EditorBrowsable(EditorBrowsableState.Never)]
32     public sealed class Session
33     {
34         /// <summary>
35         /// UID of current system user.
36         /// </summary>
37         [EditorBrowsable(EditorBrowsableState.Never)]
38         public static readonly int CurrentUID;
39
40         /// <summary>
41         /// Maximum length of any given user ID.
42         /// </summary>
43         [EditorBrowsable(EditorBrowsableState.Never)]
44         public const int MaxUserLength = 20;
45
46         /// <summary>
47         /// Special subsession ID, which is always present and does not represent any user.
48         /// </summary>
49         [EditorBrowsable(EditorBrowsableState.Never)]
50         public const string EmptyUser = "";
51
52         private static ConcurrentDictionary<int, Session> s_sessionInstances = new ConcurrentDictionary<int, Session>();
53
54         private readonly object _replyLock = new object();
55
56         private IDictionary<int, Interop.Session.SubsessionReplyCallback> _replyMap = new ConcurrentDictionary<int, Interop.Session.SubsessionReplyCallback>();
57
58         private int _replyID = 0;
59
60         private delegate void EventDelegate(SubsessionEventInfoNative infoNative, IntPtr data);
61
62         static Session()
63         {
64             CurrentUID = Interop.Session.GetUID();
65         }
66
67         private Session(int sessionUID)
68         {
69             SessionUID = sessionUID;
70         }
71
72         /// <summary>
73         /// Gets a Session object instance for a given sessionUID.
74         /// </summary>
75         /// <param name="sessionUID">Session UID of a requested Session object instance</param>
76         /// <returns>Returns requested Session object</returns>
77         /// <remarks>
78         /// To ensure thread safety, expilicit creation of Session object is not allowed.
79         /// </remarks>
80         [EditorBrowsable(EditorBrowsableState.Never)]
81         public static Session GetInstance(int sessionUID)
82         {
83             if (!s_sessionInstances.ContainsKey(sessionUID))
84                 s_sessionInstances.TryAdd(sessionUID, new Session(sessionUID));
85             return s_sessionInstances[sessionUID];
86         }
87
88         /// <summary>
89         /// Gets session UID of this session object.
90         /// </summary>
91         [EditorBrowsable(EditorBrowsableState.Never)]
92         public int SessionUID { get; private set; }
93
94         /// <summary>
95         /// Gets a list of all availible subsession user IDs for this session.
96         /// </summary>
97         /// <remarks>
98         /// The list of users depends on whether the session UID for this session object exists or not. If it
99         /// doesn't, the user list is empty (in particular this is not an error).
100         /// However if the session UID exists, the user list will contain the subsession
101         /// IDs (if they exist), but also the default value, which is "" (empty string, see EmptyUser field).
102         /// This doesn't mean that "" is a subsession ID in the same way as others; it is just a marker meaning that no subsession is
103         /// enabled.
104         /// </remarks>
105         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
106         /// <exception cref="OutOfMemoryException">Out of memory</exception>
107         /// <exception cref="IOException">Internal error</exception>
108         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
109         /// <exception cref="NotSupportedException">Not supported</exception>
110         [EditorBrowsable(EditorBrowsableState.Never)]
111         public IReadOnlyList<string> GetUsers()
112         {
113
114             IntPtr ptr;
115             int count;
116
117             SessionError ret = Interop.Session.SubsessionGetUserList(SessionUID, out ptr, out count);
118             CheckError(ret, "Interop failed to get user list");
119
120             string[] users;
121             IntPtrToStringArray(ptr, count, out users);
122             Interop.Session.SubsessionFreeUserList(ptr);
123
124             return new List<string>(users);
125         }
126
127         /// <summary>
128         /// Gets a currently active subession user ID for this session.
129         /// </summary>
130         /// <remarks>
131         /// When no subsession is enabled, "" (empty string, see EmptyUser field) is returned.
132         /// This doesn't mean that "" is a subsession ID in the same way as others; it is just a marker meaning
133         /// that no subsession is enabled.
134         /// </remarks>
135         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
136         /// <exception cref="OutOfMemoryException">Out of memory</exception>
137         /// <exception cref="IOException">Internal error</exception>
138         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
139         /// <exception cref="NotSupportedException">Not supported</exception>
140         [EditorBrowsable(EditorBrowsableState.Never)]
141         public string GetCurrentUser()
142         {
143             StringBuilder user = new StringBuilder(MaxUserLength);
144             SessionError ret = Interop.Session.SubsessionGetCurrentUser(SessionUID, user);
145             CheckError(ret, "Interop failed to get current subsession user");
146
147             return user.ToString();
148         }
149
150         /// <summary>
151         /// Request new subsession to be created.
152         /// </summary>
153         /// <param name="userName">Subesssion user ID to be created</param>
154         /// <remarks>
155         /// Subsession ID must not start with a dot or have slashes.
156         /// </remarks>
157         /// <exception cref="ArgumentException">Session UID of this object is invalid, or user ID is not a valid subession ID</exception>
158         /// <exception cref="InvalidOperationException"> Provided subsession user ID already exists</exception>
159         /// <exception cref="OutOfMemoryException">Out of memory</exception>
160         [EditorBrowsable(EditorBrowsableState.Never)]
161         public Task SubsessionAddUserAsync(string userName)
162         {
163             var task = new TaskCompletionSource<bool>();
164             int taskID = 0;
165
166             lock (_replyLock)
167             {
168                 taskID = _replyID++;
169             }
170
171             _replyMap[taskID] = (int result, IntPtr data) =>
172             {
173                 try
174                 {
175                     CheckError((SessionError)result, "Interop failed to complete adding a new subsession user");
176                     task.SetResult(true);
177                 }
178                 catch (Exception exception)
179                 {
180                     task.SetException(exception);
181                 }
182                 finally
183                 {
184                     _replyMap.Remove((int)data);
185                 }
186             };
187
188             SessionError ret = Interop.Session.SubsessionAddUser(SessionUID, userName, _replyMap[taskID], (IntPtr)taskID);
189             CheckError(ret, "Interop failed to register a reply for adding a user");
190             return task.Task;
191         }
192
193         /// <summary>
194         /// Request an existing subsession to be removed.
195         /// </summary>
196         /// <param name="userName">Existing subesssion user ID to be removed</param>
197         /// <remarks>
198         /// Subsession ID must not start with a dot or have slashes.
199         /// Only inactive session ID can be removed. In order remove currently used session ID first switch to special
200         /// session ID "" (empty string, see EmptyUser), and only after switch completes, remove previously active session ID.
201         /// </remarks>
202         /// <exception cref="ArgumentException">Session UID of this object is invalid, or user ID is not a valid subession ID</exception>
203         /// <exception cref="InvalidOperationException">Provided subsession user ID does not exist</exception>
204         /// <exception cref="OutOfMemoryException">Out of memory</exception>
205         [EditorBrowsable(EditorBrowsableState.Never)]
206         public Task SubsessionRemoveUserAsync(string userName)
207         {
208             var task = new TaskCompletionSource<bool>();
209             int taskID = 0;
210
211             lock (_replyLock)
212             {
213                 taskID = _replyID++;
214             }
215
216             _replyMap[taskID] = (int result, IntPtr data) =>
217             {
218
219                 try
220                 {
221                     CheckError((SessionError)result, "Interop failed to remove a subsession user");
222                     task.SetResult(true);
223                 }
224                 catch (Exception exception)
225                 {
226                     task.SetException(exception);
227                 }
228                 finally
229                 {
230                     _replyMap.Remove((int)data);
231                 }
232             };
233
234             SessionError ret = Interop.Session.SubsessionRemoveUser(SessionUID, userName, _replyMap[taskID], (IntPtr)taskID);
235             CheckError(ret, "Interop failed to register a reply for removing a user");
236             return task.Task;
237         }
238
239         /// <summary>
240         /// Request a subession to become currently active.
241         /// </summary>
242         /// <param name="userName">Existing subesssion user ID to be set as active</param>
243         /// <remarks>
244         /// Subsession ID must not start with a dot or have slashes.
245         /// Special subsession ID "" (empty string, see EmptyUser) can be switched to, when it's required to deactivate
246         /// current subsession (this step is needed when current session is to be removed).
247         /// </remarks>
248         /// <exception cref="ArgumentException">Session UID of this object is invalid, or user ID is not a valid subession ID</exception>
249         /// <exception cref="InvalidOperationException">Provided subsession user ID to switch to does not exist</exception>
250         /// <exception cref="OutOfMemoryException">Out of memory</exception>
251         [EditorBrowsable(EditorBrowsableState.Never)]
252         public Task SubsessionSwitchUserAsync(string userName)
253         {
254             var task = new TaskCompletionSource<bool>();
255             int taskID = 0;
256
257             lock (_replyLock)
258             {
259                 taskID = _replyID++;
260             }
261
262             _replyMap[taskID] = (int result, IntPtr data) =>
263             {
264                 try
265                 {
266                     CheckError((SessionError)result, "Interop failed to switch to a different subsession user");
267                     task.SetResult(true);
268                 }
269                 catch (Exception exception)
270                 {
271                     task.SetException(exception);
272                 }
273                 finally
274                 {
275                     _replyMap.Remove((int)data);
276                 }
277             };
278
279             SessionError ret = Interop.Session.SubsessionSwitchUser(SessionUID, userName, _replyMap[taskID], (IntPtr)taskID);
280             CheckError(ret, "Interop failed to register a reply for switching a user");
281             return task.Task;
282         }
283
284         /// <summary>
285         /// Mark event as completed.
286         /// </summary>
287         /// <param name="subsessionEventArgs">Event argument of the event (obtained from said event)</param>
288         /// <remarks>
289         /// This method is assumed to be called from an event handler. You can only mark an event as completed
290         /// if you registered for in in the same process.
291         /// </remarks>
292         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
293         /// <exception cref="IOException">Internal error</exception>
294         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
295         /// <exception cref="NotSupportedException">Not supported</exception>
296         [EditorBrowsable(EditorBrowsableState.Never)]
297         public void SubsessionEventMarkAsDone(SubsessionEventArgs subsessionEventArgs)
298         {
299             SessionError ret = Interop.Session.SubsessionEventWaitDone(subsessionEventArgs.SessionInfo);
300             CheckError(ret, $"Interop failed to mark this client's event (of type {subsessionEventArgs.SessionInfo.eventType}) as finished");
301         }
302
303         private void OnAddUserWait(SubsessionEventInfoNative infoNative, IntPtr data)
304         {
305             _addUserWaitHandler?.Invoke(this, new AddUserEventArgs(infoNative));
306         }
307
308         private Interop.Session.SubsessionEventCallback _addUserWaitCB = null;
309
310         private event EventHandler<AddUserEventArgs> _addUserWaitHandler = null;
311
312         private readonly object _addUserWaitLock = new object();
313
314         /// <summary>
315         /// Event to be invoked when a new subession user is successfully added to this session.
316         /// </summary>
317         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
318         /// <exception cref="OutOfMemoryException">Out of memory</exception>
319         /// <exception cref="IOException">Internal error</exception>
320         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
321         /// <exception cref="NotSupportedException">Not supported</exception>
322         [EditorBrowsable(EditorBrowsableState.Never)]
323         public event EventHandler<AddUserEventArgs> AddUserWait
324         {
325             add
326             {
327                 lock (_addUserWaitLock)
328                 {
329                     if (_addUserWaitHandler == null)
330                         RegisterCallbackForEvent(SessionEventType.AddUserWait, ref _addUserWaitCB, OnAddUserWait);
331                     _addUserWaitHandler += value;
332                 }
333             }
334             remove
335             {
336                 lock (_addUserWaitLock)
337                 {
338                     _addUserWaitHandler -= value;
339                     if (_addUserWaitHandler == null)
340                         UnregisterCallbackForEvent(SessionEventType.AddUserWait, ref _addUserWaitCB);
341                 }
342             }
343         }
344
345         private void OnRemoveUserWait(SubsessionEventInfoNative infoNative, IntPtr data)
346         {
347             _removeUserWaitHandler?.Invoke(this, new RemoveUserEventArgs(infoNative));
348         }
349
350         private Interop.Session.SubsessionEventCallback _removeUserWaitCB = null;
351
352         private event EventHandler<RemoveUserEventArgs> _removeUserWaitHandler = null;
353
354         private readonly object _removeUserWaitLock = new object();
355
356         /// <summary>
357         /// Event to be invoked when a subession user is successfully removed from this session.
358         /// </summary>
359         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
360         /// <exception cref="OutOfMemoryException">Out of memory</exception>
361         /// <exception cref="IOException">Internal error</exception>
362         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
363         /// <exception cref="NotSupportedException">Not supported</exception>
364         [EditorBrowsable(EditorBrowsableState.Never)]
365         public event EventHandler<RemoveUserEventArgs> RemoveUserWait
366         {
367             add
368             {
369                 lock (_removeUserWaitLock)
370                 {
371                     if (_removeUserWaitHandler == null)
372                         RegisterCallbackForEvent(SessionEventType.RemoveUserWait, ref _removeUserWaitCB, OnRemoveUserWait);
373                     _removeUserWaitHandler += value;
374                 }
375             }
376             remove
377             {
378                 lock (_removeUserWaitLock)
379                 {
380                     _removeUserWaitHandler -= value;
381                     if (_removeUserWaitHandler == null)
382                         UnregisterCallbackForEvent(SessionEventType.RemoveUserWait, ref _removeUserWaitCB);
383                 }
384             }
385         }
386
387         private void OnSwitchUserWait(SubsessionEventInfoNative infoNative, IntPtr data)
388         {
389             _switchUserWaitHandler?.Invoke(this, new SwitchUserWaitEventArgs(infoNative));
390         }
391
392         private Interop.Session.SubsessionEventCallback _switchUserWaitCB = null;
393
394         private event EventHandler<SwitchUserWaitEventArgs> _switchUserWaitHandler = null;
395
396         private readonly object _switchUserWaitLock = new object();
397
398         /// <summary>
399         /// Event to be invoked when an existing subession user has begun switching to an active state.
400         /// </summary>
401         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
402         /// <exception cref="OutOfMemoryException">Out of memory</exception>
403         /// <exception cref="IOException">Internal error</exception>
404         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
405         /// <exception cref="NotSupportedException">Not supported</exception>
406         [EditorBrowsable(EditorBrowsableState.Never)]
407         public event EventHandler<SwitchUserWaitEventArgs> SwitchUserWait
408         {
409             add
410             {
411                 lock (_switchUserWaitLock)
412                 {
413                     if (_switchUserWaitHandler == null)
414                         RegisterCallbackForEvent(SessionEventType.SwitchUserWait, ref _switchUserWaitCB, OnSwitchUserWait);
415                     _switchUserWaitHandler += value;
416                 }
417             }
418             remove
419             {
420                 lock (_switchUserWaitLock)
421                 {
422                     _switchUserWaitHandler -= value;
423                     if (_switchUserWaitHandler == null)
424                         UnregisterCallbackForEvent(SessionEventType.SwitchUserWait, ref _switchUserWaitCB);
425                 }
426             }
427         }
428
429         private void OnSwitchUserCompletion(SubsessionEventInfoNative infoNative, IntPtr data)
430         {
431             _switchUserCompletionHandler?.Invoke(this, new SwitchUserCompletionEventArgs(infoNative));
432         }
433
434         private Interop.Session.SubsessionEventCallback _switchUserCompletionCB = null;
435
436         private event EventHandler<SwitchUserCompletionEventArgs> _switchUserCompletionHandler = null;
437
438         private readonly object _switchUserCompletionLock = new object();
439
440         /// <summary>
441         /// Event to be invoked when an existing subession user is successfully switched to an acvite state.
442         /// </summary>
443         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
444         /// <exception cref="OutOfMemoryException">Out of memory</exception>
445         /// <exception cref="IOException">Internal error</exception>
446         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
447         /// <exception cref="NotSupportedException">Not supported</exception>
448         [EditorBrowsable(EditorBrowsableState.Never)]
449         public event EventHandler<SwitchUserCompletionEventArgs> SwitchUserCompleted
450         {
451             add
452             {
453                 lock (_switchUserCompletionLock)
454                 {
455                     if (_switchUserCompletionHandler == null)
456                         RegisterCallbackForEvent(SessionEventType.SwitchUserCompletion, ref _switchUserCompletionCB, OnSwitchUserCompletion);
457                     _switchUserCompletionHandler += value;
458                 }
459             }
460             remove
461             {
462                 lock (_switchUserCompletionLock)
463                 {
464                     _switchUserCompletionHandler -= value;
465                     if (_switchUserCompletionHandler == null)
466                         UnregisterCallbackForEvent(SessionEventType.SwitchUserCompletion, ref _switchUserCompletionCB);
467                 }
468             }
469         }
470
471         private void CheckError(SessionError ret, string msg)
472         {
473             if (ret == SessionError.None)
474                 return;
475
476             Log.Error(SessionErrorFactory.LogTag, msg);
477             Exception ex = SessionErrorFactory.CreateException(ret);
478             if (ex == null)
479             {
480                 Log.Error(SessionErrorFactory.LogTag,
481                     "Unexpected exception type for SessionError: " + Enum.GetName(typeof(SessionError), ret));
482                 throw new InvalidOperationException("Unrecognized error");
483             }
484             throw ex;
485         }
486
487         static void IntPtrToStringArray(IntPtr unmanagedArray, int size, out string[] managedArray)
488         {
489             managedArray = new string[size]; 
490             var curr = unmanagedArray;
491             
492             for (int iterator = 0; iterator < size; iterator++)
493             {
494                 managedArray[iterator] = Marshal.PtrToStringAnsi(curr, 20);
495                 curr = IntPtr.Add(curr, 20);
496             }
497         }
498
499         private void RegisterCallbackForEvent(SessionEventType eventType, ref Interop.Session.SubsessionEventCallback eventCallback,
500             EventDelegate delegateToSet)
501         {
502             eventCallback = new Interop.Session.SubsessionEventCallback(delegateToSet);
503             SessionError ret = Interop.Session.SubesssionRegisterEventCallback(SessionUID, eventType,
504                 eventCallback, IntPtr.Zero);
505             CheckError(ret, $"Interop failed to register a callback for an event of type {eventType}");
506         }
507
508         private void UnregisterCallbackForEvent(SessionEventType eventType, ref Interop.Session.SubsessionEventCallback eventCallback)
509         {
510             SessionError ret = Interop.Session.SubesssionUnregisterEventCallback(SessionUID, eventType);
511             CheckError(ret, $"Interop failed to unregister a callback for an event of type {eventType}");
512             eventCallback = null;
513         }
514     }
515 }