90a034fac41d47e53bdb57e15b1a3cf8e2e6c02f
[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 of.</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 of.</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.
211         /// </remarks>
212         /// <privilege>http://tizen.org/privilege/content.write</privilege>
213         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
214         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
215         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
216         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
217         /// <exception cref="ArgumentNullException"><paramref name="path"/> is null.</exception>
218         /// <exception cref="ArgumentException">
219         ///     <paramref name="path"/> is a zero-length string, contains only white space.<br/>
220         ///     -or-<br/>
221         ///     <paramref name="path"/> contains a hidden path that starts with '.'.<br/>
222         ///     -or-<br/>
223         ///     <paramref name="path"/> contains a directory containing the ".scan_ignore" file.
224         /// </exception>
225         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
226         /// <since_tizen> 4 </since_tizen>
227         public void ScanFile(string path)
228         {
229             ValidateState();
230
231             ValidationUtil.ValidateNotNullOrEmpty(path, nameof(path));
232
233             Interop.Content.ScanFile(path).Ignore(MediaContentError.InvalidParameter).ThrowIfError("Failed to scan");
234         }
235
236         /// <summary>
237         /// Requests to scan a folder recursively.
238         /// </summary>
239         /// <remarks>
240         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.<br/>
241         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
242         /// </remarks>
243         /// <privilege>http://tizen.org/privilege/content.write</privilege>
244         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
245         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
246         /// <param name="folderPath">The path to scan.</param>
247         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
248         /// <returns>A task that represents the asynchronous scan operation.</returns>
249         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
250         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
251         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
252         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
253         /// <exception cref="ArgumentException">
254         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.<br/>
255         ///     -or-<br/>
256         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.<br/>
257         ///     -or-<br/>
258         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
259         /// </exception>
260         /// <since_tizen> 4 </since_tizen>
261         public Task ScanFolderAsync(string folderPath)
262         {
263             return ScanFolderAsync(folderPath, true);
264         }
265
266         /// <summary>
267         /// Requests to scan a folder.
268         /// </summary>
269         /// <remarks>
270         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.<br/>
271         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
272         /// </remarks>
273         /// <privilege>http://tizen.org/privilege/content.write</privilege>
274         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
275         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
276         /// <param name="folderPath">The path to scan.</param>
277         /// <param name="recursive">The value indicating if the folder is to be recursively scanned.</param>
278         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
279         /// <returns>A task that represents the asynchronous scan operation.</returns>
280         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
281         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
282         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
283         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
284         /// <exception cref="ArgumentException">
285         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.<br/>
286         ///     -or-<br/>
287         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.<br/>
288         ///     -or-<br/>
289         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
290         /// </exception>
291         /// <since_tizen> 4 </since_tizen>
292         public Task ScanFolderAsync(string folderPath, bool recursive)
293         {
294             return ScanFolderAsync(folderPath, recursive, CancellationToken.None);
295         }
296
297         /// <summary>
298         /// Requests to scan a folder recursively.
299         /// </summary>
300         /// <remarks>
301         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.<br/>
302         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
303         /// </remarks>
304         /// <privilege>http://tizen.org/privilege/content.write</privilege>
305         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
306         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
307         /// <param name="folderPath">The path to scan.</param>
308         /// <param name="cancellationToken">The token to stop scanning.</param>
309         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
310         /// <returns>A task that represents the asynchronous scan operation.</returns>
311         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
312         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
313         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
314         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
315         /// <exception cref="ArgumentException">
316         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.<br/>
317         ///     -or-<br/>
318         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.<br/>
319         ///     -or-<br/>
320         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
321         /// </exception>
322         /// <since_tizen> 4 </since_tizen>
323         public Task ScanFolderAsync(string folderPath, CancellationToken cancellationToken)
324         {
325             return ScanFolderAsync(folderPath, true, cancellationToken);
326         }
327
328         /// <summary>
329         /// Requests to scan a folder.
330         /// </summary>
331         /// <remarks>
332         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.<br/>
333         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
334         /// </remarks>
335         /// <privilege>http://tizen.org/privilege/content.write</privilege>
336         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
337         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
338         /// <param name="folderPath">The path to scan.</param>
339         /// <param name="recursive">The value indicating if the folder is to be recursively scanned.</param>
340         /// <param name="cancellationToken">The token to stop scanning.</param>
341         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
342         /// <returns>A task that represents the asynchronous scan operation.</returns>
343         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
344         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
345         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
346         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
347         /// <exception cref="ArgumentException">
348         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.<br/>
349         ///     -or-<br/>
350         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.<br/>
351         ///     -or-<br/>
352         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
353         /// </exception>
354         /// <since_tizen> 4 </since_tizen>
355         public Task ScanFolderAsync(string folderPath, bool recursive, CancellationToken cancellationToken)
356         {
357             ValidateState();
358
359             ValidationUtil.ValidateNotNullOrEmpty(folderPath, nameof(folderPath));
360
361             return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) :
362                 ScanFolderAsyncCore(folderPath, recursive, cancellationToken);
363         }
364
365         private async Task ScanFolderAsyncCore(string folderPath, bool recursive, CancellationToken cancellationToken)
366         {
367             var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
368
369             using (var cbKeeper = ObjectKeeper.Get(GetScanCompletedCallback(tcs, cancellationToken)))
370             using (RegisterCancellationAction(tcs, folderPath, cancellationToken))
371             {
372                 Interop.Content.ScanFolder(folderPath, recursive, cbKeeper.Target)
373                     .ThrowIfError("Failed to scan");
374
375                 await tcs.Task;
376             }
377         }
378
379         private static Interop.Content.MediaScanCompletedCallback GetScanCompletedCallback(TaskCompletionSource<bool> tcs,
380             CancellationToken cancellationToken)
381         {
382             return (scanResult, _) =>
383             {
384                 if (scanResult == MediaContentError.None)
385                 {
386                     if (cancellationToken.IsCancellationRequested)
387                     {
388                         tcs.TrySetCanceled();
389                     }
390                     else
391                     {
392                         tcs.TrySetResult(true);
393                     }
394                 }
395                 else
396                 {
397                     tcs.TrySetException(scanResult.AsException("Failed to scan"));
398                 }
399             };
400         }
401
402         private static IDisposable RegisterCancellationAction(TaskCompletionSource<bool> tcs,
403             string folderPath, CancellationToken cancellationToken)
404         {
405             if (cancellationToken.CanBeCanceled == false)
406             {
407                 return null;
408             }
409
410             return cancellationToken.Register(() =>
411             {
412                 if (tcs.Task.IsCompleted)
413                 {
414                     return;
415                 }
416
417                 Interop.Content.CancelScanFolder(folderPath).ThrowIfError("Failed to cancel scanning");
418             });
419         }
420
421         internal bool IsConnected { get; set; }
422
423         internal void ValidateState()
424         {
425             ValidateNotDisposed();
426
427             if (IsConnected == false)
428             {
429                 throw new InvalidOperationException("Database is not connected.");
430             }
431         }
432
433         private void ValidateNotDisposed()
434         {
435             if (IsDisposed)
436             {
437                 throw new ObjectDisposedException(nameof(MediaDatabase));
438             }
439         }
440
441         #region IDisposable Support
442         private bool _disposed = false;
443
444         /// <summary>
445         /// Disposes of the resources (other than memory) used by the MediaDatabase.
446         /// </summary>
447         /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
448         /// <since_tizen> 4 </since_tizen>
449         protected virtual void Dispose(bool disposing)
450         {
451             if (!_disposed)
452             {
453                 if (IsConnected)
454                 {
455                     var disconnectResult = Interop.Content.Disconnect();
456
457                     if (disconnectResult != MediaContentError.None)
458                     {
459                         Log.Warn(nameof(MediaDatabase), $"Failed to disconnect {disconnectResult.ToString()}.");
460                     }
461                 }
462
463                 _disposed = true;
464             }
465         }
466
467         /// <summary>
468         /// Releases all the resources.
469         /// </summary>
470         /// <since_tizen> 4 </since_tizen>
471         public void Dispose()
472         {
473             Dispose(true);
474         }
475
476         /// <summary>
477         /// Gets the value indicating whether the database has been disposed of.
478         /// </summary>
479         /// <value>true if the database has been disposed of; otherwise, false.</value>
480         /// <since_tizen> 4 </since_tizen>
481         public bool IsDisposed => _disposed;
482         #endregion
483
484     }
485 }