2 * Copyright (c) 2016 Samsung Electronics Co., Ltd All Rights Reserved
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 using System.Threading;
20 using System.Threading.Tasks;
22 namespace Tizen.Content.MediaContent
25 /// Provides the ability to connect to and manage the database.
27 public class MediaDatabase : IDisposable
30 /// Initializes a new instance of the <see cref="MediaDatabase"/> class.
32 public MediaDatabase()
36 private object _lock = new object();
39 /// Connects to the database.
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>
46 ValidateNotDisposed();
52 throw new InvalidOperationException("The database is already connected.");
55 Interop.Content.Connect().ThrowIfError("Failed to connect");
62 /// Disconnects from the media database.
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()
69 ValidateNotDisposed();
75 throw new InvalidOperationException("The database is not connected.");
78 Interop.Content.Disconnect().ThrowIfError("Failed to disconnect");
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 _) =>
88 if (updateItem == ItemType.Directory)
93 _mediaInfoUpdated?.Invoke(
94 null, new MediaInfoUpdatedEventArgs(pid, updateType, mediaType, uuid, filePath, mimeType));
97 private static IntPtr _mediaInfoUpdatedHandle = IntPtr.Zero;
98 private static event EventHandler<MediaInfoUpdatedEventArgs> _mediaInfoUpdated;
99 private static readonly object _mediaInfoUpdatedLock = new object();
102 /// Occurs when there is a change for media in the database.
104 public static event EventHandler<MediaInfoUpdatedEventArgs> MediaInfoUpdated
108 lock (_mediaInfoUpdatedLock)
110 if (_mediaInfoUpdated == null)
112 Interop.Content.AddDbUpdatedCb(_mediaInfoUpdatedCb, IntPtr.Zero,
113 out _mediaInfoUpdatedHandle).ThrowIfError("Failed to register an event handler");
116 _mediaInfoUpdated += value;
126 lock (_mediaInfoUpdatedLock)
128 if (_mediaInfoUpdated == value)
130 Interop.Content.RemoveDbUpdatedCb(_mediaInfoUpdatedHandle).ThrowIfError("Failed to unregister");
133 _mediaInfoUpdated -= value;
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 _) =>
142 if (updateItem == ItemType.File)
147 _folderUpdated?.Invoke(null, new FolderUpdatedEventArgs(updateType, uuid, filePath));
150 private static IntPtr _folderUpdatedHandle = IntPtr.Zero;
151 private static event EventHandler<FolderUpdatedEventArgs> _folderUpdated;
152 private static readonly object _folderUpdatedLock = new object();
155 /// Occurs when there is a change for the folder in the database.
157 public static event EventHandler<FolderUpdatedEventArgs> FolderUpdated
161 lock (_folderUpdatedLock)
163 if (_folderUpdated == null)
165 Interop.Content.AddDbUpdatedCb(_folderUpdatedCb, IntPtr.Zero,
166 out _folderUpdatedHandle).ThrowIfError("Failed to register an event handler");
169 _folderUpdated += value;
179 lock (_folderUpdatedLock)
181 if (_folderUpdated == value)
183 Interop.Content.RemoveDbUpdatedCb(_folderUpdatedHandle).ThrowIfError("Failed to unregister");
186 _folderUpdated -= value;
192 /// Requests to scan a media file.
194 /// <param name="path">The path of the media to be scanned.</param>
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
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.
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
215 /// <paramref name="path"/> contains a hidden path that starts with '.'.\n
217 /// <paramref name="path"/> contains a directory containing the ".scan_ignore" file.
219 /// <exception cref="UnauthorizedAccessException">The caller has no required privilege.</exception>
220 public void ScanFile(string path)
224 ValidationUtil.ValidateNotNullOrEmpty(path, nameof(path));
226 Interop.Content.ScanFile(path).Ignore(MediaContentError.InvalidParameter).ThrowIfError("Failed to scan");
230 /// Requests to scan a folder recursively.
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.
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
249 /// <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
251 /// <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
253 public Task ScanFolderAsync(string folderPath)
255 return ScanFolderAsync(folderPath, true);
259 /// Requests to scan a folder.
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.
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
279 /// <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
281 /// <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
283 public Task ScanFolderAsync(string folderPath, bool recursive)
285 return ScanFolderAsync(folderPath, recursive, CancellationToken.None);
289 /// Requests to scan a folder recursively.
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.
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
309 /// <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
311 /// <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
313 public Task ScanFolderAsync(string folderPath, CancellationToken cancellationToken)
315 return ScanFolderAsync(folderPath, true, cancellationToken);
319 /// Requests to scan a folder recursively.
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.
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
340 /// <paramref name="folderPath"/> contains a hidden path that starts with '.'.\n
342 /// <paramref name="folderPath"/> contains a directory containing the ".scan_ignore" file.
344 public Task ScanFolderAsync(string folderPath, bool recursive, CancellationToken cancellationToken)
348 ValidationUtil.ValidateNotNullOrEmpty(folderPath, nameof(folderPath));
350 return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) :
351 ScanFolderAsyncCore(folderPath, recursive, cancellationToken);
354 private async Task ScanFolderAsyncCore(string folderPath, bool recursive, CancellationToken cancellationToken)
356 var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
358 using (var cbKeeper = ObjectKeeper.Get(GetScanCompletedCallback(tcs, cancellationToken)))
359 using (RegisterCancellationAction(tcs, folderPath, cancellationToken))
361 Interop.Content.ScanFolder(folderPath, recursive, cbKeeper.Target)
362 .ThrowIfError("Failed to scan");
368 private static Interop.Content.MediaScanCompletedCallback GetScanCompletedCallback(TaskCompletionSource<bool> tcs,
369 CancellationToken cancellationToken)
371 return (scanResult, _) =>
373 if (scanResult == MediaContentError.None)
375 if (cancellationToken.IsCancellationRequested)
377 tcs.TrySetCanceled();
381 tcs.TrySetResult(true);
386 tcs.TrySetException(scanResult.AsException("Failed to scan"));
391 private static IDisposable RegisterCancellationAction(TaskCompletionSource<bool> tcs,
392 string folderPath, CancellationToken cancellationToken)
394 if (cancellationToken.CanBeCanceled == false)
399 return cancellationToken.Register(() =>
401 if (tcs.Task.IsCompleted)
406 Interop.Content.CancelScanFolder(folderPath).ThrowIfError("Failed to cancel scanning");
410 internal bool IsConnected { get; set; }
412 internal void ValidateState()
414 ValidateNotDisposed();
416 if (IsConnected == false)
418 throw new InvalidOperationException("Database is not connected.");
422 private void ValidateNotDisposed()
426 throw new ObjectDisposedException(nameof(MediaDatabase));
430 #region IDisposable Support
431 private bool _disposed = false;
434 /// Disposes of the resources (other than memory) used by the MediaDatabase.
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)
443 var disconnectResult = Interop.Content.Disconnect();
445 if (disconnectResult != MediaContentError.None)
447 Log.Warn(nameof(MediaDatabase), $"Failed to disconnect {disconnectResult.ToString()}.");
456 /// Releases all the resources.
458 public void Dispose()
464 /// Gets the value indicating whether the database has been disposed of.
466 /// <value>true if the database has been disposed of; otherwise, false.</value>
467 public bool IsDisposed => _disposed;