0

I have a pdf i've added as an AndroidAsset and a BundleResource for my Android and IOS projects using xamarin forms.

I just want to be able to open those files from any device, using whatever pdf viewer the device defaults to.

Essentially, i just want to be able to do something like:

Device.OpenUri("file:///android_asset/filename.pdf");

but this doesn't seem to work. Nothing happens and the user is never prompted to open the pdf. I don't want to use any 3rd party libraries that allow the pdf to open in app, i just want it to redirect the user to a pdf viewer or browser.

Any ideas?

mo alaz
  • 4,529
  • 8
  • 30
  • 36
  • Device.OpenUri opens the file in the default browser. You need to provide full file path to the function and in order to get the full path for each platform, you need to create a dependency service in all platforms. This dependency service needs to send the full path of the document with respect to the device. – Sparsha Bhattarai Sep 12 '18 at 10:36

1 Answers1

5

First of all you will need an interface class, since you need to call the dependency service in order to pass your document to the native implementation(s) of your app:

so in your shared code add an interface, called "IDocumentView.cs":

public interface IDocumentView
{
    void DocumentView(string file, string title);
}

Android

Now in your android project create the corresponding implementation "DocumentView.cs":

assembly: Dependency(typeof(DocumentView))]
namespace MyApp.Droid.Services
{
public class DocumentView: IDocumentView
{
    void IDocumentView.DocumentView(string filepath, string title)
    {
        try
        {
            File file = new File(filepath);

            String mime = FileTypes.GetMimeTypeByExtension(MimeTypeMap.GetFileExtensionFromUrl(filepath));
            File extFile = new File (Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDocuments), file.Name);
            File extDir = extFile.ParentFile;
            // Copy file to external storage to allow other apps to access ist
            if (System.IO.File.Exists(extFile.AbsolutePath))
                System.IO.File.Delete(extFile.AbsolutePath);

            System.IO.File.Copy(file.AbsolutePath, extFile.AbsolutePath);
            file.AbsolutePath, extFile.AbsolutePath);
            // if copying was successful, start Intent for opening this file
            if (System.IO.File.Exists(extFile.AbsolutePath))
            {
                Intent intent = new Intent();
                intent.SetAction(Android.Content.Intent.ActionView);
                intent.SetDataAndType(Android.Net.Uri.FromFile(extFile), mime);
                MainApplication.FormsContext.StartActivityForResult(intent, 10);
            }
        }
        catch (ActivityNotFoundException anfe)
        {
            // android could not find a suitable app for this file
            var alert = new AlertDialog.Builder(MainApplication.FormsContext);
            alert.SetTitle("Error");
            alert.SetMessage("No suitable app found to open this file");
            alert.SetCancelable(false);
            alert.SetPositiveButton("Okay", (object sender, DialogClickEventArgs e) => ((AlertDialog)sender).Hide());
            alert.Show();
        }
        catch (Exception ex)
        {
            // another exception
            var alert = new AlertDialog.Builder(MainApplication.FormsContext);
            alert.SetTitle("Error");
            alert.SetMessage("Error when opening document");
            alert.SetCancelable(false);
            alert.SetPositiveButton("Okay", (object sender, DialogClickEventArgs e) => ((AlertDialog)sender).Hide());
            alert.Show();
        }
    }
}
}

Please note that MainApplication.FormsContext is a static variable I added to my MainApplication.cs in order to be able to access the Context of the app quickly.

In your Android manifest, add

In your application resources, add an xml resource (into folder "xml") called file_paths.xml with the following content:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
   <external-files-path name="root" path="/"/>
   <external-files-path name="files" path="files" />
</paths>

Also you need to ensure that there are apps installed on the target device, which are able to handle the file in question. (Acrobat Reader, Word, Excel, etc...).

iOS

iOS already comes with a quite nice document preview built in, so you can simply use that (again create a file named "DocumentView.cs" in your iOS Project):

[assembly: Dependency(typeof(DocumentView))]
namespace MyApp.iOS.Services
{
public class DocumentView: IDocumentView
{
    void IDocumentView.DocumentView(string file, string title)
    {
        UIApplication.SharedApplication.InvokeOnMainThread(() =>
        {
            QLPreviewController previewController = new QLPreviewController();

            if (File.Exists(file))
            {
                previewController.DataSource = new PDFPreviewControllerDataSource(NSUrl.FromFilename(file), title);
                UIApplication.SharedApplication.KeyWindow.RootViewController.PresentViewController(previewController, true, null);
            }
        });
    }
}

public class PDFItem : QLPreviewItem
{
    public PDFItem(string title, NSUrl uri)
    {
        this.Title = title;
        this.Url = uri;
    }
    public string Title { get; set; }
    public NSUrl Url { get; set; }
    public override NSUrl ItemUrl { get { return Url; } }
    public override string ItemTitle { get { return Title; } }
}

public class PDFPreviewControllerDataSource : QLPreviewControllerDataSource
{
    PDFItem[] sources;

    public PDFPreviewControllerDataSource(NSUrl url, string filename)
    {
        sources = new PDFItem[1];
        sources[0] = new PDFItem(filename, url);
    }

    public override IQLPreviewItem GetPreviewItem(QLPreviewController controller, nint index)
    {
        int idx = int.Parse(index.ToString());
        if (idx < sources.Length)
            return sources.ElementAt(idx);
        return null;
    }

    public override nint PreviewItemCount(QLPreviewController controller)
    {
        return (nint)sources.Length;
    }
}
}

Finally you can call

DependencyService.Get<IDocumentView>().DocumentView(file.path, "Title of the view"); 

to display the file in question.

Markus Michel
  • 2,289
  • 10
  • 18
  • On Android, I keep getting exceptions that the file is not found, when checking if it exists or trying to copy. Currently, the filepath i'm passing is something like "file:///android_asset/name.pdf". – mo alaz Oct 16 '18 at 11:10
  • That won't work, since your file isn't lying on a proper file path but is being compiled into your application's assets. Even worse, another app won't be able to access it, since other apps are not allowed to access your app's (internal) assets. So basically you need to get the file content by using Assets.Open("name.pdf"); and write that to a public file location so that the file can be accessed by other apps. – Markus Michel Oct 16 '18 at 11:53
  • Ah, ok. Isn't that the reason we are copying the file in the first place? So if the file is in my assets, i would have to use Assets.Open("name.pdf"), write it that to a public file location and i wouldn't need to copy it? What's a public location that all android phones have? – mo alaz Oct 16 '18 at 13:29
  • Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDocuments) should return a public directory to use. By default your app, it's assets and even all files it downloads or generates by itself are put inside their own sandbox, which is closed to the outside world (meaning other apps on the same device). The android solution basically asks the other apps, if they can handle the file and then shows the user which apps can use that file. So the file has to made accessible to them by either copying or writing them to a place they are "allowed" to access. – Markus Michel Oct 16 '18 at 13:35
  • I just saw that my answer was lacking setting up the fileprovider information in the manifest. I updated that in my answer in the android section. – Markus Michel Oct 16 '18 at 13:44
  • @MarkusMichel: I was not able to get it working. I have posted a question here https://stackoverflow.com/questions/57695141/open-pdf-by-3rd-party-app-in-xamarin-forms-error-error-when-opening-document. Could you please tell me what I am doing wrong? Am I missing something? – Yaza Aug 28 '19 at 14:54