3
\$\begingroup\$

I have a Xamarin.Forms application which utilizes dependency services to access Android specific features, in this case file access on the storage.

The file we are accessing is a serialized .xml file that contains the data for our specific class, called ConfigInfo, whose implementation isn't necessary for this review.

The goal is to open an .xml file with the Android file picker and then de-serialize it to our object. There are a few things to note

  1. We get the file as a URI, in my example the URI is represented as content://com.android.externalstorage.documents/document/primary%3Aconfig_V01_14.xml, with a Path element of /document/primary:config_V01_14_vtest.xml.

  2. We also utilize a dependency service, which is exposed through IDataStore. In order to appropriately alert the user that the data from the file has been loaded, I am utilizing an event that I need to fire manually using a public function (Bad practice, I know) which I can then respond to in the Xamarin.Forms view model.

  3. I am firing the event by getting the IDataStore dependency service instance, which is implemented by MainActivity, but it appears that the DependencyService might create a different global instance, because I am currently required to use the (apparently obselete) Xamarin.Forms.Forms.Context to call StartActivityForResult without getting a null pointer exception. I am wondering if, perhaps, implementing the dependency service interface in the main activity is a bad idea. If I decide to implement the dependency service in a separate class, how do I launch the file activity from that class?

So here are the functions at hand (again, this code is working, I just don't like it)

// Function declared in IDataStore and implemented in MainActivity
// Calling example:
// IDataStore<ConfigItem> dataStore = DependencyService.Get<IDataStore<ConfigItem>>();
// await dataStore.LoadConfig();
public async Task<bool> LoadConfig()
{
    Intent intent = new Intent(Intent.ActionOpenDocument);
    intent.AddCategory(Intent.CategoryOpenable);
    intent.SetType("text/xml");
    ((Activity)Forms.Context).StartActivityForResult(intent, READ_REQUEST_CODE);
    return true;
}

The code that receives the intent result is as follows:

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    base.OnActivityResult(requestCode, resultCode, data);
    if (resultCode == Result.Ok && data != null && requestCode == READ_REQUEST_CODE)
    {
        ParcelFileDescriptor parcelFileDescriptor = ContentResolver.OpenFileDescriptor(data.Data, "r");
        Java.IO.FileDescriptor fileDescriptor = parcelFileDescriptor.FileDescriptor;
        Java.IO.FileReader reader = null;
        Java.IO.BufferedReader bufferedReader = null;
        StringBuilder text = null;
        try
        {
            reader = new Java.IO.FileReader(fileDescriptor);
            bufferedReader = new Java.IO.BufferedReader(reader);
            text = new StringBuilder();
            String line;
            while ((line = bufferedReader.ReadLine()) != null)
            {
                text.Append(line);
            }
        }
        catch (Exception)
        {
            return;
        }
        finally
        {
            bufferedReader?.Close();
            reader?.Close();
            parcelFileDescriptor.Close();
        }

        if (text != null)
        {
            using (var stringReader = new StringReader(text.ToString()))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(ConfigInfo));
                object obj = serializer.Deserialize(stringReader);
                if (obj is ConfigInfo info)
                {
                    IDataStore<ConfigItem> dataStore = Xamarin.Forms.DependencyService.Get<IDataStore<ConfigItem>>();
                    dataStore.ConfigInfo = info;
                    dataStore.FireConfigInfoLoaded();
                    Console.WriteLine("Successfully read {0} with {1} items", data.Data.ToString(), dataStore.ConfigInfo.Items.Count);
                }
            }
        }
    }
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ @Ludisposed, just to be clear, this is actually c# code, but it is referencing exposed Java interfaces. It's a Xamarin Android project. \$\endgroup\$
    – StephenH
    Dec 5, 2017 at 16:50
  • \$\begingroup\$ @Ludisposed, yeah, I know it looks odd, sorry for the confusion. Thanks! \$\endgroup\$
    – StephenH
    Dec 5, 2017 at 16:52

1 Answer 1

0
\$\begingroup\$

Alright, so after a few days and doing some other work in the project, I've found the answers to my questions:

Issue: We also utilize a dependency service, which is exposed through IDataStore. In order to appropriately alert the user that the data from the file has been loaded, I am utilizing an event that I need to fire manually using a public function (Bad practice, I know) which I can then respond to in the Xamarin.Forms view model. Solution What was happening here is that by throwing my interface into MainActivity to attempt to get access to said MainActivity, I was getting the null pointer because MainActivity was being created on startup by the OS and then a new instance of MainActivity was being created by the DependencyService (and as such, wasn't a true Activity object as far as the Android OS was concerned). I did end up putting the interface into a separate class and now when the real MainActivity starts, it gets the dependency service and then casts it to the Android type that it knows and sets an Activity property so that the dependency service can call this property later.

Issue: Code to read from file was verbose and lengthy and relied on lots of java classes. Solution: Apparently in Xamarin the function Activity.ContentResolver.OpenInputStream returns a System.IO.Stream (.net) object instead of a Java.IO.Stream object! This was not initially obvious in the documentation but it greatly simplified the parsing of the file (in my case, an .xml file) because I could pass the opened Stream object right into my serializer.

Additional Information: I also simplified calling the event and now call the event in the set property for ConfigInfo.

DataStoreAndroid.cs

[assembly: Xamarin.Forms.Dependency(typeof(DataStoreAndroid))]
namespace NFCConfigMobile.Droid
{
    public class DataStoreAndroid : IDataStore
    {
        ...
        internal Activity MainActivity { get; set; }
        ...

        public async Task<bool> LoadConfig()
        {
            Intent intent = new Intent(Intent.ActionOpenDocument);
            intent.AddCategory(Intent.CategoryOpenable);
            intent.SetType("text/xml");
            MainActivity.StartActivityForResult(intent, READ_CONFIG_REQUEST_CODE);
            return true;
        }

        internal void ReadConfigFromFile(Android.Net.Uri uri)
        {
            Stream inputStream = MainActivity.ContentResolver.OpenInputStream(uri);
            XmlSerializer serializer = new XmlSerializer(typeof(ConfigInfo));
            object obj = serializer.Deserialize(inputStream);
            if (obj is ConfigInfo info)
            {
                Filename = uri.ToString();
                ConfigInfo = info;
                Console.WriteLine("Successfully read {0} with {1} items", uri.ToString(), ConfigInfo.Items.Count);
            }
        }

        public ConfigInfo ConfigInfo
        {
            get
            {
                return configInfo;
            }
            set
            {
                configInfo = value;
                ConfigInfoLoaded?.Invoke(this, new EventArgs());
            }
        }
    }
}

MainActivity.cs

namespace NFCConfigMobile.Droid
{
    [Activity(Label = "NFCConfigMobile.Android", Icon = "@drawable/icon", Theme = "@style/MyTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        private DataStoreAndroid dataStore;
        ...
        protected override void OnCreate(Bundle savedInstanceState)
        {
            ...
            // Called after Init
            dataStore = DependencyService.Get<IDataStore>() as DataStoreAndroid;
            dataStore.MainActivity = this;

            LoadApplication(new App());
        }
        ...

        protected override async void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            base.OnActivityResult(requestCode, resultCode, data);

            if (resultCode == Result.Ok && data != null)
            {
                if (requestCode == DataStoreAndroid.READ_CONFIG_REQUEST_CODE)
                {
                    dataStore.ReadConfigFromFile(data.Data);
                }
                ...
            }
        }
    }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.