Release 4.0.0-preview1-00304
[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         private static readonly Interop.Content.MediaContentDBUpdatedCallback _folderUpdatedCb = (
139             MediaContentError error, int pid, ItemType updateItem, OperationType updateType,
140             MediaType mediaType, string uuid, string filePath, string mimeType, IntPtr _) =>
141         {
142             if (updateItem == ItemType.File)
143             {
144                 return;
145             }
146
147             _folderUpdated?.Invoke(null, new FolderUpdatedEventArgs(updateType, uuid, filePath));
148         };
149
150         private static IntPtr _folderUpdatedHandle = IntPtr.Zero;
151         private static event EventHandler<FolderUpdatedEventArgs> _folderUpdated;
152         private static readonly object _folderUpdatedLock = new object();
153
154         /// <summary>
155         /// Occurs when there is a change for the folder in the database.
156         /// </summary>
157         public static event EventHandler<FolderUpdatedEventArgs> FolderUpdated
158         {
159             add
160             {
161                 lock (_folderUpdatedLock)
162                 {
163                     if (_folderUpdated == null)
164                     {
165                         Interop.Content.AddDbUpdatedCb(_folderUpdatedCb, IntPtr.Zero,
166                             out _folderUpdatedHandle).ThrowIfError("Failed to register an event handler");
167                     }
168
169                     _folderUpdated += value;
170                 }
171             }
172             remove
173             {
174                 if (value == null)
175                 {
176                     return;
177                 }
178
179                 lock (_folderUpdatedLock)
180                 {
181                     if (_folderUpdated == value)
182                     {
183                         Interop.Content.RemoveDbUpdatedCb(_folderUpdatedHandle).ThrowIfError("Failed to unregister");
184                     }
185
186                     _folderUpdated -= value;
187                 }
188             }
189         }
190
191         /// <summary>
192         /// Requests to scan a media file.
193         /// </summary>
194         /// <param name="path">The path of the media to be scanned.</param>
195         /// <remarks>
196         /// It requests to scan a media file to the media server.\n
197         /// If the specified file is not registered to the database yet,
198         /// the media file information will be added to the database.\n
199         /// If it is already registered to the database, the media information is refreshed.\n
200         /// If the specified file does not exist,
201         /// the record of the media file will be deleted from the database.\n
202         /// \n
203         /// If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
204         /// If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
205         /// </remarks>
206         /// <privilege>http://tizen.org/privilege/content.write</privilege>
207         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
208         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
209         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
210         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
211         /// <exception cref="ArgumentNullException"><paramref name="path"/> is null.</exception>
212         /// <exception cref="ArgumentException">
213         ///     <paramref name="path"/> is a zero-length string, contains only white space.\n
214         ///     -or-\n
215         ///     <paramref name="path"/> contains a hidden path that starts with '.'.\n
216         ///     -or-\n
217         ///     <paramref name="path"/> contains a directory containing the ".scan_ignore" file.
218         /// </exception>
219         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
220         public void ScanFile(string path)
221         {
222             ValidateState();
223
224             ValidationUtil.ValidateNotNullOrEmpty(path, nameof(path));
225
226             Interop.Content.ScanFile(path).Ignore(MediaContentError.InvalidParameter).ThrowIfError("Failed to scan");
227         }
228
229         /// <summary>
230         /// Requests to scan a folder recursively.
231         /// </summary>
232         /// <remarks>
233         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
234         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
235         /// </remarks>
236         /// <privilege>http://tizen.org/privilege/content.write</privilege>
237         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
238         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
239         /// <param name="folderPath">The path to scan.</param>
240         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
241         /// <returns>A task that represents the asynchronous scan operation.</returns>
242         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
243         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
244         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
245         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
246         /// <exception cref="ArgumentException">
247         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
248         ///     -or-\n
249         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
250         ///     -or-\n
251         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
252         /// </exception>
253         public Task ScanFolderAsync(string folderPath)
254         {
255             return ScanFolderAsync(folderPath, true);
256         }
257
258         /// <summary>
259         /// Requests to scan a folder.
260         /// </summary>
261         /// <remarks>
262         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
263         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
264         /// </remarks>
265         /// <privilege>http://tizen.org/privilege/content.write</privilege>
266         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
267         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
268         /// <param name="folderPath">The path to scan.</param>
269         /// <param name="recursive">The value indicating if the folder is to be recursively scanned.</param>
270         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
271         /// <returns>A task that represents the asynchronous scan operation.</returns>
272         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
273         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
274         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
275         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
276         /// <exception cref="ArgumentException">
277         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
278         ///     -or-\n
279         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
280         ///     -or-\n
281         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
282         /// </exception>
283         public Task ScanFolderAsync(string folderPath, bool recursive)
284         {
285             return ScanFolderAsync(folderPath, recursive, CancellationToken.None);
286         }
287
288         /// <summary>
289         /// Requests to scan a folder recursively.
290         /// </summary>
291         /// <remarks>
292         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
293         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
294         /// </remarks>
295         /// <privilege>http://tizen.org/privilege/content.write</privilege>
296         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
297         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
298         /// <param name="folderPath">The path to scan.</param>
299         /// <param name="cancellationToken">The token to stop scanning.</param>
300         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
301         /// <returns>A task that represents the asynchronous scan operation.</returns>
302         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
303         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
304         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
305         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
306         /// <exception cref="ArgumentException">
307         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
308         ///     -or-\n
309         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
310         ///     -or-\n
311         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
312         /// </exception>
313         public Task ScanFolderAsync(string folderPath, CancellationToken cancellationToken)
314         {
315             return ScanFolderAsync(folderPath, true, cancellationToken);
316         }
317
318         /// <summary>
319         /// Requests to scan a folder recursively.
320         /// </summary>
321         /// <remarks>
322         ///     If you want to access internal storage, you should add privilege http://tizen.org/privilege/mediastorage.\n
323         ///     If you want to access external storage, you should add privilege http://tizen.org/privilege/externalstorage.
324         /// </remarks>
325         /// <privilege>http://tizen.org/privilege/content.write</privilege>
326         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
327         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
328         /// <param name="folderPath">The path to scan.</param>
329         /// <param name="recursive">The value indicating if the folder is to be recursively scanned.</param>
330         /// <param name="cancellationToken">The token to stop scanning.</param>
331         /// <remarks>Folders that contains a file named ".scan_ignore" will not be scanned.</remarks>
332         /// <returns>A task that represents the asynchronous scan operation.</returns>
333         /// <exception cref="InvalidOperationException">The database is not connected.</exception>
334         /// <exception cref="ObjectDisposedException">The <see cref="MediaDatabase"/> has already been disposed of.</exception>
335         /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
336         /// <exception cref="ArgumentNullException"><paramref name="folderPath"/> is null.</exception>
337         /// <exception cref="ArgumentException">
338         ///     <paramref name="folderPath"/> is a zero-length string, contains only white space.\n
339         ///     -or-\n
340         ///     <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
341         ///     -or-\n
342         ///     <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
343         /// </exception>
344         public Task ScanFolderAsync(string folderPath, bool recursive, CancellationToken cancellationToken)
345         {
346             ValidateState();
347
348             ValidationUtil.ValidateNotNullOrEmpty(folderPath, nameof(folderPath));
349
350             return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) :
351                 ScanFolderAsyncCore(folderPath, recursive, cancellationToken);
352         }
353
354         private async Task ScanFolderAsyncCore(string folderPath, bool recursive, CancellationToken cancellationToken)
355         {
356             var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
357
358             using (var cbKeeper = ObjectKeeper.Get(GetScanCompletedCallback(tcs, cancellationToken)))
359             using (RegisterCancellationAction(tcs, folderPath, cancellationToken))
360             {
361                 Interop.Content.ScanFolder(folderPath, recursive, cbKeeper.Target)
362                     .ThrowIfError("Failed to scan");
363
364                 await tcs.Task;
365             }
366         }
367
368         private static Interop.Content.MediaScanCompletedCallback GetScanCompletedCallback(TaskCompletionSource<bool> tcs,
369             CancellationToken cancellationToken)
370         {
371             return (scanResult, _) =>
372             {
373                 if (scanResult == MediaContentError.None)
374                 {
375                     if (cancellationToken.IsCancellationRequested)
376                     {
377                         tcs.TrySetCanceled();
378                     }
379                     else
380                     {
381                         tcs.TrySetResult(true);
382                     }
383                 }
384                 else
385                 {
386                     tcs.TrySetException(scanResult.AsException("Failed to scan"));
387                 }
388             };
389         }
390
391         private static IDisposable RegisterCancellationAction(TaskCompletionSource<bool> tcs,
392             string folderPath, CancellationToken cancellationToken)
393         {
394             if (cancellationToken.CanBeCanceled == false)
395             {
396                 return null;
397             }
398
399             return cancellationToken.Register(() =>
400             {
401                 if (tcs.Task.IsCompleted)
402                 {
403                     return;
404                 }
405
406                 Interop.Content.CancelScanFolder(folderPath).ThrowIfError("Failed to cancel scanning");
407             });
408         }
409
410         internal bool IsConnected { get; set; }
411
412         internal void ValidateState()
413         {
414             ValidateNotDisposed();
415
416             if (IsConnected == false)
417             {
418                 throw new InvalidOperationException("Database is not connected.");
419             }
420         }
421
422         private void ValidateNotDisposed()
423         {
424             if (IsDisposed)
425             {
426                 throw new ObjectDisposedException(nameof(MediaDatabase));
427             }
428         }
429
430         #region IDisposable Support
431         private bool _disposed = false;
432
433         /// <summary>
434         /// Disposes of the resources (other than memory) used by the MediaDatabase.
435         /// </summary>
436         /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
437         protected virtual void Dispose(bool disposing)
438         {
439             if (!_disposed)
440             {
441                 if (IsConnected)
442                 {
443                     var disconnectResult = Interop.Content.Disconnect();
444
445                     if (disconnectResult != MediaContentError.None)
446                     {
447                         Log.Warn(nameof(MediaDatabase), $"Failed to disconnect {disconnectResult.ToString()}.");
448                     }
449                 }
450
451                 _disposed = true;
452             }
453         }
454
455         /// <summary>
456         /// Releases all the resources.
457         /// </summary>
458         public void Dispose()
459         {
460             Dispose(true);
461         }
462
463         /// <summary>
464         /// Gets the value indicating whether the database has been disposed of.
465         /// </summary>
466         /// <value>true if the database has been disposed of; otherwise, false.</value>
467         public bool IsDisposed => _disposed;
468         #endregion
469
470     }
471 }