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.

Camera or gallery picture chooser

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.

FormsWebChromeClient: OnShowFileChooser override detail
FormsWebChromeClient: ChooseFile method detail

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;
            }
        }
    }
}
Android’s CustomWebViewRenderer

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;
        }
    }
}
Android’s MainActivity with OnActivityResult for CustomWebViewRenderer

I think this is not the best solution, but it works like a charm.

Image for post

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

Popular Posts