0

I follow this blog to implement downloading image from URL and saving it to device.

My code:

IDownloader.cs

public interface IDownloader
{
    void DownloadFile(string url, string folder);
    event EventHandler<DownloadEventArgs> OnFileDownloaded;
}
public class DownloadEventArgs : EventArgs
{
    public bool FileSaved = false;
    public DownloadEventArgs(bool fileSaved)
    {
        FileSaved = fileSaved;
    }
}

AndroidDownloader.cs

[assembly: Dependency(typeof(AndroidDownloader))]
namespace MyApp.Droid.Renderer
{
    public class AndroidDownloader : IDownloader
    {
        public event EventHandler<DownloadEventArgs> OnFileDownloaded;

        public void DownloadFile(string url, string folder)
        {
            string pathToNewFolder = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, folder);
            Directory.CreateDirectory(pathToNewFolder);

            try
            {
                WebClient webClient = new WebClient();
                webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
                string pathToNewFile = Path.Combine(pathToNewFolder, Path.GetFileName(url));
                webClient.DownloadFileAsync(new Uri(url), pathToNewFile);
            }
            catch (Exception ex)
            {
                if (OnFileDownloaded != null)
                    OnFileDownloaded.Invoke(this, new DownloadEventArgs(false));
            }
        }

        private void Completed(object sender, AsyncCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                if (OnFileDownloaded != null)
                    OnFileDownloaded.Invoke(this, new DownloadEventArgs(false));
            }
            else
            {
                if (OnFileDownloaded != null)
                    OnFileDownloaded.Invoke(this, new DownloadEventArgs(true));
            }
        }
    }
}

On my xaml.cs (I have added permission check on LIVE running)

IDownloader downloader = DependencyService.Get<IDownloader>();
    
private async void DownloadImage(object sender, EventArgs e)
{
    try
    {
        var status = await CrossPermissions.Current.CheckPermissionStatusAsync<StoragePermission>();
        if (status != PermissionStatus.Granted)
        {
            if (await CrossPermissions.Current.ShouldShowRequestPermissionRationaleAsync(Plugin.Permissions.Abstractions.Permission.Storage))
            {
                await DisplayAlert("Storage permission", "Need storage permision to download images.", "OK");
            }
            status = await CrossPermissions.Current.RequestPermissionAsync<StoragePermission>();
        }

        if (status == PermissionStatus.Granted)
        {
            downloader.DownloadFile("http://www.dada-data.net/uploads/image/hausmann_abcd.jpg", "My App");
        }
        else if (status != PermissionStatus.Unknown)
        {
            //location denied
        }
    }
    catch (Exception ex)
    {
        //Something went wrong
    }
}

private async void OnFileDownloaded(object sender, DownloadEventArgs e)
{
    await PopupNavigation.Instance.PopAsync();
    if (e.FileSaved)
    {
        UserDialogs.Instance.Toast("The image saved Successfully.");
    }
    else
    {
        UserDialogs.Instance.Toast("Error while saving the image.");
    }
}

IosDownloader.cs

[assembly: Dependency(typeof(IosDownloader))]
namespace MyApp.iOS.Renderer
{
    public class IosDownloader : IDownloader
    {
        public event EventHandler<DownloadEventArgs> OnFileDownloaded;

        public void DownloadFile(string url, string folder)
        {
            string pathToNewFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), folder);
            Directory.CreateDirectory(pathToNewFolder);

            try
            {
                WebClient webClient = new WebClient();
                webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
                string pathToNewFile = Path.Combine(pathToNewFolder, Path.GetFileName(url));
                webClient.DownloadFileAsync(new Uri(url), pathToNewFile);
            }
            catch (Exception ex)
            {
                if (OnFileDownloaded != null)
                    OnFileDownloaded.Invoke(this, new DownloadEventArgs(false));
            }
        }

        private void Completed(object sender, AsyncCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                if (OnFileDownloaded != null)
                    OnFileDownloaded.Invoke(this, new DownloadEventArgs(false));
            }
            else
            {
                if (OnFileDownloaded != null)
                    OnFileDownloaded.Invoke(this, new DownloadEventArgs(true));
            }
        }
    }
}

I am facing below issues:

  1. It is working on android 10 and not working on android 12. Should I add anything else for the proper working on android 12 or above?
  2. Not working on the ios platform. Added the permissions on info.plist.

UPDATE-20230205

I need to view the downloaded images on the device gallery and file manager applications.

Sreejith Sree
  • 3,055
  • 4
  • 36
  • 105
  • 1
    "not working" is not a helpful description of the problem – Jason May 01 '23 at 15:31
  • First of all, please explain your goal. What do you want to do with the downloaded file, does it need to be publicly available or is it sufficient that only your app can access it? Answering this will already help with finding the easiest solution to your requirement. There might be an easier way to do it. Then, what is not working? Do you see any exceptions? Please elaborate. One thing I noticed is that you don't check whether the directory already exists. Calling `Directory.CreateDirectory()` on an existing directory will fail with an exception, AFAIK. – Julian May 01 '23 at 16:26
  • You're also not awaiting the `DownloadFileAsync()` method, which means that your `try-catch`-block around it will not catch any exceptions, because it will run out of scope before the download task finishes or throws. – Julian May 01 '23 at 16:30
  • As a general note, I think you're making things too complicated. The only real difference between your platform implementations is the platform-specific download path. You could therefore create a platform-independent `IDownloader` implementation and only pass in the platform-specific path. – Julian May 01 '23 at 16:43

1 Answers1

0

Permissions

If it is sufficient for you to download files to the app's private data storage (inaccessible to other apps), you won't need to request any special permissions from the user, AFAIK.

However, if for some reason, you do need to ask permission because of the storage location, then you should read about Xamarin.Essentials: Permissions as well as Android 13.0 specific READ/WRITE permissions.

IDownloader

One problem I noticed is that you don't await the asynchronous download. This is because the WebClient.DownloadFileAsync() method has an async void signature. Not awaiting the download means that your try-catch-block will run out of scope before the DownloadFileAsync() method has finished downloading the file. This also means that the catch()-clause won't catch any exceptions thrown by that method. However, there is also another method which you can use, called DownloadFileTaskAsync(), which provides an awaitable Task instead.

Another issue is that you're not checking whether the directory you want to write to already exists or not. The Directory.CreateDirectory() method will throw an exception if the directory already exists. Therefore, you should check for the existence of the directory before attempting to create it.

You can simplify your IDownloader implementation if you make a few changes to the interface and create a single, platform-independent implementation in the shared project. Then, you can use Xamarin.Essentials.FileSystem.AppDataDirectory to access the platform-specific app data folder, without the need of using the Dependency Service. This way, you can get rid of the Android and iOS specific implementations.

Updated Interface

Update your interface to use a Task instead of a void method:

using System.Threading.Tasks;

public interface IDownloader
{
    Task DownloadFileAsync(string url, string folder);
}

Technically, you only need this interface, if you're using some form of Inversion of Control (IoC) (e.g. if you're also writing unit tests and this code is a dependency of some other class).

Shared Implementation

The new shared implementation could look something like this using the platform-specific, private app data directory on each platform:

using System.Threading.Tasks;

public class Downloader: IDownloader
{
    public async Task DownloadFileAsync(string url, string folder)
    {
        try
        {
            // get the platform-specific app data directory
            string pathToNewFolder = Path.Combine(Xamarin.Essentials.FileSystem.AppDataDirectory, folder);

            if (!Directory.Exists(pathToNewFolder ))
            {
                Directory.CreateDirectory(pathToNewFolder );
            }

            WebClient webClient = new WebClient();
            string pathToNewFile = Path.Combine(pathToNewFolder, Path.GetFileName(url));
            // Important: await the download in order to ensure that exceptions are caught 
            await webClient.DownloadFileTaskAsync(new Uri(url), pathToNewFile);           
        }
        catch (Exception ex) 
        {
            // this may provide more information on what may be wrong
            Console.WriteLine(ex);
        }
    }
}

You can call this inside an asynchronous method as follows then (example without IoC):

var pathToFile = "some/valid/path.png";
var somePath = "some/path/under/appdata";
var downloader = new Downloader();
await downloader.DownloadFileAsync(pathToFile, somePath);

Note: I've skipped the part about the OnFileDownloaded event, which you can, of course, still use anyway. For example, this may be useful, if you do not want to await the download (e.g. by simply calling downloader.DownloadFileAsync(pathToFile, somePath); without the await, which I don't recommend).


Platform-specific Download location

If you cannot use the app's private data directory need to access the "Download" folder, you can also implement a separate interface to obtain the platform-specific directory.

Interface

Define some IFolderProvider interface (or similar) in your shared project:

public interface IFolderProvider
{
    string GetExternalStorageLocation();
}

Then create platform-specific implementations.

Android

[assembly: Dependency(typeof(FolderProvider))]
namespace MyApp.Droid.FileSystem
{
    public class FolderProvider : IFolderProvider
    {
        public string GetExternalStorageLocation()
        {
            try
            {
                return Path.Combine(Android.OS.Environment.ExternalStorageDirectory?.AbsolutePath ?? string.Empty, Android.OS.Environment.DirectoryDownloads ?? string.Empty);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return string.Empty;
            }
        }
    }
}

iOS

[assembly: Dependency(typeof(FolderProvider))]
namespace MyApp.iOS.FileSystem
{
    public class FolderProvider : IFolderProvider
    {
        public string GetExternalStorageLocation()
        {
            return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        }
    }
}

Usage

In your Downloader implementation you could then replace this line:

string pathToNewFolder = Path.Combine(Xamarin.Essentials.FileSystem.AppDataDirectory, folder);

with:

var downloadFolder = DependencyService.Get<IFolderProvider>().GetExternalStorageLocation();
var pathToNewFolder = Path.Combine(downloadFolder, folder);

Disclaimer: You may have to request platform-specific permissions based on the location you choose for the download. The locations I chose here should work without special permissions, but I give no guarantee for it and these things also keep changing all the time on Android and iOS.

Julian
  • 5,290
  • 1
  • 17
  • 40
  • I tried this on my side. Updated the interface and added a new class on shared project for downloading image. Then called the download image class from my page. The code is running without any exceptions or errors, but the image is not downloading. The image is not showing on file manager or gallery. Even the folder is also not created on file manager. If I create a sample project with the latest codes, could you please check? – Sreejith Sree May 02 '23 at 14:08
  • How do you know that the file isn't being downloaded? Have you checked that in the debugger? Like I wrote in the beginning of my answer, this solution only works if other apps don't need access to the file. Any gallery and file manager apps won't have access, either. That's only possible when you download to a different location using the appropriate OS-specific permissions. **Please clarify how you want to use the picture *(in the question, not here in the comments)*.** – Julian May 02 '23 at 14:12
  • I have created a sample project with this implementation. Could you please check and let me know if I miss anything? https://drive.google.com/file/d/1jXyeHH4OCtdfSkXs_8juVdCKdFC6Ijq8/view?usp=share_link – Sreejith Sree May 02 '23 at 15:14
  • Updated the question: UPDATE-20230205 I need to view the downloaded images on the device gallery and file manager applications. – Sreejith Sree May 02 '23 at 15:18
  • Then you cannot use the `Xamarin.Essentials.FileSystem.AppDataDirectory` location from my answer and instead need to use a different folder on each platform. There are restrictions on this for each platform, so you'll need to check which permissions you need and which locations you can write to. – Julian May 02 '23 at 15:21
  • @SreejithSree I've updated the answer with a platform-specific publicly accessible download location. – Julian May 02 '23 at 16:37
  • I tried the updated solution and it is working on android devices and able to view the image on filemanager. But on ios part still the image is not showing on filemanager or albums. – Sreejith Sree May 04 '23 at 15:36
  • @SreejithSree You can add `LSSupportsOpeningDocumentsInPlace` to Info.plist and then set it to true. For more info, you can check this: [Sharing with the Files app](https://learn.microsoft.com/en-us/xamarin/ios/app-fundamentals/file-system#sharing-with-the-files-app). – Jianwei Sun - MSFT May 09 '23 at 09:56