diff options
Diffstat (limited to 'Xamarin.Forms.Core/UriImageSource.cs')
-rw-r--r-- | Xamarin.Forms.Core/UriImageSource.cs | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/Xamarin.Forms.Core/UriImageSource.cs b/Xamarin.Forms.Core/UriImageSource.cs new file mode 100644 index 00000000..f6b3ead1 --- /dev/null +++ b/Xamarin.Forms.Core/UriImageSource.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Forms +{ + public sealed class UriImageSource : ImageSource + { + internal const string CacheName = "ImageLoaderCache"; + + public static readonly BindableProperty UriProperty = BindableProperty.Create("Uri", typeof(Uri), typeof(UriImageSource), default(Uri), + propertyChanged: (bindable, oldvalue, newvalue) => ((UriImageSource)bindable).OnUriChanged(), validateValue: (bindable, value) => value == null || ((Uri)value).IsAbsoluteUri); + + static readonly IIsolatedStorageFile Store = Device.PlatformServices.GetUserStoreForApplication(); + + static readonly object s_syncHandle = new object(); + static readonly Dictionary<string, LockingSemaphore> s_semaphores = new Dictionary<string, LockingSemaphore>(); + + TimeSpan _cacheValidity = TimeSpan.FromDays(1); + + bool _cachingEnabled = true; + + static UriImageSource() + { + if (!Store.GetDirectoryExistsAsync(CacheName).Result) + Store.CreateDirectoryAsync(CacheName).Wait(); + } + + public TimeSpan CacheValidity + { + get { return _cacheValidity; } + set + { + if (_cacheValidity == value) + return; + + OnPropertyChanging(); + _cacheValidity = value; + OnPropertyChanged(); + } + } + + public bool CachingEnabled + { + get { return _cachingEnabled; } + set + { + if (_cachingEnabled == value) + return; + + OnPropertyChanging(); + _cachingEnabled = value; + OnPropertyChanged(); + } + } + + [TypeConverter(typeof(UriTypeConverter))] + public Uri Uri + { + get { return (Uri)GetValue(UriProperty); } + set { SetValue(UriProperty, value); } + } + + internal async Task<Stream> GetStreamAsync(CancellationToken userToken = default(CancellationToken)) + { + OnLoadingStarted(); + userToken.Register(CancellationTokenSource.Cancel); + Stream stream = null; + try + { + stream = await GetStreamAsync(Uri, CancellationTokenSource.Token); + OnLoadingCompleted(false); + } + catch (OperationCanceledException) + { + OnLoadingCompleted(true); + throw; +#if DEBUG + } + catch (Exception e) + { + Debug.WriteLine(e); + throw; +#endif + } + return stream; + } + + static string GetCacheKey(Uri uri) + { + return Device.PlatformServices.GetMD5Hash(uri.AbsoluteUri); + } + + async Task<bool> GetHasLocallyCachedCopyAsync(string key, bool checkValidity = true) + { + DateTime now = DateTime.UtcNow; + DateTime? lastWriteTime = await GetLastWriteTimeUtcAsync(key).ConfigureAwait(false); + return lastWriteTime.HasValue && now - lastWriteTime.Value < CacheValidity; + } + + static async Task<DateTime?> GetLastWriteTimeUtcAsync(string key) + { + string path = Path.Combine(CacheName, key); + if (!await Store.GetFileExistsAsync(path).ConfigureAwait(false)) + return null; + + return (await Store.GetLastWriteTimeAsync(path).ConfigureAwait(false)).UtcDateTime; + } + + async Task<Stream> GetStreamAsync(Uri uri, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + Stream stream; + if (!CachingEnabled) + { + try + { + stream = await Device.GetStreamAsync(uri, cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + stream = null; + } + } + else + stream = await GetStreamFromCacheAsync(uri, cancellationToken).ConfigureAwait(false); + return stream; + } + + async Task<Stream> GetStreamAsyncUnchecked(string key, Uri uri, CancellationToken cancellationToken) + { + if (await GetHasLocallyCachedCopyAsync(key).ConfigureAwait(false)) + { + var retry = 5; + while (retry >= 0) + { + int backoff; + try + { + Stream result = await Store.OpenFileAsync(Path.Combine(CacheName, key), FileMode.Open, FileAccess.Read).ConfigureAwait(false); + return result; + } + catch (IOException) + { + // iOS seems to not like 2 readers opening the file at the exact same time, back off for random amount of time + backoff = new Random().Next(1, 5); + retry--; + } + + if (backoff > 0) + { + await Task.Delay(backoff); + } + } + return null; + } + + Stream stream; + try + { + stream = await Device.GetStreamAsync(uri, cancellationToken).ConfigureAwait(false); + if (stream == null) + return null; + } + catch (Exception) + { + return null; + } + + Stream writeStream = await Store.OpenFileAsync(Path.Combine(CacheName, key), FileMode.Create, FileAccess.Write).ConfigureAwait(false); + await stream.CopyToAsync(writeStream, 16384, cancellationToken).ConfigureAwait(false); + if (writeStream != null) + writeStream.Dispose(); + + stream.Dispose(); + + return await Store.OpenFileAsync(Path.Combine(CacheName, key), FileMode.Open, FileAccess.Read).ConfigureAwait(false); + } + + async Task<Stream> GetStreamFromCacheAsync(Uri uri, CancellationToken cancellationToken) + { + string key = GetCacheKey(uri); + LockingSemaphore sem; + lock(s_syncHandle) + { + if (s_semaphores.ContainsKey(key)) + sem = s_semaphores[key]; + else + s_semaphores.Add(key, sem = new LockingSemaphore(1)); + } + + try + { + await sem.WaitAsync(cancellationToken); + Stream stream = await GetStreamAsyncUnchecked(key, uri, cancellationToken); + if (stream == null) + { + sem.Release(); + return null; + } + var wrapped = new StreamWrapper(stream); + wrapped.Disposed += (o, e) => sem.Release(); + return wrapped; + } + catch (OperationCanceledException) + { + sem.Release(); + throw; + } + } + + void OnUriChanged() + { + if (CancellationTokenSource != null) + CancellationTokenSource.Cancel(); + OnSourceChanged(); + } + } +}
\ No newline at end of file |