8edfc4a69c7b7220b6f5d9c193398ada0334b099
[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                 _replyMap.Remove((int)data);
174                 try
175                 {
176                     CheckError((SessionError)result, "Interop failed to complete adding a new subsession user");
177                 }
178                 catch (Exception exception)
179                 {
180                     task.SetException(exception);
181                     return;
182                 }
183
184                 task.SetResult(true);
185             };
186
187             SessionError ret = Interop.Session.SubsessionAddUser(SessionUID, userName, _replyMap[taskID], (IntPtr)taskID);
188             CheckError(ret, "Interop failed to register a reply for adding a user");
189             return task.Task;
190         }
191
192         /// <summary>
193         /// Request an existing subsession to be removed.
194         /// </summary>
195         /// <param name="userName">Existing subesssion user ID to be removed</param>
196         /// <remarks>
197         /// Subsession ID must not start with a dot or have slashes.
198         /// Only inactive session ID can be removed. In order remove currently used session ID first switch to special
199         /// session ID "" (empty string, see EmptyUser), and only after switch completes, remove previously active session ID.
200         /// </remarks>
201         /// <exception cref="ArgumentException">Session UID of this object is invalid, or user ID is not a valid subession ID</exception>
202         /// <exception cref="InvalidOperationException">Provided subsession user ID does not exist</exception>
203         /// <exception cref="OutOfMemoryException">Out of memory</exception>
204         [EditorBrowsable(EditorBrowsableState.Never)]
205         public Task SubsessionRemoveUserAsync(string userName)
206         {
207             var task = new TaskCompletionSource<bool>();
208             int taskID = 0;
209
210             lock (_replyLock)
211             {
212                 taskID = _replyID++;
213             }
214
215             _replyMap[taskID] = (int result, IntPtr data) =>
216             {
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                     return;
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                     return;
273                 }
274                 finally
275                 {
276                     _replyMap.Remove((int)data);
277                 }
278             };
279
280             SessionError ret = Interop.Session.SubsessionSwitchUser(SessionUID, userName, _replyMap[taskID], (IntPtr)taskID);
281             CheckError(ret, "Interop failed to register a reply for switching a user");
282             return task.Task;
283         }
284
285         /// <summary>
286         /// Mark event as completed.
287         /// </summary>
288         /// <param name="subsessionEventArgs">Event argument of the event (obtained from said event)</param>
289         /// <remarks>
290         /// This method is assumed to be called from an event handler. You can only mark an event as completed
291         /// if you registered for in in the same process.
292         /// </remarks>
293         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
294         /// <exception cref="IOException">Internal error</exception>
295         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
296         /// <exception cref="NotSupportedException">Not supported</exception>
297         [EditorBrowsable(EditorBrowsableState.Never)]
298         public void SubsessionEventMarkAsDone(SubsessionEventArgs subsessionEventArgs)
299         {
300             SessionError ret = Interop.Session.SubsessionEventWaitDone(subsessionEventArgs.SessionInfo);
301             CheckError(ret, $"Interop failed to mark this client's event (of type {subsessionEventArgs.SessionInfo.eventType}) as finished");
302         }
303
304         private void OnAddUserWait(SubsessionEventInfoNative infoNative, IntPtr data)
305         {
306             _addUserWaitHandler?.Invoke(this, new AddUserEventArgs(infoNative));
307         }
308
309         private Interop.Session.SubsessionEventCallback _addUserWaitCB = null;
310
311         private event EventHandler<AddUserEventArgs> _addUserWaitHandler = null;
312
313         private readonly object _addUserWaitLock = new object();
314
315         /// <summary>
316         /// Event to be invoked when a new subession user is successfully added to this session.
317         /// </summary>
318         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
319         /// <exception cref="OutOfMemoryException">Out of memory</exception>
320         /// <exception cref="IOException">Internal error</exception>
321         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
322         /// <exception cref="NotSupportedException">Not supported</exception>
323         [EditorBrowsable(EditorBrowsableState.Never)]
324         public event EventHandler<AddUserEventArgs> AddUserWait
325         {
326             add
327             {
328                 lock (_addUserWaitLock)
329                 {
330                     if (_addUserWaitHandler == null)
331                         RegisterCallbackForEvent(SessionEventType.AddUserWait, _addUserWaitCB, OnAddUserWait);
332                     _addUserWaitHandler += value;
333                 }
334             }
335             remove
336             {
337                 lock (_addUserWaitLock)
338                 {
339                     _addUserWaitHandler -= value;
340                     if (_addUserWaitHandler == null)
341                         UnregisterCallbackForEvent(SessionEventType.AddUserWait, _addUserWaitCB);
342                 }
343             }
344         }
345
346         private void OnRemoveUserWait(SubsessionEventInfoNative infoNative, IntPtr data)
347         {
348             _removeUserWaitHandler?.Invoke(this, new RemoveUserEventArgs(infoNative));
349         }
350
351         private Interop.Session.SubsessionEventCallback _removeUserWaitCB = null;
352
353         private event EventHandler<RemoveUserEventArgs> _removeUserWaitHandler = null;
354
355         private readonly object _removeUserWaitLock = new object();
356
357         /// <summary>
358         /// Event to be invoked when a subession user is successfully removed from this session.
359         /// </summary>
360         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
361         /// <exception cref="OutOfMemoryException">Out of memory</exception>
362         /// <exception cref="IOException">Internal error</exception>
363         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
364         /// <exception cref="NotSupportedException">Not supported</exception>
365         [EditorBrowsable(EditorBrowsableState.Never)]
366         public event EventHandler<RemoveUserEventArgs> RemoveUserWait
367         {
368             add
369             {
370                 lock (_removeUserWaitLock)
371                 {
372                     if (_removeUserWaitHandler == null)
373                         RegisterCallbackForEvent(SessionEventType.RemoveUserWait, _removeUserWaitCB, OnRemoveUserWait);
374                     _removeUserWaitHandler += value;
375                 }
376             }
377             remove
378             {
379                 lock (_removeUserWaitLock)
380                 {
381                     _removeUserWaitHandler -= value;
382                     if (_removeUserWaitHandler == null)
383                         UnregisterCallbackForEvent(SessionEventType.RemoveUserWait, _removeUserWaitCB);
384                 }
385             }
386         }
387
388         private void OnSwitchUserWait(SubsessionEventInfoNative infoNative, IntPtr data)
389         {
390             _switchUserWaitHandler?.Invoke(this, new SwitchUserWaitEventArgs(infoNative));
391         }
392
393         private Interop.Session.SubsessionEventCallback _switchUserWaitCB = null;
394
395         private event EventHandler<SwitchUserWaitEventArgs> _switchUserWaitHandler = null;
396
397         private readonly object _switchUserWaitLock = new object();
398
399         /// <summary>
400         /// Event to be invoked when an existing subession user has begun switching to an active state.
401         /// </summary>
402         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
403         /// <exception cref="OutOfMemoryException">Out of memory</exception>
404         /// <exception cref="IOException">Internal error</exception>
405         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
406         /// <exception cref="NotSupportedException">Not supported</exception>
407         [EditorBrowsable(EditorBrowsableState.Never)]
408         public event EventHandler<SwitchUserWaitEventArgs> SwitchUserWait
409         {
410             add
411             {
412                 lock (_switchUserWaitLock)
413                 {
414                     if (_switchUserWaitHandler == null)
415                         RegisterCallbackForEvent(SessionEventType.SwitchUserWait, _switchUserWaitCB, OnSwitchUserWait);
416                     _switchUserWaitHandler += value;
417                 }
418             }
419             remove
420             {
421                 lock (_switchUserWaitLock)
422                 {
423                     _switchUserWaitHandler -= value;
424                     if (_switchUserWaitHandler == null)
425                         UnregisterCallbackForEvent(SessionEventType.SwitchUserWait, _switchUserWaitCB);
426                 }
427             }
428         }
429
430         private void OnSwitchUserCompletion(SubsessionEventInfoNative infoNative, IntPtr data)
431         {
432             _switchUserCompletionHandler?.Invoke(this, new SwitchUserCompletionEventArgs(infoNative));
433         }
434
435         private Interop.Session.SubsessionEventCallback _switchUserCompletionCB = null;
436
437         private event EventHandler<SwitchUserCompletionEventArgs> _switchUserCompletionHandler = null;
438
439         private readonly object _switchUserCompletionLock = new object();
440
441         /// <summary>
442         /// Event to be invoked when an existing subession user is successfully switched to an acvite state.
443         /// </summary>
444         /// <exception cref="ArgumentException">Session UID of this object is invalid</exception>
445         /// <exception cref="OutOfMemoryException">Out of memory</exception>
446         /// <exception cref="IOException">Internal error</exception>
447         /// <exception cref="UnauthorizedAccessException">Not permitted</exception>
448         /// <exception cref="NotSupportedException">Not supported</exception>
449         [EditorBrowsable(EditorBrowsableState.Never)]
450         public event EventHandler<SwitchUserCompletionEventArgs> SwitchUserCompleted
451         {
452             add
453             {
454                 lock (_switchUserCompletionLock)
455                 {
456                     if (_switchUserCompletionHandler == null)
457                         RegisterCallbackForEvent(SessionEventType.SwitchUserCompletion, _switchUserCompletionCB, OnSwitchUserCompletion);
458                     _switchUserCompletionHandler += value;
459                 }
460             }
461             remove
462             {
463                 lock (_switchUserCompletionLock)
464                 {
465                     _switchUserCompletionHandler -= value;
466                     if (_switchUserCompletionHandler == null)
467                         UnregisterCallbackForEvent(SessionEventType.SwitchUserCompletion, _switchUserCompletionCB);
468                 }
469             }
470         }
471
472         private void CheckError(SessionError ret, string msg)
473         {
474             if (ret == SessionError.None)
475                 return;
476
477             Log.Error(SessionErrorFactory.LogTag, msg);
478             Exception ex = SessionErrorFactory.CreateException(ret);
479             if (ex == null)
480             {
481                 Log.Error(SessionErrorFactory.LogTag,
482                     "Unexpected exception type for SessionError: " + Enum.GetName(typeof(SessionError), ret));
483                 throw new InvalidOperationException("Unrecognized error");
484             }
485             throw ex;
486         }
487
488         private void IntPtrToStringArray(IntPtr unmanagedArray, int size, out string[] managedArray)
489         {
490             managedArray = new string[size];
491
492             byte[] byteArray = new byte[MaxUserLength * size];
493             byte[] tmpArray = new byte[MaxUserLength];
494
495             Marshal.Copy(unmanagedArray, byteArray, 0, MaxUserLength * size);
496
497             for (int i = 0; i < size; i++)
498             {
499                 Array.Copy(byteArray, MaxUserLength * i, tmpArray, 0, MaxUserLength);
500                 managedArray[i] = Encoding.UTF8.GetString(tmpArray);
501             }
502         }
503
504         private void RegisterCallbackForEvent(SessionEventType eventType, Interop.Session.SubsessionEventCallback eventCallback,
505             EventDelegate delegateToSet)
506         {
507             eventCallback = new Interop.Session.SubsessionEventCallback(delegateToSet);
508             SessionError ret = Interop.Session.SubesssionRegisterEventCallback(SessionUID, eventType,
509                 eventCallback, IntPtr.Zero);
510             CheckError(ret, $"Interop failed to register a callback for an event of type {eventType}");
511         }
512
513         private void UnregisterCallbackForEvent(SessionEventType eventType, Interop.Session.SubsessionEventCallback eventCallback)
514         {
515             SessionError ret = Interop.Session.SubesssionUnregisterEventCallback(SessionUID, eventType);
516             CheckError(ret, $"Interop failed to unregister a callback for an event of type {eventType}");
517             eventCallback = null;
518         }
519     }
520 }