DEV Community

Jefry Pozo
Jefry Pozo

Posted on • Edited on

Open multiple images from Gallery with Xamarin.Forms on Android

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 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.

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

[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)]
Enter fullscreen mode Exit fullscreen mode

2) By adding the request in the AndroidManifest.xml

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Enter fullscreen mode Exit fullscreen mode

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.

public interface IPhoneMediaPicker
{
    Task<IEnumerable<MediaFile>> PickPhotosAsync(string intentTitle);
}
Enter fullscreen mode Exit fullscreen mode

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

public class PhoneMediaPicker : IPhoneMediaPicker
{
}
Enter fullscreen mode Exit fullscreen mode

And the class we'll be using as our model containing the image:

public class MediaFile
{
    public string MimeType { get; set; }
    public Stream FileStream { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

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.
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.

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)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }            
}

Enter fullscreen mode Exit fullscreen mode

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.

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;
        }
Enter fullscreen mode Exit fullscreen mode

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 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

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();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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 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.

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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.

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

Enter fullscreen mode Exit fullscreen mode

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 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.

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

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 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

Top comments (0)