Quantcast
Channel: Xamarin.Forms — Xamarin Community Forums
Viewing all articles
Browse latest Browse all 58056

Image downloader...cause...android

$
0
0

I hit an interesting ... feature... today. Images from a social media site would not display in android. They displayed in iOS just fine. After much head scratching and forum spelunking I noticed that there were a smattering of reports of android images not displaying. A bit more testing and I noticed that most images would in fact download, but the ones from the social media simply would not. I suspect it has to do with either the fact they are https rather than http or the presence of some odd characters in the uri... ex:
https://lh4.googleusercontent.com/-xe__6tIhwRI/AAAAAAAAAAI/AAAAAAAAGIc/6OZRKpu_Pg0/photo.jpg (Yes that is a terrible photo...)

Anyways since my app is going to rely on uris such as these I needed a quick solution. I had a number of goals:
1) PCL so as to not need multiple implementations.
2) No slower than the current built in image downloader, preferably faster as I have situations where I will need to download 500+ images.
3) Not a battery drain.
4) Simple code at the viewmodel level.

What I ended up with was a multithreaded downloader based on a BlockingCollection that communicated it's results via Observables. For those who prefer the Task.ContinueWith model it would be fairly easy to convert this code to that model instead.

Here is how to use it:
Somewhere, before you start downloading you need to call Start.
ImageService.Start();
I call it in the application ctor, there is a corresponding Stop method as well.

In your viewmodel define a ImageSource property and then set it from the result of the ImageService:

    public class HomeViewModel : BaseViewModel
    {
        private readonly IMobileServiceClient _client;
        private string _name;

        private ImageSource _imageuri;

        public HomeViewModel(IMobileServiceClient client)
        {
            _client = client;
            IsBusy = true;
            //ImageUri = "question.png";
            this.LoadUserInfo();
        }

        public string Name { get { return _name; } set { this.SetField(ref _name, value); } }
        public ImageSource ImageSource { get { return _imageuri; } set { this.SetField(ref _imageuri, value); } }

        internal async void LoadUserInfo()
        {
            try
            {
                var userInfo = await _client.InvokeApiAsync<ClientDto>("userInfo", HttpMethod.Get, null);
                Name = userInfo.Name;
                ImageService.Fetch(userInfo.ImageUri).SubscribeOn(SynchronizationContext.Current).Subscribe(x => { this.ImageSource = x.Source; });
            }
            catch (Exception ex)
            {
                //Your error logging goes here
                throw;
            }
            finally
            {
                IsBusy = false;
            }
        }
    }

That is about as simple as I could make it...
The 'x' above has two properties Source (an ImageSource) and OriginatingUri. The uri is passed back as you can add an IEnumerable to the ImageService and still get a single Observable back..this allows for fast processing of batchs.

Last but not least here is the ImageService code..comment, critique or rotten vegetables always welcome.

 using System.Collections.Concurrent;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Net;
    using System.Reactive.Linq;
    using System.Reactive.Subjects;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Autofac;
    using Xamarin.Forms;


    /// <summary>
    /// Multi threaded image downloader...because Xamarin forms and Android Images...
    /// </summary>
    /// Element created at 21/04/2015,11:18 PM by Charles
    static class ImageService 
    {
        /// <summary>
        /// The ImageRequest class handles the actual mechanics of downloading an image from the web
        /// </summary>
        /// Element created at 21/04/2015,11:21 PM by Charles
        private class ImageRequest
        {

            /// <summary>
            /// Initializes a new instance of the <see cref="ImageRequest"/> class.
            /// </summary>
            /// <param name="uri">The images URI.</param>
            /// Element created at 21/04/2015,11:21 PM by Charles
            public ImageRequest(string uri)
            {
                Uri = uri;
                TryCount = 0;
            }

            /// <summary>Fetches the image and returns a byte array.</summary>
            /// <returns>A byte[]containing the downloaded image.</returns>
            /// Element created at 21/04/2015,11:21 PM by Charles
            public async Task<byte[]> Fetch()
            {
                TryCount++;
                try
                {
                    var request = (HttpWebRequest)WebRequest.Create(Uri);
                    var response = (HttpWebResponse)(await request.GetResponseAsync());

                    // Check that the remote file was found. 
                    // Occasional mendacious sites return OK with a 404.....black hearts
                    if ((response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Moved || response.StatusCode == HttpStatusCode.Redirect) &&
                        response.ContentType.StartsWith("image", StringComparison.OrdinalIgnoreCase))
                    {

                        // if the remote file was found, download it
                        using (var inputStream = response.GetResponseStream())
                        using (var outputStream = new MemoryStream())
                        {
                            var buffer = new byte[4096];
                            int bytesRead;
                            do
                            {
                                bytesRead = inputStream.Read(buffer, 0, buffer.Length);
                                outputStream.Write(buffer, 0, bytesRead);
                            } while (bytesRead != 0);
                            return outputStream.ToArray();
                        }
                    }
                }
                catch (Exception ex)
                {
                   //Your error logging goes here
                }
                return null;
            }

            /// <summary>Gets the IObservable for the consumer.</summary>
            /// <value>The IObservable that will be signaleed when the image is downloaded.  If the image cannot be downloaded then OnError is called.</value>
            /// Element created at 21/04/2015,11:22 PM by Charles
            public IObservable<ImageResult> Source { get { return _subject; } }

            internal void SendSource(ImageResult src)
            {
                _subject.OnNext(src);
                _subject.OnCompleted();
            }

            internal void SendError()
            {
                _subject.OnError(new Exception("MaxRetry count ("+RetryCount+") exceeded."));
            }

            /// <summary>The Observable for this image request</summary>
            /// Element created at 21/04/2015,11:24 PM by Charles
            private readonly Subject<ImageResult> _subject = new Subject<ImageResult>();

            /// <summary>Gets or sets the try count.</summary>
            /// <value>The try count.</value>
            /// Element created at 21/04/2015,11:24 PM by Charles
            internal int TryCount { get; private set; }

            /// <summary>Gets or sets the URI.</summary>
            /// <value>The URI.</value>
            /// Element created at 21/04/2015,11:24 PM by Charles
            internal string Uri { get;  private set; }
        }


        private readonly static BlockingCollection<ImageRequest> FetchQueue = new BlockingCollection<ImageRequest>(new ConcurrentQueue<ImageRequest>());

        private const int MaxConcurrencyAllowed = 4;
        private const int RetryCount = 3;
        private static int maxconcurrency=2;
        private static Task[] tasks;
        private static CancellationTokenSource cancellationSource;
        private static bool startstop;

        /// <summary>Gets or sets the maximum concurrency.</summary>
        /// <value>The maximum concurrency.</value>
        /// Element created at 21/04/2015,11:25 PM by Charles
        public static int MaxConcurrency 
        { 
            get { return maxconcurrency; }
            set{ Contract.Assert(value <= MaxConcurrencyAllowed,string.Format("More than {0} concurrent downloads is...perhaps not a good idea.",MaxConcurrencyAllowed));maxconcurrency = value;} 
        }

        /// <summary>Starts this instance.</summary>
        /// Element created at 21/04/2015,11:25 PM by Charles
        public static void Start()
        {
            if (tasks != null || startstop) return;
            startstop = true;
            tasks = new Task[maxconcurrency];
            cancellationSource = new CancellationTokenSource();
            var token = cancellationSource.Token;
            for (var i = 0; i < maxconcurrency; i++)
            {
                tasks[i] = Task.Run(() => Fetcher(token), token);
            }
            startstop = false;
        }

        /// <summary>Stops this instance.</summary>
        /// Element created at 21/04/2015,11:25 PM by Charles
        /// <exception cref="System.AggregateException"></exception>
        public static void Stop()
        {
            if (tasks == null || startstop) return;
            startstop = true;
            try
            {
                cancellationSource.Cancel();
                Task.WaitAll(tasks);
                tasks = null;
                cancellationSource = null;
            }
            catch (AggregateException aex)
            {
                var real = aex.InnerExceptions.Where(ex => !(ex is TaskCanceledException)).ToList();
                if (real.Any()) throw new AggregateException(real);
            }
            finally
            {
                startstop = false;
            }
        }

        /// <summary>Adds a series of Uris to the fetch queue.</summary>
        /// <param name="uris">The uris.</param>
        /// <returns></returns>
        /// Element created at 21/04/2015,11:26 PM by Charles
        internal static IObservable<ImageResult> Fetch(IEnumerable<string> uris)
        {
            var ret = new List<IObservable<ImageResult>>();
            ret.AddRange(uris.Select(Fetch));
            return ret.Merge();
        }
        internal static IObservable<ImageResult> Fetch(string uri)
        {
            if (cancellationSource == null) throw new InvalidOperationException("ImageService must be started before adding items");
            var request = new ImageRequest(uri);
            FetchQueue.Add(request,cancellationSource.Token);
            return request.Source;
        }


        private static async void Fetcher(CancellationToken token)
        {
            foreach (var imageRequest in FetchQueue.GetConsumingEnumerable(token))
            {
                if(token.IsCancellationRequested)
                    token.ThrowIfCancellationRequested();

                var imagebytes = await imageRequest.Fetch();
                if (imagebytes != null && imagebytes.Length > 0)
                {
                    var ms = new MemoryStream(imagebytes);
                    imageRequest.SendSource(new ImageResult{Source = ImageSource.FromStream(() => ms),OrginatingUri=imageRequest.Uri});                   
                }
                else
                {
                    if (imageRequest.TryCount <= RetryCount)
                    {
                        FetchQueue.Add(imageRequest, token);
                    }
                    else
                    {
                        imageRequest.SendError();
                    }
                }

            }

        }
    }


    public class ImageResult
    {
        public ImageSource Source { get; set; }
        public string OrginatingUri { get; set; }
    }

Viewing all articles
Browse latest Browse all 58056

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>