My app is a wrapper for a Xamarin.Forms.WebView through which users browse my web site.
On the site users can upload files of effectively any type, and users are also able to download those files.
The URLs for downloading each file differs depending on the area the file was uploaded to, but the server will add the following header to the response:
"content-disposition: attachment; filename="filename"
When using the Xamarin.Forms.WebView to navigate to a download URL, I'm getting frustrating results.
Take an example of two files: dog.jpg and owl.txt
In Android, navigating to the url for either file does absolutely nothing.
In iOS, navigating to the url for dog.jpg opens it in the WebView (and prevents the user from navigating away). The owl.txt link does nothing.
In desktop browsers (Chrome, Safari, IE) both links will save the file to the desktop (depending on settings it will let me choose the location)
Following literally hours of Google-fu, I tried to extend Xamarin.Forms.WebView with an abstract class, then use dependency injection to create a platform-specific implementation so I could add a custom DownloadListender on Android.
However, Xamarin.Forms.WebView does not have a SetDownloadListener method (or equivalent), and I couldn't cast it as an Android.Webkit.WebView, so that solution was a dead-end.
Next, I tried to create a custom WebViewRenderer to essentially do what I described above:
using Android.App;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Android.Webkit;
using Orchidnet.Mobile.App.Droid;
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(CustomWebViewRenderer))]
namespace Orchidnet.Mobile.App.Droid
{
public class CustomWebViewRenderer : ViewRenderer<Xamarin.Forms.WebView, global::Android.Webkit.WebView>
{
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (this.Control == null)
{
var webView = new global::Android.Webkit.WebView(this.Context);
webView.SetWebViewClient(new WebViewClient());
webView.SetWebChromeClient(new WebChromeClient());
webView.SetDownloadListener(new CustomDownloadListener());
this.SetNativeControl(webView);
var source = e.NewElement.Source as UrlWebViewSource;
if (source != null)
{
webView.LoadUrl(source.Url);
}
}
}
}
public class CustomDownloadListener : Java.Lang.Object, IDownloadListener
{
public void OnDownloadStart(string url, string userAgent, string contentDisposition, string mimetype, long contentLength)
{
DownloadManager.Request request = new DownloadManager.Request(Android.Net.Uri.Parse(url));
request.AllowScanningByMediaScanner();
request.SetNotificationVisibility(DownloadVisibility.VisibleNotifyCompleted);
request.SetDestinationInExternalFilesDir(Forms.Context, Android.OS.Environment.DirectoryDownloads, "download");
DownloadManager dm = (DownloadManager)Android.App.Application.Context.GetSystemService(Android.App.Application.DownloadService);
dm.Enqueue(request);
}
}
}
That seemed like it would work, but it somehow disabled more essential WebView functionality (i.e. for a given file there might be different qualities to download it in, listed within a popover which appears when the user clicks a button - using the above code, the button stopped showing the popover) and so that solution was inadequate.
The second-last solution I tried was to intercept the URL in the navigating function of the WebView and use a DownloadManager to process the file:
void w_Navigating(object sender, WebNavigatingEventArgs e)
{
if(e.Url.ToLower().Contains("forcesave=true"))
{
var downloadProcessor = DependencyService.Get<IDownloadProcessor>();
downloadProcessor.DownloadFromUrl(e.Url);
}
}
}
using Android.App;
using Android.Content;
using Orchidnet.Mobile.Core.Extensions;
using Xamarin.Forms;
using Orchidnet.Mobile.App.Droid;
using Android.Webkit;
[assembly: Dependency(typeof(DownloadProcessor_Android))]
namespace Orchidnet.Mobile.App.Droid
{
public class DownloadProcessor_Android : IDownloadProcessor
{
public void DownloadFromUrl(string url)
{
string cookie = CookieManager.Instance.GetCookie(url);
var source = Android.Net.Uri.Parse(url);
DownloadManager.Request request = new DownloadManager.Request(source);
request.AddRequestHeader("Cookie", cookie);
request.AllowScanningByMediaScanner();
request.SetNotificationVisibility(DownloadVisibility.VisibleNotifyCompleted);
request.SetDestinationInExternalFilesDir(Forms.Context, Android.OS.Environment.DirectoryDownloads, source.LastPathSegment);
DownloadManager dm = (DownloadManager)Android.App.Application.Context.GetSystemService(Context.DownloadService);
dm.Enqueue(request);
}
}
}
I don't like this solution either, for a number of reasons:
1. There is no guarantee that the url will always contain that parameter on the querystring, but it's the most precise I can get.
2. The DownloadManager requires that I specify the filename, which might not be part of the URL - so using the LastPathSegment might result in a Guid without any extension, rather than dog.jpg
3. Similar to the above, there is no way to distinguish between the different qualities of a file, so they may be overwritten - catching the quality from the URL and appending it to the forced filename could work, but is yet more work that should not be necessary.
4. I can't find any good examples of how to do this in iOS without even more issues (i.e. if it's an image, which I can't possibly know at this point from the URL, how can I make it available in the photos library, which is presumably what the user would want)?
The last solution I've tried is to intercept the URL in the Navigating method, and if I think it is a download, to then open the URL in the device's native browser:
void w_Navigating(object sender, WebNavigatingEventArgs e)
{
if(e.Url.ToLower().Contains("forcesave=true"))
{
var uri = new Uri(e.Url);
Device.OpenUri(uri);
e.Cancel = true;
}
}
}
This works, but results in a very clunky user experience. Is there not a way to make the WebView respect the content-disposition of a request and process it like the device's native browser does?