Open multiple images from Gallery with Xamarin.Forms on Android
Tags: csharp, dotnet, xamarin, xamarinforms
Published: 2021-02-26 00:11:30

If you've ever developed for Android, be it native with Java/Kotlin or using a cross-platform framework like Xamarin, you may have noticed the process for getting a file from the user storage (an image from the Gallery in this case) is a bit complicated. You need to launch an `Intent`, then await its `Result` and handle any `ClipData` associated with it.

Fortunately for us Xamarin developers, the Xamarin team and the community have created the [Xamarin Essentials](https://github.com/xamarin/Essentials) library and one of its utilities is the **MediaPicker**, which abstracts from you the logic for taking a picture with the camera or getting an image from the storage. You can read the documentation for the Media picker [here](https://docs.microsoft.com/en-us/xamarin/essentials/media-picker?context=xamarin%2Fandroid&tabs=android).

Unfortunately, the MediaPicker from the Xamarin Essentials only supports to take or load one image, so if you need to get multiple pictures you need to roll your own. In this post, I'll show you how do it. 


## Configuring permissions needed to load files from storage
In order to access the user's storage, you need to tell Android you need the *READ_EXTERNAL_STORAGE* permission. In Xamarin.Android, there are two ways to do so:

1) By adding an attribute in the Android project's **AssemblyInfo.cs**
```csharp
[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)]
```
2) By adding the request in the **AndroidManifest.xml**
```xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
```


## Adding classes for the PhoneMediaPicker service
In order to open the file picker, we need to create an interface in our shared project so we can use Dependency Injection and run the corresponding implementation according to the runtime platform. 

```csharp
public interface IPhoneMediaPicker
{
    Task<IEnumerable<MediaFile>> PickPhotosAsync(string intentTitle);
}
```

Then we create an implementation of this interface in our Android project:

```csharp
public class PhoneMediaPicker : IPhoneMediaPicker
{
}
```
And the class we'll be using as our model containing the image:
```csharp
public class MediaFile
{
    public string MimeType { get; set; }
    public Stream FileStream { get; set; }
}
```



## Implementing the IPhoneMediaPicker
Now, it's time to roll up our shirt's sleeves and getting our hands dirty and start coding the implementation.

### Ensure permission and launch intent
Even though we configured the READ_EXTERNAL_STORAGE permission, since Android 6 (API Level 23) you need the user to approve the permission at runtime. Thus, before launching the intent we're gonna use the Xamarin.Essentials *Permissions* API to show the request.

We're gonna need a constant to know the request is coming from our service and since our service is going to launch an intent and it request will be fulfilled in another thread, we need a way to hold on to the async task while the process is complete. For this, we are going to use a [TaskCompletionSource](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1?view=net-5.0).
Then we are going to add a method for handling the ActivityResult from the MainActivity. 

Then, the code in our IPhoneMediaService should be like this.
```csharp
public class PhoneMediaPicker : IPhoneMediaPicker
{
    /// We use this constant on the MainActivity to know the intent was launched by our MediaPicker 
    private const int MediaPickerRequest = 2001;
    private TaskCompletionSource<IEnumerable<MediaFile>> _completionSource;


      public async Task<IEnumerable<MediaFile>> PickPhotosAsync(string title)
      {
          var results = new List<MediaFile>();
          var permissionStatus = await Permissions.RequestAsync<Permissions.StorageRead>();

          if (permissionStatus == PermissionStatus.Denied)
          {
              return results;
          }

          var intent = new Intent(Intent.ActionPick);
          intent.PutExtra(Intent.ExtraAllowMultiple, true);
          intent.SetType("image/*");

          string pickerTitle = string.IsNullOrWhiteSpace(title) ? "Select pictures" : title;
          var pickerIntent = Intent.CreateChooser(intent, title);
          try
          {
              var intentChooser = Intent.CreateChooser(pickerIntent, pickerTitle);
              _completionSource = new TaskCompletionSource<IEnumerable<MediaFile>>();
              var mainActivity = Platform.CurrentActivity as MainActivity;
              mainActivity.ActivityResult += OnActivityResult;
             mainActivity.StartActivityForResult(intentChooser, RequestMediaPicker);

              return await _completionSource.Task;
          }
          catch (Exception ex)
          {
              return results;
          }
      }

    public static void OnActivityResult(Result resultCode, Intent data)
    {
    }
}
```
### Get Intent result in MainActivity
As we are going to use the **MainActivity** for handling the intent result, we need to override the OnActivityResult method there and call this event that were are going to subscribe to in the PhoneMediaPicker. 

```csharp
public event Action<int, Result, Intent> ActivityResult;

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    base.OnActivityResult(requestCode, resultCode, data);
    if (ActivityResult != null)
    {
        ActivityResult(requestCode,resultCode, data);
    }            
}

```

### Reading ClipData and Uri from the Intent
Once we get the result from the Intent, we need to iterate over the `ClipData` that comes from the Intent (if there are multiple images selected) or load the file from the `Uri` in the `Data` field from the Intent.

```csharp
private void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            if (data != null)
            {
                var fileResults = new Collection<MediaFile>();
                if (resultCode == Result.Ok)
                {
                    ClipData clipData = data.ClipData;
                    if (clipData != null && clipData.ItemCount > 0)
                    {
                        for (int i = 0; i < clipData.ItemCount; i++)
                        {
                            var item = clipData.GetItemAt(i);
                            var result = GetFileFromUri(item.Uri);
                            if (result != null)
                            {
                                fileResults.Add(result);
                            }
                        }
                    }
                    else
                    {
                        Uri fileUri = data.Data;
                        var result = GetFileFromUri(fileUri);
                        if (result != null)
                        {
                            fileResults.Add(result);
                        }
                    }
                }

                _completionSource?.TrySetResult(fileResults);
            }
                var mainActivity = Platform.CurrentActivity as MainActivity;
                mainActivity.ActivityResult -= OnActivityResult;
        }
```

### Read the file from the storage and return the MediaFile
The code for reading a file from the storage in Android is a bit needlessly complex. The information is like a database (MediaStore) that you can query and we need to follow a certain procedure in order to get the file from the Uri, like this:

1) Get the `ContentResolver` from the current activity
2) Make a string projection with the columns (in this case we need the `Id` and `Data` columns from the MediaStore)
3) Call the **Query** method from the ContentResolver and pass the Uri from the file to get a `Cursor`
4) If the Cursor has information, then query the Id column and get its string data (which contains the actual path of the file)
5) If the Cursor doesn't have the information, then we need to try and get the file id from the [DocumentsContract](https://developer.android.com/reference/android/provider/DocumentsContract) and make a new projection with this id
6) Once we have the Id we need to get a new Cursor using the projection and `InternalContentUri` or the `ExternalContentUri` if the former fails
7) Finally, query the data column get and its string Data which should contain the actual file path for you to use


```csharp
private MediaFile GetFileFromLegacyUri(Uri uri)
{
    MediaFile result = null;

    ICursor imageCursor = null;
    try

    {
        var contentResolver = Platform.CurrentActivity.ContentResolver;
        const string idColumn = MediaStore.Images.ImageColumns.Id;
        const string dataColumn = MediaStore.Images.ImageColumns.Data;
        var internalContentUri = MediaStore.Images.Media.InternalContentUri;
        var externalContentUri = MediaStore.Images.Media.ExternalContentUri;

        result = new MediaFile();
        var projection = new string[] {dataColumn};
        imageCursor = contentResolver.Query(uri, null, null, null, null,null);
        if (imageCursor != null)
        {
            imageCursor.MoveToFirst();
            int dataIndex = imageCursor.GetColumnIndex(dataColumn);
            if (dataIndex != -1)
            {
                var mime = contentResolver.GetType(uri);
                var idIndex = imageCursor.GetColumnIndexOrThrow(idColumn);
                var path = imageCursor.GetString(idIndex);

                result.MimeType = mime;
                result.PathUri = new System.Uri(path);
                result.FileStream = System.IO.File.OpenRead(path);
            }
            else
            {
                var documentId = DocumentsContract.GetDocumentId(uri);
                var pictureId = documentId.Contains(":") ? documentId.Split(":")[1] : documentId;
                var whereSelection = idColumn + "=?";
                imageCursor = contentResolver.Query(internalContentUri, projection, whereSelection,
                    new string[] {pictureId}, null,null);
                if (imageCursor.Count == 0)
                {
                    imageCursor = contentResolver.Query(externalContentUri, projection, whereSelection,
                        new string[] {pictureId}, null, null);
                }


                var columnData = imageCursor.GetColumnIndexOrThrow(dataColumn);
                imageCursor.MoveToFirst();
                var path = imageCursor.GetString(columnData);
                result.FileStream = System.IO.File.OpenRead(path);
            }
        }

        return result;
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
    finally
    {
        if (imageCursor != null)
        {
            imageCursor.Close();
            imageCursor.Dispose();
        }
    }
}

```

### Loading the images from the storage on Android 10 (API Level 29) and up
When I was trying to load the images from the storage on my emulator I noticed the code in the previous section doesn't work and and every file I tried to load was causing an exception citing missing permissions even though the Storage permission was approved. 

Most code you can find on the Internet for loading files from the storage is identical to that code above and after a long search I found that from Android 10 and on there is a new policy called [Scoped storage](https://developer.android.com/about/versions/11/privacy/storage) and this causes an exception saying you're not authorized to read the file. The good thing is that there is a simplified way to get a file from the storage once you have the Uri. 
For this we are going to use the **OpenInputStream** method from the `ContentResolver`.

```csharp
private MediaFile GetScopedFileFromUri(Uri uri)
{
    MediaFile result = null;
    try
    {
        var contentResolver = Platform.CurrentActivity.ContentResolver;
        var mime = contentResolver.GetType(uri);
        result = new MediaFile();
        result.MimeType = mime;
        result.FileStream = contentResolver.OpenInputStream(uri);
        return result;
    }

    catch (Exception e)
    {
        Console.WriteLine(e);
        return result;
    }
}

```

Having this in mind we need to add code to get the file when the user has Android 10+ and use our legacy code when not.

```csharp
private MediaFile GetFileFromUri(Uri uri)
{
    if (Android.OS.Build.VERSION.SdkInt >= BuildVersionCodes.P)
    {
        return GetScopedFileFromUri(uri);
    }
    else
    {
        return GetFileFromLegacyUri(uri);
    }
}

```

### Opting-out of ScopedStorage
On Android 10 (API Level 29 or in Android 11 with API Level 29 as target) you have the option to set a flag in the `AndroidManifest.xml` [to opt-out of ScopedStorage](https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage) and use the legacy method for reading files from the storage, but this flag will be ignored when you target API Level 30 and above. 

Although it didn't work when I tested it on the emulator, some people have said that it does work on Android 10. 

```xml
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
     Android 10 or higher. -->
  <application android:requestLegacyExternalStorage="true" ... >
    ...
  </application>
</manifest>
```

## Conclusions

It's been a while since I last published here so I wanted to share my findings in this aspect when I got that unexpected error. The code is a bit verbose due to the nature of Android but once you understand the process it's ezpz. 

I hope this can be useful to you and stay tuned for the next post, where I'll be showing you have to make an [outlined material entry](https://material.io/components/text-fields#interactive-demo) in Xamarin.Forms.

## References
Select Multiple Images and Videos in Xamarin Forms by XamBoy
https://www.xamboy.com/2019/03/12/select-multiple-images-and-videos-in-xamarin-forms/

Select multiple images from the gallery in Xamarin Forms by Daniel Kondrashevich
https://medium.com/swlh/select-multiple-images-from-the-gallery-in-xamarin-forms-df2e037be572

Xamarin.Essentials Media Picker
https://docs.microsoft.com/en-us/xamarin/essentials/media-picker?tabs=android

Storage updated in Android 11
https://developer.android.com/about/versions/11/privacy/storage

Opting out of scoped storage
https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage

Android docs for opening media files
https://developer.android.com/training/data-storage/shared/media#open-file