locked
Saving to a ContentProvider RRS feed

  • Question

  • User369958 posted

    In my app I am opening a text file retrieved using Xamarin.Plugin.FilePicker. The stream is converted to text, modified, edited etc, converted back to a stream ready to save. The question in the c# Android project how do you save the stream back to a ContentProvider uri for example:

    content://com.android.externalstorage.documents/document/home%3ARecords.txt

    content://com.google.android.apps.docs.storage/document/acc%3D1%3Bdoc%3Dencoded%3DtEuwAnry2ndjYGUkS32f4shX1HSY9rRn6eKHL1vTx8y9stV9Lg%3D%3D

    content://com.microsoft.skydrive.content.StorageAccessProvider/document/content%3A%2Fcom.microsoft.skydrive.content.metadata%2FDrive%2FID%2F1%2FItem%2fID%2F123592%2FProperty%2F%2F%3FRefreshOption%3DAutoRefresh%26RefreshTimeOut%3D15000

    Thanks

    Wednesday, November 13, 2019 11:13 PM

All replies

  • User382871 posted

    Create the Uri and specify the authority and path. public const string AUTHORITY = "com.xamarin.sample.VegetableProvider"; static string BASE_PATH = "vegetables"; public static readonly Android.Net.Uri CONTENT_URI = Android.Net.Uri.Parse("content://" + AUTHORITY + "/" + BASE_PATH); Then use ContentResolver to operate data. ContentResolver contains many methods like insert, delete etc.

    Check the Tutorial: https://docs.microsoft.com/en-us/xamarin/android/platform/content-providers/how-it-works

    Thursday, November 14, 2019 9:41 AM
  • User369958 posted

    This is what I am attempting -

    public async Task<Boolean> SaveFile(StorageData storageData)
            {
                Boolean status = false;
    
                try
                {
                    //storageData.Path contains string version of uri returned by Xamarin.Plugin.FilePicker
                    var uUri = Android.Net.Uri.Parse(storageData.Path);
    
                    var stream = ContentResolver.OpenOutputStream(uUri, "w");
                    //Error CS0120 An object reference is required for the non-static field, method, or property 
                       //'ContentResolver.OpenOutputStream(Uri, string)'
    
                    //storageData.ContentStream contains a System.IO.Stream
                    storageData.ContentStream.CopyTo(stream);
    
                    stream.Flush();
                    stream.Close();
    
                    status = true;
                }
                catch(Exception ex)
                {
                    //TODO: Code for error processing.
                    status = false;
                }
    
                return status;
            }
    

    At the moment I can't get past proper referencing of ContentResolver to test.

    Thursday, November 14, 2019 12:33 PM
  • User369958 posted

    Seem to have this down to a permission error - not sure why it would seem to have necessary permissions.

    Java.Lang.SecurityException: 'Permission Denial: writing com.microsoft.skydrive.content.StorageAccessProvider uri content://com.microsoft.skydrive.content.StorageAccessProvider/document/content%3A%2F%2Fcom.microsoft.skydrive.content.metadata%2FDrive%2FID%2F1%2FItem%2FID%2F123591%2FProperty%2F%3FRefreshOption%3DAutoRefresh%26RefreshTimeOut%3D15000 from pid=21205, uid=10416 requires android.permission.MANAGE_DOCUMENTS, or grantUriPermission()'

    [assembly: Dependency(typeof(DataGather.Droid.StorageService.StorageHelper))]
    [assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)]
    [assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]
    [assembly: UsesPermission(Android.Manifest.Permission.ManageDocuments)]
    
    namespace DataGather.Droid.StorageService
    {
        public class StorageHelper : IStorageHelper
        {
            private Context context;
    
            public async Task<Boolean> SaveFile(StorageData storageData)
            {
                Boolean status = false;
    
                this.context = Android.App.Application.Context;
    
                //context.GrantUriPermission();
    
                try
                {
                    //storageData.Path contains string version of uri returned by Xamarin.Plugin.FilePicker
                    var uUri = Android.Net.Uri.Parse(storageData.Path);
    
                    var stream = context.ContentResolver.OpenOutputStream(uUri, "w");
    
                    //storageData.ContentStream contains a System.IO.Stream
                    storageData.ContentStream.CopyTo(stream);
    
                    stream.Flush();
                    stream.Close();
    
                    status = true;
                }
                catch (Exception ex)
                {
                    //TODO: Code for error processing.
    
                    status = false;
                    throw;
                }
    
                return status;
            }
        }
    }
    
    Thursday, November 14, 2019 6:55 PM
  • User382871 posted

    Seem to have this down to a permission error - not sure why it would seem to have necessary permissions. Some data types require special permission to access. Like the built-in contacts list requires the android.permission.READ_CONTACTS permission in the AndroidManifest.xml file.

    Check the tutorial: https://docs.microsoft.com/en-us/xamarin/android/platform/content-providers/contacts-contentprovider

    Friday, November 15, 2019 10:06 AM
  • User369958 posted

    I appreciate your thoughts. I managed to get it down to the error (below). I think what that error is telling me is that since ContentResolver.TakePersistableUriPermission..... was not executed by Xamarin.Plugin.FilePicker at the time the file was picked and because this Uri is a ContentProvider, I'll have to pick the file again outside of the plugin to regain the permissions in order to save the modified stream. At that point this code should work just fine. Thanks

    Java.Lang.SecurityException Message=UID 10416 does not have permission to content://com.microsoft.skydrive.content.StorageAccessProvider/document/content%3A%2F%2Fcom.microsoft.skydrive.content.metadata%2FDrive%2FID%2F1%2FItem%2FID%2F123591%2FProperty%2F%3FRefreshOption%3DAutoRefresh%26RefreshTimeOut%3D15000 [user 0]; you could obtain access using ACTIONOPENDOCUMENT or related APIs

    Friday, November 15, 2019 3:34 PM
  • User382871 posted

    Any update? If you have solved your issue, please mark the help solution as the answer. It'll help others who face the similar problem.

    Monday, November 18, 2019 2:36 PM
  • User369958 posted

    I have solved the security issue. I resolved it by forking the Xamarin.Plugin.FilePicker, adding the following lines of code at line 153 in FilePickerActivity.android.cs, and rolling the Android portion of the project into my code -

    if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat)
    {
         this.context.ContentResolver.TakePersistableUriPermission(uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
     }
    

    Now I've spent the last 2 days pounding my head over updating the file. Using the following code text is written to the file no problem. EXCEPT if the new file content is shorter than the current file content there doesn't appear to be any way to truncate the stream, Or just clear the stream before writing the new text. According to the documentation OpenAssetFileDescriptor(uUri, "w") the "w" should also erase whatever content is currently in the file, but it does not. And of course System.IO.Stream offers no truncate. I've seen a technique where Stream.Seek was used to truncate, but the Stream returned by both methods below have CanSeek = false.

    docs.microsoft.com/en-us/dotnet/api/android.content.contentresolver.openassetfiledescriptor?view=xamarin-android-sdk-9

    docs.microsoft.com/en-us/dotnet/api/android.content.contentprovider.openassetfile?view=xamarin-android-sdk-9#AndroidContentContentProviderOpenAssetFileAndroidNetUriSystemString

     public async Task<String> SaveFile(StorageData storageData)
            {
                String status = "";
    
                try
                {
                    var uUri = Android.Net.Uri.Parse(storageData.Path);
    
                    var fileDesc = context.ContentResolver.OpenAssetFileDescriptor(uUri, "w");
    
                    Stream stream = fileDesc.CreateOutputStream();
    
                    String test = "JUST ANOTHER OVERWRITE TEST OF EXISTING FILE CONTENT";
    
                    //This works, overwrites but does not truncate.
                    using (StreamWriter writer = new StreamWriter(stream))
                    {
                        await writer.WriteAsync(test);
    
                        await writer.FlushAsync();
                    }
    
                    fileDesc.Close();
    
                    status = "success";
                }
                catch (Exception ex)
                {
                    //TODO: Code for error processing.
    
                    status = ex.Message + " - " + ex.TargetSite.Name;
                }
    
                return status;
            }
    

    I have also tried this with the same inability to truncate the original content issue.

    public async Task<String> SaveFile(StorageData storageData)
            {
                  String status = "";
    
                try
                {
                    var uUri = Android.Net.Uri.Parse(storageData.Path);
    
                    Stream stream = context.ContentResolver.OpenOutputStream(uUri, "w");
    
                    String test = "JUST ANOTHER OVERWRITE TEST OF EXISTING FILE CONTENT";
    
                    //This works, overwrites but does not truncate.
                    using (StreamWriter writer = new StreamWriter(stream))
                    {
                        await writer.WriteAsync(test);
    
                        await writer.FlushAsync();
                    }
    
                    status = "success";
                }
                catch (Exception ex)
                {
                    //TODO: Code for error processing.
    
                    status = ex.Message + " - " + ex.TargetSite.Name;
                }
    
                return status;
            }
    
    Tuesday, November 19, 2019 10:20 PM
  • User382871 posted

    Try to set the length of this stream. Check the link. FileStream fileStream = new FileStream(...); fileStream.SetLength(sizeInBytesNotChars);

    Refer to: https://stackoverflow.com/questions/6537926/how-to-truncate-a-file-in-c

    Wednesday, November 20, 2019 3:54 PM
  • User369958 posted

    Tried something similar using stream. The Stream returned by the ContentResolver methods is very limited.

      var fileDesc = context.ContentResolver.OpenAssetFileDescriptor(uUri, "w");
      Stream stream = fileDesc.CreateOutputStream(); 
      stream.SetLength(1);  //Specified method is not supported.
    

    OR

    Stream stream = context.ContentResolver.OpenOutputStream(uUri, "w");
    stream.SetLength(1);    //Specified method is not supported.
    

    Either way the result is the same. If you look at what is available on the stream during debug it shows - CanRead = false CanSeek = false CanWrite = true Length = System.NotSupportedException: Specified method is not supported. Position = System.NotSupportedException: Specified method is not supported.

    Today (after golf) I'll explore deleting the file and writing to a new one with the same name. I am suspicious this may not work either because in an earlier test I tried

    var howmany = context.ContentResolver.Delete(uUri, "'=*'", null); //error message - Delete not supported.

    Should not be this complicated to work with a text file.

    Thursday, November 21, 2019 12:22 PM
  • User382871 posted

    Did you use the ContentProvider.OpenFile method. It will truncates any existing file.

    Friday, November 22, 2019 3:10 PM
  • User369958 posted

    Didn't try that specifically because ContentResolver.OpenFileDescriptor(uUri, "w") and ContentResolver.OpenOutputStream(uUri, "w") ContentResolver.OpenAssetFileDescriptor(uUri, "w") should also as well. These sit on top of the ContentProvider.Open varieties. All of these returned a similar stream -

    CanRead = false CanSeek = false CanWrite = true Length = System.NotSupportedException: Specified method is not supported. Position = System.NotSupportedException: Specified method is not supported.

    One of two things has to be happening, either I am leaving out a code step necessary to clear or truncate the stream, or the Xamarin Android SDK 9 does not behave as expected. Examples in the MS documents are slim to none. But at Android the docs are more complete and there is a good process example for using storage access framework. developer.android.com/guide/topics/providers/document-provider#kotlin

    I'll throw the code as it stands right now below. In it you'll see each method tried, some notes and links to applicable references. I think as next steps I'll try and quick and dirty replicate this in Android studio and Kotlin. I'm no fan of Java which is part of why I was using Xamarin and c# in the first place. But If I can make the steps work native, maybe I can work that back into Xamarin Android. It should also help determine if it is a step issue or sdk issue.

    Second I think it's time to reframe this as a ContentResolver issue with clearing or truncating a file or stream. Then posting here under Xamarin.Android and also maybe up at StackOverflow and elsewhere. Thread has become specific to that issue, since saving works but only as an overwrite. I'll make a final post here with links when I get that done, in case you wish to continue to follow. Thank you for the input provided.

    public async Task<String> SaveFile(StorageData storageData)
            {
                String status = ""; // "Android SaveFile not yet implemented";
    
                String test = "ALAS ANOTHER OVERWRITE TEST OF EXISTING FILE CONTENT";
                byte[] bytest = System.Text.Encoding.UTF8.GetBytes(test);
    
                try
                {
                    //storageData.Path contains string version of uri returned by Xamarin.Plugin.FilePicker
                    var uUri = Android.Net.Uri.Parse(storageData.Path);
    
                    //https://docs.microsoft.com/en-us/dotnet/api/android.content.contentresolver.openassetfiledescriptor?view=xamarin-android-sdk-9
                    //https://www.android-doc.com/reference/android/content/ContentResolver.html#
                    //https://www.android-doc.com/reference/android/content/ContentProvider.html#openAssetFile(android.net.Uri,%20java.lang.String)
                    //https://developer.android.com/guide/topics/providers/document-provider#kotlin
    
                    //Access mode for the file. May be "r" for read - only access, "w" for write - only access(erasing whatever data is currently in the file),
                    //"wa" for write - only access to append to any existing data, "rw" for read and write access on any existing data, 
                    //and "rwt" for read and write access that truncates any existing file.
    
                    //========= METHOD 1 ==========
                    //https://docs.microsoft.com/en-us/dotnet/api/android.content.contentresolver.openfiledescriptor?view=xamarin-android-sdk-9
    
                    //var fileDesc = context.ContentResolver.OpenFileDescriptor(uUri, "w");     //also tried "rwt"
                    //Stream stream = fileDesc.CreateOutputStream();
    
                    //stream.SetLength(1);    //Specified method is not supported.
    
                    ////This works, overwrites but does not truncate.
                    //using (StreamWriter writer = new StreamWriter(stream))
                    //{
                    //    await writer.WriteAsync(test);
                    //    await writer.FlushAsync();
                    //}
    
                    //fileDesc.Close();
    
                    //========= METHOD 2 ==========
                    //https://www.android-doc.com/reference/android/content/ContentProvider.html#openAssetFile(android.net.Uri,%20java.lang.String)
                    //https://www.android-doc.com/reference/android/content/ContentResolver.html#openAssetFileDescriptor(android.net.Uri,%20java.lang.String)
    
                    var fileDesc = context.ContentResolver.OpenAssetFileDescriptor(uUri, "w");     //also tried "rwt"
                    Stream stream = fileDesc.CreateOutputStream();
    
                    //stream.SetLength(1);    //Specified method is not supported.
    
                    //TODO: This works, overwrites but does not truncate.
                    using (StreamWriter writer = new StreamWriter(stream))
                    {
                        await writer.WriteAsync(test);
                        await writer.FlushAsync();
                    }
    
                    fileDesc.Close();
    
                    //======== METHOD 3 ===========
                    //https://docs.microsoft.com/en-us/dotnet/api/android.content.contentresolver.openoutputstream?view=xamarin-android-sdk-9
    
                    //Stream stream = context.ContentResolver.OpenOutputStream(uUri, "w");     //also tried "rwt"
    
                    //stream.SetLength(1);    //Specified method is not supported.
    
                    ////This works, overwrites but does not truncate.
                    //using (StreamWriter writer = new StreamWriter(stream))
                    //{
                    //    await writer.WriteAsync(test);
                    //    await writer.FlushAsync();
                    //}
    
                    //stream.Close();
    
                    //======== METHOD 4 ===========
                    //https://docs.microsoft.com/en-us/dotnet/api/system.io.stream.writeasync?view=netstandard-2.0#System_IO_Stream_WriteAsync_System_Byte___System_Int32_System_Int32_
    
                    //Stream stream = context.ContentResolver.OpenOutputStream(uUri, "w");     //also tried "rwt"
    
                    //stream.SetLength(1);    //Specified method is not supported.
    
                    //await stream.WriteAsync(bytest, 0, bytest.Length);
                    //await stream.FlushAsync();
                    //stream.Close();
    
                    //===============================
    
                    status = "success";
                }
                catch (Exception ex)
                {
                    //TODO: Code for error processing.
    
                    status = ex.Message + " - " + ex.TargetSite.Name;
                }
    
                return status;
            }
    
    Saturday, November 23, 2019 1:28 AM
  • User369958 posted

    Well who knew that my first Android Studio/Java program would be for troubleshooting a Xamarin/c# program not working quite right on an Android device, but working correctly in UWP. Here is what I learned...

    Using Java/Android Studio with Android phone 9 api 28 and the java code below, a text file that lives on Google Drive erases first as expected, leaving only the newly written content in the file. If the file lives on OneDrive the first x bytes are overwritten and the remaining original bytes left intact.

    So the behavior for a OneDrive file is the same (incorrect) whether coded with Java/Android Studio or C#/Xamarin.

    I have posted this at stackoverflow if you wish to follow along. Once there is a final resolution I'll post back here.

    Java/Android Studio

    public void saveFile(View view)
        {
            try
            {
                AssetFileDescriptor pfd = getContentResolver().openAssetFileDescriptor(fileUri, "w");
    
                FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
    
                fileOutputStream.write(("Overwritten again " + System.currentTimeMillis() + "\n").getBytes());
    
                fileOutputStream.close();
    
                pfd.close();
            }
            catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    Tuesday, November 26, 2019 9:50 PM