Post-review fixes #2 (#4)
[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(IntPtr 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                 try
219                 {
220                     CheckError((SessionError)result, "Interop failed to remove a subsession user");
221                     task.SetResult(true);
222                 }
223                 catch (Exception exception)
224                 {
225                     task.SetException(exception);
226                 }
227                 finally
228                 {
229                     _replyMap.Remove((int)data);
230                 }
231             };
232
233             SessionError ret = Interop.Session.SubsessionRemoveUser(SessionUID, userName, _replyMap[taskID], (IntPtr)taskID);
234             CheckError(ret, "Interop failed to register a reply for removing a user");
235             return task.Task;
236         }
237
238         /// <summary>
239         /// Request a subession to become currently active.
240         /// </summary>
241         /// <param name="userName">Existing subesssion user ID to be set as active</param>
242         /// <remarks>
243         /// Subsession ID must not start with a dot or have slashes.
244         /// Special subsession ID "" (empty string, see EmptyUser) can be switched to, when it's required to deactivate
245         /// current subsession (this step is needed when current session is to be removed).
246         /// </remarks>
247         /// <exception cref="ArgumentException">Session UID of this object is invalid, or user ID is not a valid subession ID</exception>
248         /// <exception cref="InvalidOperationException">Provided subsession user ID to switch to does not exist</exception>
249         /// <exception cref="OutOfMemoryException">Out of memory</exception>
250         [EditorBrowsable(EditorBrowsableState.Never)]
251         public Task SubsessionSwitchUserAsync(string userName)
252         {
253             var task = new TaskCompletionSource<bool>();
254             int taskID = 0;
255
256             lock (_replyLock)
257             {
258                 taskID = _replyID++;
259             }
260
261             _replyMap[taskID] = (int result, IntPtr data) =>
262             {
263                 try
264                 {
265                     CheckError((SessionError)result, "Interop failed to switch to a different subsession user");
266                     task.SetResult(true);
267                 }
268                 catch (Exception exception)
269                 {
270                     task.SetException(exception);
271                 }
272                 finally
273                 {
274                     _replyMap.Remove((int)data);
275                 }
276             };
277
278             SessionError ret = Interop.Session.SubsessionSwitchUser(SessionUID, userName, _replyMap[taskID], (IntPtr)taskID);
279             CheckError(ret, "Interop failed to register a reply for switching a user");
280             return task.Task;
281         }
282
283         /// <summary>
284         /// Mark event as completed.
285         /// </summary>
286         /// <param name="subsessionEventArgs">Event argument of the event (obtained from said event)</param>
287         /// <remarks>
288         /// This method is assumed to be called from an event handler. You can only mark an event as completed
289         /// if you registered for in in the same process.
290         /// </remarks>
291         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
292         /// <exception cref="IOException">Internal error</exception>
293         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
294         /// <exception cref="NotSupportedException">Not supported</exception>
295         [EditorBrowsable(EditorBrowsableState.Never)]
296         public void SubsessionEventMarkAsDone(SubsessionEventArgs subsessionEventArgs)
297         {
298             SessionError ret = Interop.Session.SubsessionEventWaitDone(subsessionEventArgs.SessionInfo);
299             CheckError(ret, $"Interop failed to mark this client's event (of type {subsessionEventArgs.SessionInfo.eventType}) as finished");
300         }
301
302         private void OnAddUserWait(IntPtr infoNative, IntPtr data)
303         {
304             _addUserWaitHandler?.Invoke(this, new AddUserEventArgs(infoNative));
305         }
306
307         private Interop.Session.SubsessionEventCallback _addUserWaitCB = null;
308
309         private event EventHandler<AddUserEventArgs> _addUserWaitHandler = null;
310
311         private readonly object _addUserWaitLock = new object();
312
313         /// <summary>
314         /// Event to be invoked when a new subession user is successfully added to this session.
315         /// </summary>
316         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
317         /// <exception cref="OutOfMemoryException">Out of memory</exception>
318         /// <exception cref="IOException">Internal error</exception>
319         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
320         /// <exception cref="NotSupportedException">Not supported</exception>
321         [EditorBrowsable(EditorBrowsableState.Never)]
322         public event EventHandler<AddUserEventArgs> AddUserWait
323         {
324             add
325             {
326                 lock (_addUserWaitLock)
327                 {
328                     if (_addUserWaitHandler == null)
329                         RegisterCallbackForEvent(SessionEventType.AddUserWait, ref _addUserWaitCB, OnAddUserWait);
330                     _addUserWaitHandler += value;
331                 }
332             }
333             remove
334             {
335                 lock (_addUserWaitLock)
336                 {
337                     _addUserWaitHandler -= value;
338                     if (_addUserWaitHandler == null)
339                         UnregisterCallbackForEvent(SessionEventType.AddUserWait, ref _addUserWaitCB);
340                 }
341             }
342         }
343
344         private void OnRemoveUserWait(IntPtr infoNative, IntPtr data)
345         {
346             _removeUserWaitHandler?.Invoke(this, new RemoveUserEventArgs(infoNative));
347         }
348
349         private Interop.Session.SubsessionEventCallback _removeUserWaitCB = null;
350
351         private event EventHandler<RemoveUserEventArgs> _removeUserWaitHandler = null;
352
353         private readonly object _removeUserWaitLock = new object();
354
355         /// <summary>
356         /// Event to be invoked when a subession user is successfully removed from this session.
357         /// </summary>
358         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
359         /// <exception cref="OutOfMemoryException">Out of memory</exception>
360         /// <exception cref="IOException">Internal error</exception>
361         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
362         /// <exception cref="NotSupportedException">Not supported</exception>
363         [EditorBrowsable(EditorBrowsableState.Never)]
364         public event EventHandler<RemoveUserEventArgs> RemoveUserWait
365         {
366             add
367             {
368                 lock (_removeUserWaitLock)
369                 {
370                     if (_removeUserWaitHandler == null)
371                         RegisterCallbackForEvent(SessionEventType.RemoveUserWait, ref _removeUserWaitCB, OnRemoveUserWait);
372                     _removeUserWaitHandler += value;
373                 }
374             }
375             remove
376             {
377                 lock (_removeUserWaitLock)
378                 {
379                     _removeUserWaitHandler -= value;
380                     if (_removeUserWaitHandler == null)
381                         UnregisterCallbackForEvent(SessionEventType.RemoveUserWait, ref _removeUserWaitCB);
382                 }
383             }
384         }
385
386         private void OnSwitchUserWait(IntPtr infoNative, IntPtr data)
387         {
388             _switchUserWaitHandler?.Invoke(this, new SwitchUserWaitEventArgs(infoNative));
389         }
390
391         private Interop.Session.SubsessionEventCallback _switchUserWaitCB = null;
392
393         private event EventHandler<SwitchUserWaitEventArgs> _switchUserWaitHandler = null;
394
395         private readonly object _switchUserWaitLock = new object();
396
397         /// <summary>
398         /// Event to be invoked when an existing subession user has begun switching to an active state.
399         /// </summary>
400         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
401         /// <exception cref="OutOfMemoryException">Out of memory</exception>
402         /// <exception cref="IOException">Internal error</exception>
403         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
404         /// <exception cref="NotSupportedException">Not supported</exception>
405         [EditorBrowsable(EditorBrowsableState.Never)]
406         public event EventHandler<SwitchUserWaitEventArgs> SwitchUserWait
407         {
408             add
409             {
410                 lock (_switchUserWaitLock)
411                 {
412                     if (_switchUserWaitHandler == null)
413                         RegisterCallbackForEvent(SessionEventType.SwitchUserWait, ref _switchUserWaitCB, OnSwitchUserWait);
414                     _switchUserWaitHandler += value;
415                 }
416             }
417             remove
418             {
419                 lock (_switchUserWaitLock)
420                 {
421                     _switchUserWaitHandler -= value;
422                     if (_switchUserWaitHandler == null)
423                         UnregisterCallbackForEvent(SessionEventType.SwitchUserWait, ref _switchUserWaitCB);
424                 }
425             }
426         }
427
428         private void OnSwitchUserCompletion(IntPtr infoNative, IntPtr data)
429         {
430             _switchUserCompletionHandler?.Invoke(this, new SwitchUserCompletionEventArgs(infoNative));
431         }
432
433         private Interop.Session.SubsessionEventCallback _switchUserCompletionCB = null;
434
435         private event EventHandler<SwitchUserCompletionEventArgs> _switchUserCompletionHandler = null;
436
437         private readonly object _switchUserCompletionLock = new object();
438
439         /// <summary>
440         /// Event to be invoked when an existing subession user is successfully switched to an acvite state.
441         /// </summary>
442         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
443         /// <exception cref="OutOfMemoryException">Out of memory</exception>
444         /// <exception cref="IOException">Internal error</exception>
445         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
446         /// <exception cref="NotSupportedException">Not supported</exception>
447         [EditorBrowsable(EditorBrowsableState.Never)]
448         public event EventHandler<SwitchUserCompletionEventArgs> SwitchUserCompleted
449         {
450             add
451             {
452                 lock (_switchUserCompletionLock)
453                 {
454                     if (_switchUserCompletionHandler == null)
455                         RegisterCallbackForEvent(SessionEventType.SwitchUserCompletion, ref _switchUserCompletionCB, OnSwitchUserCompletion);
456                     _switchUserCompletionHandler += value;
457                 }
458             }
459             remove
460             {
461                 lock (_switchUserCompletionLock)
462                 {
463                     _switchUserCompletionHandler -= value;
464                     if (_switchUserCompletionHandler == null)
465                         UnregisterCallbackForEvent(SessionEventType.SwitchUserCompletion, ref _switchUserCompletionCB);
466                 }
467             }
468         }
469
470         private void CheckError(SessionError ret, string msg)
471         {
472             if (ret == SessionError.None)
473                 return;
474
475             Log.Error(SessionErrorFactory.LogTag, msg);
476             Exception ex = SessionErrorFactory.CreateException(ret);
477             if (ex == null)
478             {
479                 Log.Error(SessionErrorFactory.LogTag,
480                     "Unexpected exception type for SessionError: " + Enum.GetName(typeof(SessionError), ret));
481                 throw new InvalidOperationException("Unrecognized error");
482             }
483             throw ex;
484         }
485
486         static void IntPtrToStringArray(IntPtr unmanagedArray, int size, out string[] managedArray)
487         {
488             managedArray = new string[size]; 
489             var curr = unmanagedArray;
490
491             for (int iterator = 0; iterator < size; iterator++)
492             {
493                 managedArray[iterator] = Marshal.PtrToStringAnsi(curr, 20);
494                 curr = IntPtr.Add(curr, 20);
495             }
496         }
497
498         private void RegisterCallbackForEvent(SessionEventType eventType, ref Interop.Session.SubsessionEventCallback eventCallback,
499             EventDelegate delegateToSet)
500         {
501             eventCallback = new Interop.Session.SubsessionEventCallback(delegateToSet);
502             SessionError ret = Interop.Session.SubesssionRegisterEventCallback(SessionUID, eventType,
503                 eventCallback, IntPtr.Zero);
504             CheckError(ret, $"Interop failed to register a callback for an event of type {eventType}");
505         }
506
507         private void UnregisterCallbackForEvent(SessionEventType eventType, ref Interop.Session.SubsessionEventCallback eventCallback)
508         {
509             SessionError ret = Interop.Session.SubesssionUnregisterEventCallback(SessionUID, eventType);
510             CheckError(ret, $"Interop failed to unregister a callback for an event of type {eventType}");
511             eventCallback = null;
512         }
513     }
514 }