Download files in MAUI Android WebView

1.3k Views Asked by At

I have a WebView in a MAUI app that generally works, but whenever I click a link on Android that is supposed to download a file (link returns a Content-Disposition header) nothing happens.

How is this supposed to be implemented? I can't find any documentation.

<WebView
    x:Name="WebView"
    Source=".." />
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    protected override bool OnBackButtonPressed()
    {
        base.OnBackButtonPressed();

        if (WebView.CanGoBack)
        {
            WebView.GoBack();
            return true;
        }
        else
        {
            base.OnBackButtonPressed();
            return true;
        }
    }
}

Related question for iOS: Download files in MAUI iOS WebView

2

There are 2 best solutions below

4
On BEST ANSWER

For the android, you can try to add a DownloadListener to the webview. I have testd it and the file can download successfully.

Create the custom downloadlistener class in the \Platforms\Android:

    public class MyDownLoadListener : Java.Lang.Object, IDownloadListener
    {
        public void OnDownloadStart(string url, string userAgent, string contentDisposition, string mimetype, long contentLength)
        {
            var manager = (DownloadManager)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity.GetSystemService(global::Android.App.Application.DownloadService);
            var uri = global::Android.Net.Uri.Parse(url);
            DownloadManager.Request downloadrequest = new DownloadManager.Request(uri);
            downloadrequest.SetNotificationVisibility(DownloadVisibility.VisibleNotifyCompleted);
            manager.Enqueue(downloadrequest);
        }
    }

In the page's xaml:

 <WebView x:Name="webview" Source="https://www.myfreemp3.com.cn/" HeightRequest="500"/>

And set the listener for the webview in the Page.cs:

   protected override void OnHandlerChanged()
    {
        base.OnHandlerChanged();
#if ANDROID
        (webview.Handler.PlatformView as Android.Webkit.WebView).SetDownloadListener(new Platforms.Android.MyDownLoadListener());
        (webview.Handler.PlatformView as Android.Webkit.WebView).Settings.JavaScriptEnabled = true;
        (webview.Handler.PlatformView as Android.Webkit.WebView).Settings.DomStorageEnabled = true;
#endif
    }
4
On

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