Merge "[Stt/SttEngine/Tts/TtsEngine/VoiceControl][Remove build warnings]"
[platform/core/csapi/tizenfx.git] / src / Tizen.Content.MediaContent / Tizen.Content.MediaContent / MediaDatabase.cs
1 /*
2  * Copyright (c) 2016 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.Linq;
19 using System.Threading;
20 using System.Threading.Tasks;
21
22 namespace Tizen.Content.MediaContent
23 {
24     /// <summary>
25     /// Provides the ability to connect to and manage the database.
26     /// </summary>
27     public class MediaDatabase : IDisposable
28     {
29         /// <summary>
30         /// Initializes a new instance of the <see cref="MediaDatabase"/> class.
31         /// </summary>
32         public MediaDatabase()
33         {
34         }
35
36         private object _lock = new object();
37
38         /// <summary>
39         /// Connects to the database.
40         /// </summary>
41         /// <exception cref="InvalidOperationException">The database is already connected.</exception>
42         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
43         /// <exception cref="MediaDatabaseException">An error occurred while connecting.</exception>
44         public void Connect()
45         {
46             ValidateNotDisposed();
47
48             lock (_lock)
49             {
50                 if (IsConnected)
51                 {
52                     throw new InvalidOperationException("The database is already connected.");
53                 }
54
55                 Interop.Content.Connect().ThrowIfError("Failed to connect");
56
57                 IsConnected = true;
58             }
59         }
60
61         /// <summary>
62         /// Disconnects from the media database.
63         /// </summary>
64         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
65         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
66         /// <exception cref="MediaDatabaseException">An error occurred while connecting.</exception>
67         public void Disconnect()
68         {
69             ValidateNotDisposed();
70
71             lock (_lock)
72             {
73                 if (!IsConnected)
74                 {
75                     throw new InvalidOperationException("The database is not connected.");
76                 }
77
78                 Interop.Content.Disconnect().ThrowIfError("Failed to disconnect");
79
80                 IsConnected = false;
81             }
82         }
83
84         private static readonly Interop.Content.MediaContentDBUpdatedCallback _mediaInfoUpdatedCb = (
85             MediaContentError error, int pid, ItemType updateItem, OperationType updateType,
86             MediaType mediaType, string uuid, string filePath, string mimeType, IntPtr _) =>
87         {
88             if (updateItem == ItemType.Directory)
89             {
90                 return;
91             }
92
93             _mediaInfoUpdated?.Invoke(
94                 null, new MediaInfoUpdatedEventArgs(pid, updateType, mediaType, uuid, filePath, mimeType));
95         };
96
97         private static IntPtr _mediaInfoUpdatedHandle = IntPtr.Zero;
98         private static event EventHandler<MediaInfoUpdatedEventArgs> _mediaInfoUpdated;
99         private static readonly object _mediaInfoUpdatedLock = new object();
100
101         /// <summary>
102         /// Occurs when there is a change for media in the database.
103         /// </summary>
104         public static event EventHandler<MediaInfoUpdatedEventArgs> MediaInfoUpdated
105         {
106             add
107             {
108                 lock (_mediaInfoUpdatedLock)
109                 {
110                     if (_mediaInfoUpdated == null)
111                     {
112                         Interop.Content.AddDbUpdatedCb(_mediaInfoUpdatedCb, IntPtr.Zero,
113                             out _mediaInfoUpdatedHandle).ThrowIfError("Failed to register an event handler");
114                     }
115
116                     _mediaInfoUpdated += value;
117                 }
118             }
119             remove
120             {
121                 if (value == null)
122                 {
123                     return;
124                 }
125
126                 lock (_mediaInfoUpdatedLock)
127                 {
128                     if (_mediaInfoUpdated == value)
129                     {
130                         Interop.Content.RemoveDbUpdatedCb(_mediaInfoUpdatedHandle).ThrowIfError("Failed to unregister");
131                     }
132
133                     _mediaInfoUpdated -= value;
134                 }
135             }
136         }
137
138
139         private static readonly Interop.Content.MediaContentDBUpdatedCallback _folderUpdatedCb = (
140             MediaContentError error, int pid, ItemType updateItem, OperationType updateType,
141             MediaType mediaType, string uuid, string filePath, string mimeType, IntPtr _) =>
142         {
143             if (updateItem == ItemType.File)
144             {
145                 return;
146             }
147
148             _folderUpdated?.Invoke(null, new FolderUpdatedEventArgs(updateType, uuid, filePath));
149         };
150
151         private static IntPtr _folderUpdatedHandle = IntPtr.Zero;
152         private static event EventHandler<FolderUpdatedEventArgs> _folderUpdated;
153         private static readonly object _folderUpdatedLock = new object();
154
155         /// <summary>
156         /// Occurs when there is a change for the folder in the database.
157         /// </summary>
158         public static event EventHandler<FolderUpdatedEventArgs> FolderUpdated
159         {
160             add
161             {
162                 lock (_folderUpdatedLock)
163                 {
164                     if (_folderUpdated == null)
165                     {
166                         Interop.Content.AddDbUpdatedCb(_folderUpdatedCb, IntPtr.Zero,
167                             out _folderUpdatedHandle).ThrowIfError("Failed to register an event handler");
168                     }
169
170                     _folderUpdated += value;
171                 }
172             }
173             remove
174             {
175                 if (value == null)
176                 {
177                     return;
178                 }
179
180                 lock (_folderUpdatedLock)
181                 {
182                     if (_folderUpdated == value)
183                     {
184                         Interop.Content.RemoveDbUpdatedCb(_folderUpdatedHandle).ThrowIfError("Failed to unregister");
185                     }
186
187                     _folderUpdated -= value;
188                 }
189             }
190         }
191
192         /// <summary>
193         /// Requests to scan a media file.
194         /// </summary>
195         /// <param name="path">The path of the media to be scanned.</param>
196         /// <remarks>
197         /// It requests to scan a media file to the media server.\n
198         /// If the specified file is not registered to the database yet,
199         /// the media file information will be added to the database.\n
200         /// If it is already registered to the database, the media information is refreshed.\n
201         /// If the specified file does not exist,
202         /// the record of the media file will be deleted from the database.\n
203         /// \n
204         /// If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
205         /// If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
206         /// </remarks>
207         /// <privilege>http://tizen.org/privilege/content.write</privilege>
208         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
209         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
210         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
211         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
212         /// <exception cref="ArgumentNullException"><paramref name="path"/> is null.</exception>
213         /// <exception cref="ArgumentException">
214         ///     <paramref name="path"/> is a zero-length string, contains only white space.\n
215         ///     -or-\n
216         ///     <paramref name="path"/> contains a hidden path that starts with '.'.\n
217         ///     -or-\n
218         ///     <paramref name="path"/> contains a directory containing the ".scan_ignore" file.
219         /// </exception>
220         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
221         public void ScanFile(string path)
222         {
223             ValidateState();
224
225             ValidationUtil.ValidateNotNullOrEmpty(path, nameof(path));
226
227             Interop.Content.ScanFile(path).Ignore(MediaContentError.InvalidParameter).ThrowIfError("Failed to scan");
228         }
229
230         /// <summary>
231         /// Requests to scan a folder recursively.
232         /// </summary>
233         /// <remarks>
234         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
235         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
236         /// </remarks>
237         /// <privilege>http://tizen.org/privilege/content.write</privilege>
238         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
239         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
240         /// <param name="folderPath">The path to scan.</param>
241         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
242         /// <returns>A task that represents the asynchronous scan operation.</returns>
243         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
244         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
245         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
246         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
247         /// <exception cref="ArgumentException">
248         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
249         ///     -or-\n
250         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
251         ///     -or-\n
252         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
253         /// </exception>
254         public Task ScanFolderAsync(string folderPath)
255         {
256             return ScanFolderAsync(folderPath, true);
257         }
258
259         /// <summary>
260         /// Requests to scan a folder.
261         /// </summary>
262         /// <remarks>
263         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
264         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
265         /// </remarks>
266         /// <privilege>http://tizen.org/privilege/content.write</privilege>
267         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
268         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
269         /// <param name="folderPath">The path to scan.</param>
270         /// <param name="recursive">The value indicating if the folder is to be recursively scanned.</param>
271         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
272         /// <returns>A task that represents the asynchronous scan operation.</returns>
273         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
274         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
275         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
276         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
277         /// <exception cref="ArgumentException">
278         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
279         ///     -or-\n
280         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
281         ///     -or-\n
282         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
283         /// </exception>
284         public Task ScanFolderAsync(string folderPath, bool recursive)
285         {
286             return ScanFolderAsync(folderPath, recursive, CancellationToken.None);
287         }
288
289         /// <summary>
290         /// Requests to scan a folder recursively.
291         /// </summary>
292         /// <remarks>
293         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
294         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
295         /// </remarks>
296         /// <privilege>http://tizen.org/privilege/content.write</privilege>
297         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
298         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
299         /// <param name="folderPath">The path to scan.</param>
300         /// <param name="cancellationToken">The token to stop scanning.</param>
301         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
302         /// <returns>A task that represents the asynchronous scan operation.</returns>
303         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
304         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
305         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
306         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
307         /// <exception cref="ArgumentException">
308         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
309         ///     -or-\n
310         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
311         ///     -or-\n
312         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
313         /// </exception>
314         public Task ScanFolderAsync(string folderPath, CancellationToken cancellationToken)
315         {
316             return ScanFolderAsync(folderPath, true, cancellationToken);
317         }
318
319         /// <summary>
320         /// Requests to scan a folder recursively.
321         /// </summary>
322         /// <remarks>
323         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
324         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
325         /// </remarks>
326         /// <privilege>http://tizen.org/privilege/content.write</privilege>
327         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
328         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
329         /// <param name="folderPath">The path to scan.</param>
330         /// <param name="recursive">The value indicating if the folder is to be recursively scanned.</param>
331         /// <param name="cancellationToken">The token to stop scanning.</param>
332         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
333         /// <returns>A task that represents the asynchronous scan operation.</returns>
334         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
335         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
336         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
337         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
338         /// <exception cref="ArgumentException">
339         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
340         ///     -or-\n
341         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
342         ///     -or-\n
343         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
344         /// </exception>
345         public Task ScanFolderAsync(string folderPath, bool recursive, CancellationToken cancellationToken)
346         {
347             ValidateState();
348
349             ValidationUtil.ValidateNotNullOrEmpty(folderPath, nameof(folderPath));
350
351             return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) :
352                 ScanFolderAsyncCore(folderPath, recursive, cancellationToken);
353         }
354
355         private async Task ScanFolderAsyncCore(string folderPath, bool recursive, CancellationToken cancellationToken)
356         {
357             var tcs = new TaskCompletionSource<bool>(TaskContinuationOptions.RunContinuationsAsynchronously);
358
359             using (var cbKeeper = ObjectKeeper.Get(GetScanCompletedCallback(tcs, cancellationToken)))
360             using (RegisterCancellationAction(tcs, folderPath, cancellationToken))
361             {
362                 Interop.Content.ScanFolder(folderPath, recursive, cbKeeper.Target)
363                     .ThrowIfError("Failed to scan");
364
365                 await tcs.Task;
366             }
367         }
368
369         private static Interop.Content.MediaScanCompletedCallback GetScanCompletedCallback(TaskCompletionSource<bool> tcs,
370             CancellationToken cancellationToken)
371         {
372             return (scanResult, _) =>
373             {
374                 if (scanResult == MediaContentError.None)
375                 {
376                     if (cancellationToken.IsCancellationRequested)
377                     {
378                         tcs.TrySetCanceled();
379                     }
380                     else
381                     {
382                         tcs.TrySetResult(true);
383                     }
384                 }
385                 else
386                 {
387                     tcs.TrySetException(scanResult.AsException("Failed to scan"));
388                 }
389             };
390         }
391
392         private static IDisposable RegisterCancellationAction(TaskCompletionSource<bool> tcs,
393             string folderPath, CancellationToken cancellationToken)
394         {
395             if (cancellationToken.CanBeCanceled == false)
396             {
397                 return null;
398             }
399
400             return cancellationToken.Register(() =>
401             {
402                 if (tcs.Task.IsCompleted)
403                 {
404                     return;
405                 }
406
407                 Interop.Content.CancelScanFolder(folderPath).ThrowIfError("Failed to cancel scanning");
408             });
409         }
410
411         internal bool IsConnected { get; set; }
412
413         internal void ValidateState()
414         {
415             ValidateNotDisposed();
416
417             if (IsConnected == false)
418             {
419                 throw new InvalidOperationException("Database is not connected.");
420             }
421         }
422
423         private void ValidateNotDisposed()
424         {
425             if (IsDisposed)
426             {
427                 throw new ObjectDisposedException(nameof(MediaDatabase));
428             }
429         }
430
431         #region IDisposable Support
432         private bool _disposed = false;
433
434         /// <summary>
435         /// Disposes of the resources (other than memory) used by the MediaDatabase.
436         /// </summary>
437         /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
438         protected virtual void Dispose(bool disposing)
439         {
440             if (!_disposed)
441             {
442                 if (IsConnected)
443                 {
444                     var disconnectResult = Interop.Content.Disconnect();
445
446                     if (disconnectResult != MediaContentError.None)
447                     {
448                         Log.Warn(nameof(MediaDatabase), $"Failed to disconnect {disconnectResult.ToString()}.");
449                     }
450                 }
451
452                 _disposed = true;
453             }
454         }
455
456         /// <summary>
457         /// Releases all the resources.
458         /// </summary>
459         public void Dispose()
460         {
461             Dispose(true);
462         }
463
464         /// <summary>
465         /// Gets the value indicating whether the database has been disposed of.
466         /// </summary>
467         /// <value>true if the database has been disposed of; otherwise, false.</value>
468         public bool IsDisposed => _disposed;
469         #endregion
470
471     }
472 }