For anyone interested, here's my full solution for downloading a file from a WebView in MAUI on Android and opening it according to the Content-Disposition
header.
I am downloading files to the public Downloads folder, which allows me to avoid dealing with FileProvider. DownloadManager.GetUriForDownloadedFile()
already returns a content://
URI that can be used by an intent.
MainPage.xaml.cs, which has the WebView:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override void OnHandlerChanged()
{
base.OnHandlerChanged();
#if ANDROID
var androidWebView = WebView.Handler.PlatformView as Android.Webkit.WebView;
// If this is not disabled then download links that open in a new tab won't work
androidWebView.Settings.SetSupportMultipleWindows(false);
// Custom download listener for Android
androidWebView.SetDownloadListener(new Platforms.Android.MyDownloadListener());
#endif
}
}
Platforms\Android\MyDownloadListener.cs:
using Android.App;
using Android.Content;
using Android.Webkit;
using Android.Widget;
using System.Text.RegularExpressions;
public class MyDownloadListener : Java.Lang.Object, IDownloadListener
{
private readonly Regex _fileNameRegex = new("filename\\*?=['\"]?(?:UTF-\\d['\"]*)?([^;\\r\\n\"']*)['\"]?;?", RegexOptions.Compiled);
public void OnDownloadStart(string url, string userAgent, string contentDisposition, string mimetype, long contentLength)
{
if (!TryGetFileNameFromContentDisposition(contentDisposition, out var fileName))
{
// GuessFileName doesn't work well, use it as a fallback
fileName = URLUtil.GuessFileName(url, contentDisposition, mimetype);
}
var text = $"Downloading {fileName}...";
var uri = global::Android.Net.Uri.Parse(url);
var context = Platform.CurrentActivity?.Window?.DecorView.FindViewById(global::Android.Resource.Id.Content)?.RootView?.Context;
try
{
var request = new DownloadManager.Request(uri);
request.SetTitle(fileName);
request.SetDescription(text);
request.SetMimeType(mimetype);
request.SetNotificationVisibility(DownloadVisibility.VisibleNotifyCompleted);
// File should be saved in public downloads so that it can be opened without extra effort
request.SetDestinationInExternalPublicDir(global::Android.OS.Environment.DirectoryDownloads, fileName);
// Cookies have to be copied, otherwise authorized files won't download
var cookie = CookieManager.Instance.GetCookie(url);
request.AddRequestHeader("Cookie", cookie);
var downloadManager = (DownloadManager)Platform.CurrentActivity.GetSystemService(Context.DownloadService);
var downloadId = downloadManager.Enqueue(request);
if (ShouldOpenFile(contentDisposition))
{
// Receiver will open the file after the download has finished
context.RegisterReceiver(new MyBroadcastReceiver(downloadId), new IntentFilter(DownloadManager.ActionDownloadComplete));
}
Toast
.MakeText(
context,
text,
ToastLength.Short)
.Show();
}
catch (Java.Lang.Exception ex)
{
Toast
.MakeText(
context,
$"Unable to download file: {ex.Message}",
ToastLength.Long)
.Show();
}
}
private bool TryGetFileNameFromContentDisposition(string contentDisposition, out string fileName)
{
if (string.IsNullOrEmpty(contentDisposition))
{
fileName = null;
return false;
}
var match = _fileNameRegex.Match(contentDisposition);
if (!match.Success)
{
fileName = null;
return false;
}
// Use first match even though there could be several matched file names
fileName = match.Groups[1].Value;
return true;
}
private bool ShouldOpenFile(string contentDisposition)
{
if (string.IsNullOrEmpty(contentDisposition))
{
return false;
}
return contentDisposition.StartsWith("inline", StringComparison.InvariantCultureIgnoreCase);
}
}
Platforms\Android\MyBroadcastReceiver.cs:
using Android.App;
using Android.Content;
using Android.Widget;
public class MyBroadcastReceiver : BroadcastReceiver
{
private readonly long _downloadId;
public MyBroadcastReceiver(long downloadId)
{
_downloadId = downloadId;
}
public override void OnReceive(Context context, Intent intent)
{
// Only handle download broadcasts
if (intent.Action == DownloadManager.ActionDownloadComplete)
{
var downloadId = intent.GetLongExtra(DownloadManager.ExtraDownloadId, 0);
// Only handle specific download ID
if (downloadId == _downloadId)
{
OpenFile(context, downloadId);
context.UnregisterReceiver(this);
}
}
}
private void OpenFile(Context context, long downloadId)
{
var downloadManager = (DownloadManager)context.GetSystemService(Context.DownloadService);
var fileUri = downloadManager.GetUriForDownloadedFile(downloadId);
var fileMimeType = downloadManager.GetMimeTypeForDownloadedFile(downloadId);
if (fileUri == null || fileMimeType == null)
{
return;
}
var viewFileIntent = new Intent(Intent.ActionView);
viewFileIntent.SetDataAndType(fileUri, fileMimeType);
viewFileIntent.SetFlags(ActivityFlags.GrantReadUriPermission);
viewFileIntent.AddFlags(ActivityFlags.NewTask);
try
{
context.StartActivity(viewFileIntent);
}
catch (Java.Lang.Exception ex)
{
Toast
.MakeText(
context,
$"Unable to open file: {ex.Message}",
ToastLength.Long)
.Show();
}
}
}