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