Using Agular’s ng-file-upload library on Xamarin.Forms WebView
Recently I faced including a WebView in my app that navigates to an angular website which allows user to take a photo or to pick a picture from gallery and upload it to our server using the ng-file-upload library.

The problem
The angular’s library didn’t work for me on Android: the bottom sheet to choose the app to take action appears empty with a No apps can perform this action message. I guessed my problem was similar to this one, so I went to Android’s WebViewRenderer to check out the XF code related to OnShowFileChooser. I saw that FormsWebChromeClient is setted as WebChromeClient.


I wrote my own Android’s WebViewRenderer with custom WebChromeClient to test and debug OnShowFileChooser implementation. The Intent.CreateChooser didn’t work. I don’t know why.
The quick solution
I went for my own OnShowFileChooser implementation which do the below things:
- It ensures camera permission.
- It sets FileUploadCallback on MainActivity.
- It creates the gallery Intent.
- It creates the camera Intent.
- It creates a temporary image file using FileProvider (check the whole setup for FileProvider here). Set PhotoUriToUpload on MainActivity.
- It creates and launches the camera or gallery chooser Intent.
[assembly: ExportRenderer(typeof(CustomWebView), typeof(MyProject.Droid.Renderers.CustomWebViewRenderer))]
namespace MyProject.Droid.Renderers
{
public class CustomWebViewRenderer : WebViewRenderer
{
Context _context;
public CustomWebViewRenderer(Context context) : base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
Control.SetWebChromeClient(new CustomWebChromeClient(_context as Activity));
}
}
public class CustomWebChromeClient : WebChromeClient
{
readonly ICameraService _cameraService;
readonly MainActivity _activity;
public CustomWebChromeClient(Activity context)
{
_activity = context as MainActivity;
_cameraService = DependencyService.Get(); // You can use your own permissions service like me or use anything else
}
public override bool OnShowFileChooser(Android.Webkit.WebView webView, IValueCallback filePathCallback, FileChooserParams fileChooserParams)
{
CreateGalleryOrCameraChooser(filePathCallback);
return true;
}
private async void CreateGalleryOrCameraChooser(IValueCallback filePathCallback)
{
var permissionResult = await _cameraService.GetPermissionsAsync();
if (!permissionResult.IsAuthorized)
{
filePathCallback.OnReceiveValue(null);
return;
}
_activity.FileUploadCallback = filePathCallback;
Intent galleryIntent = new Intent(Intent.ActionGetContent);
galleryIntent.SetType("image/*");
Intent cameraIntent = new Intent(MediaStore.ActionImageCapture);
cameraIntent.AddFlags(ActivityFlags.GrantReadUriPermission);
if (cameraIntent.ResolveActivity(_activity.PackageManager) != null) // Ensure that there's a camera activity to handle the intent
{
File photoFile = CreateTemporaryImageFile();
if (photoFile != null)
{
Android.Net.Uri photoUri =
FileProvider.GetUriForFile(_activity, "com.example.myapp.fileprovider", photoFile);
cameraIntent.PutExtra(MediaStore.ExtraOutput, photoUri);
_activity.PhotoUriToUpload = photoUri;
}
}
Intent chooser = new Intent(Intent.ActionChooser);
chooser.PutExtra(Intent.ExtraIntent, galleryIntent);
chooser.PutExtra(Intent.ExtraTitle, AppResources.SelectPictureFromLabel);
IParcelable[] intentArray = {cameraIntent};
chooser.PutExtra(Intent.ExtraInitialIntents, intentArray);
_activity.StartActivityForResult(chooser, MainActivity.FILECHOOSER_RESULTCODE);
}
private File CreateTemporaryImageFile()
{
string timeStamp = DateTime.Now.ToString("yyyyMMddHHmmss");
string imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = _activity.GetExternalFilesDir(Environment.DirectoryPictures);
File image = File.CreateTempFile(imageFileName, ".jpg", storageDir);
return image;
}
}
}
}
You need two properties on MainActivity to store upload file callback and photo uri and you need to implement OnActivityResult.
namespace MyProject.Droid
{
[Activity(Label = "MyProject", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
public IValueCallback FileUploadCallback { get; set; }
public Uri PhotoUriToUpload { get; set; }
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
}
protected override void OnDestroy()
{
base.OnDestroy();
// You should delete temporary camera files created on OnShowFileChooser here
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (FileUploadCallback == null)
{
return;
}
if (data != null && requestCode == FILECHOOSER_RESULTCODE)
{
FileUploadCallback.OnReceiveValue(WebChromeClient.FileChooserParams.ParseResult((int)resultCode, data));
}
else if(PhotoUriToUpload != null && requestCode == FILECHOOSER_RESULTCODE)
{
Intent cameraPhotoIntent = new Intent(Intent.ActionView, PhotoUriToUpload);
FileUploadCallback.OnReceiveValue(WebChromeClient.FileChooserParams.ParseResult((int)resultCode, cameraPhotoIntent));
}
else
{
FileUploadCallback.OnReceiveValue(null);
}
FileUploadCallback = null;
PhotoUriToUpload = null;
}
}
}
I think this is not the best solution, but it works like a charm.

Better solution
Thinking about a better solution it’s easy to detect bad smells like the creation of a image temporary file even if camera is not chosen.
My proposal is create my own BottomSheetDialog with the camera and gallery options instead of using the built-in chooser. This way we can create and launch the gallery or camera Intents separately.
Do you have any proposal to improve my better solution? :)
Thanks for reading this post. I hope I’ve helped!
Comments
Post a Comment