While building Basemix I’ve struggled to break the back of a quite fundamental problem I’ve had in using MAUI on Android. Let me drop the problem context first:

Problem Context

Users need to be able to save files from Basemix to user-controlled storage. Today this is used for making manual database backups and creating and saving PDF pedigrees, but long-term could be used for exporting and sharing reports or individual items for sharing.

Given that phones have quite strict models around app data, and they abstract the file system away from the user a bit, some special provisions may need making to provide this functionality on these platforms.

Immediate implementation

Oddly enough, MAUI comes with a FilePicker but no file saver which felt immediately like a short-sighted and curious omission to me. Thanks to the wonderful folks at CommunityToolkit implemented a FileSaver that works on all platforms, which I’ve used happily and blissfully ignorantly until…

Android 13, API 33

Android 13 made some changes for security, basically limiting the access any app has to the filesystem. Really good stuff, you’d think, unless you were the kind of person to punish yourself by building a free app in your free time, and you also happen to have zero Android app development experience, and you’re quite new to MAUI also, and you’ve also settled on the opinion that MAUI is a bit immature anyway. Wonder who that could be.

To wind back a bit, the app storage model on Android is as follows: Each application has its own internal and protected app data directory which it can freely use for storage and caching and whatever else it needs. This directory is not directly accessible externally, and apps cannot reach “out” of that directory without special permissions to manipulate external storage. The only interface you have to this storage is via the “Clear app data” or “Clear app cache” buttons in the applications settings menu, and when you uninstall an app it deletes this directory too. All a bit too ephemeral for my liking.

These changes nerf the READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions that I relied on for FileSaver to work. Android 13 and above now expects you to request access to one of a particular type of media directory (Images, Videos or Audio. No generic “documents” or user directory. Drat). You can still request external storage access with MANAGE_EXTERNAL_STORAGE which Google considers a high-risk permission and you basically have to really justify that it is essential to the core functionality of your app (for example, you are building a file manager), not really the case for a rat breeding app. The other option Google suggests is to use the Storage Access Framework - all of the Android documentation seems really good until you get to whatever abstractions MAUI has put over the top. I haven’t been able to really locate how or where this is implemented in MAUI yet, I get the impression Microsoft isn’t doing a good job linking their framework docs to the actual platform specifics anywhere, which makes it hard to google. You can find one kind soul who has implemented something so far but it’s unclear to me if there’s a “MAUI idiomatic way” to use it.

I found all of this out when some users simply told me “Saving a PDF doesn’t work. The app says it is successful but nothing happens”, and I was blissfully on Android 12 at the time.

The workaround

A fair few issues cropped up on the community toolkit to point this out to the maintainers. When you invoke the FileSaver, it should open up a just-in-time dialog requesting app permissions from the user, then allow the save. What actually happened was… Nothing. No error, no user prompt, it simply did not work. The maintainers were pretty satisfied that, actually, FileSaver works just fine and MAUI has an issue where it is not requesting the new Android 13 permissions for specific media types. I waited a while, updated my framework, and still didn’t really have anything usable.

If you’ve opened any of those issues up, you might see what the community concluded (and as far as I can tell, is still satisfied with) as a valid workaround: manually instructing android to target a pre-33 version of the SDK so that you can use the old permissions still. This requires a line in the android manifest along the lines of:

<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />

That’s dicey, and clearly going to be very short lived. Maybe I’m reading too much into text on the internet but a lot of people seemed very happy to take that, log off and run with it.

Anyway, as a hobbyist app developer, I was happy to take that, log off, and run with it. For now. I didn’t want to keep failing my users on Android 13 (and since Basemix thrives on user feedback, I figured it was important to make it “actually work” for users so that they can give feedback) and I had exhausted my googling for any other kind of simple solution. Again, it’s not clear to me if this is user (my) error or if the documentation, community chatter and overall neglect around MAUI is to blame. We’ll never know, and if it is my fault I’ll conveniently forget to edit this post and clarify that.

I did that on May 18th, and that’s sort of where Basemix sat for a fair few months, like, until now (October). Until the inevitable. Google pushed a warning that I needed to update my targeted SDK soon, or face being unable to push any more updates.

The Solution

Months had passed, a great time to see if anyone’s gone back and documented the “Non Workaround Fix”. Astoundingly there were a few more issues raised, and quite a lot more people gleefully echoing “the workaround”. My google searching for “MAUI Android Storage Access Framework” still wasn’t super fruitful (read: no easy solution) and I was starting to despair, until I caught one innocuous comment on one of the issues:

My only solution at the moment would be to use the Maui share instead of FileSaver

What is “Maui share” and why, github user baaaaif, are you the only one mentioning it?

https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/data/share?tabs=android#share-a-file

Oh, thanks for that link baaaaif.

It turns out, this is perfect for what I want to achieve. Instead of writing a stream directly to a user-chosen destination, now I must write the file to application storage and invoke a “Share” request with that file in order to share the file with a user-chosen app. This means users can directly attach a PDF to an email, or upload the database straight to a folder in google drive, or even download a super risky file manager and just save it wherever the heck they like again. It’s a little bit more legwork but still not awful, and arguably it’s even more convenient for some use cases. Here’s how I backup the database:

await using var file = new FileStream(dbPath, FileMode.Open);
#if ANDROID
var tempFilePath = Path.Combine(FileSystem.Current.CacheDirectory, "basemix.sqlite3");
await using var tempFile = new FileStream(tempFilePath, FileMode.Create);
await file.CopyToAsync(tempFile);
await Share.Default.RequestAsync(new ShareFileRequest
{
    Title = "Save database backup",
    File = new ShareFile(tempFilePath)
});
#else
await FileSaver.Default.SaveAsync("basemix.sqlite3", file, cts.Token);
#endif

And the PDF generation really isn’t much different:

#if ANDROID
var tempFilePath = Path.Combine(FileSystem.Current.CacheDirectory, $"{this.Rat.Name}.pdf");
await using var tempFile = new FileStream(tempFilePath, FileMode.Create);
this.PdfGenerator.WriteToStream(pdf, tempFile);
await Share.Default.RequestAsync(new ShareFileRequest
{
    Title = "Save pedigree",
    File = new ShareFile(tempFilePath)
});
#else
var stream = new MemoryStream();
this.PdfGenerator.WriteToStream(pdf, stream);
await FileSaver.Default.SaveAsync($"{this.Rat.Name}.pdf", stream, CancellationToken.None);
#endif

I want to cleanup the #if ANDROID stuff and implement some neater “Inject A Platform-Specific Strategy” kind of approach to this but I’m more than happy it hasn’t turned out to be A Whole Thing.

Irritatingly, this solution has definitely sat under my nose for ages and it has just felt impenetrable to try to find a “How to save a file on Android in MAUI: tips and best practices” style commentary on this, so I’ve written one. Sort of. Perhaps if I’d sat down and ingested a lot more MAUI documentation before diving in I’d have some idea of other tools at my disposal, but really I’m just going to ignorantly blame “discoverability” and leave it at that while I move onto adding more features and fixing somewhat less upsetting single-platform problems.

The immaturity of MAUI aside, it has so far not been a dead end for me.