Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Xamarin.Forms - How to define scoped storage for android and how can I implement it to downlaod files in Downloads directory?

In my app I have given facility to download reports in PDF formats. Everything was good until I was testing my app on Android 10 and below. Recently I updated a device to Android 11 and now I am not able to download any file in shared storage. I am getting Access Denied error while downloading file.

I have given permission to read and write external storage

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

and my download path is

Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;

I have also set android:requestLegacyExternalStorage="true" in AndroidManifest.xml

My code to download file looks like this

public async void DownloadSupplierSample(object sender, EventArgs e)
    {
        try
        {
            string basepath;
            if (Device.RuntimePlatform == Device.Android)
                basepath = DependencyService.Get<IDownloadPath>().GetDownloadsPath();
            else
                basepath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "..", "Library");

            PermissionStatus readstatus = await Permissions.CheckStatusAsync<Permissions.StorageRead>();
            PermissionStatus writestatus = await Permissions.CheckStatusAsync<Permissions.StorageWrite>();

            if (readstatus == PermissionStatus.Granted && writestatus == PermissionStatus.Granted)
            {
                DownloadSupplier(basepath);
            }
            else
            {
                var read = await Permissions.RequestAsync<Permissions.StorageRead>();
                var write = await Permissions.RequestAsync<Permissions.StorageWrite>();

                if (read == PermissionStatus.Granted && write == PermissionStatus.Granted)
                {
                    DownloadSupplier(basepath);
                }
            }
            await DisplayAlert("", "Sample is downaloaded at Downloads directory","Ok");
        }
        catch (Exception ex)
        {
        }
    }

    async void DownloadSupplier(string basepath)
    {
        using (var stream = await FileSystem.OpenAppPackageFileAsync("SupplierMaster.xlsx"))
        {
            string filename = $"SupplierMaster_{DateTime.Now.ToString("yyyyMMddHHmmss")}.xlsx";
            string filepath = Path.Combine(basepath, filename);

            byte[] bytes = Utils.Common.StreamToBytes(stream);
            File.WriteAllBytes(filepath, bytes);
            //using (Stream filestream = File.Create(filepath))
            //{
            //    //stream.Seek(0, SeekOrigin.Begin);
            //    stream.CopyTo(filestream);
            //}
        }
    }

I have tried setting flag from device as well from this link

How can I define scoped storage for my app ?

Any update how can I download PDF file in my downloads directory ?

Thank you.

like image 990
Monica Avatar asked Mar 10 '21 07:03

Monica


People also ask

How is scoped storage implemented in Android?

To enable scoped storage in your app, regardless of your app's target SDK version and manifest flag values, enable the following app compatibility flags: DEFAULT_SCOPED_STORAGE (enabled for all apps by default) FORCE_ENABLE_SCOPED_STORAGE (disabled for all apps by default)

What is scoped storage?

The goal of scoped storage is to protect the privacy of app and user data. This includes protecting user information (such as photo metadata), preventing apps from modifying or deleting user files without explicit permission, and protecting sensitive user documents downloaded to Download or other folders.

How do I create a folder in xamarin?

You can use dependenceService to create a folder and a file in device internal storage. First, create an interface in your PCL. Invoke this dependenceService in button click event or other method. Then achieve it in Android folder.

Should Xamarin apps be on internal or external storage?

As a rule of thumb, Xamarin.Android apps should prefer saving their files on internal storage when it is reasonable, and rely on external storage when files need to be shared with other apps, are very large, or should be retained even if the app is uninstalled.

What is Xamarin form in Android?

Create New Xamarin.Form Application Xamarin.Forms is a cross-platform UI toolkit, which allows the user to efficiently create a native user interface layout. The code can be shared with all the devices (IOS, Android, Windows Phone, and Win store app).

What is scoped storage in Android 10?

Android 10 introduced a new storage paradigm for apps called scoped storage which changes the way apps store and access files on a device's external storage. If you target Android 10 (API level 29) or higher, set the value of requestLegacyExternalStorage to true in your app's manifest file.

How does the Xamarin Android app determine the path to files?

The Xamarin.Android app determines the path to the file that will be manipulated, then uses standard.NET idioms for file access. Because the actual paths to internal and external storage may vary from device to device or from Android version to Android version, it is not recommended to hard code the path to the files.


2 Answers

I had the same issue a couple of months ago.

I found this on developer.android page: enter image description here (https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory())

The first alternative (getExternalFilesDir) won't work as you want, files won't appear in the download directory, this location is internal to the app.

The second alternative is for media files.

I ended up using the third alternative. I created a service class in Android project, something like this:

public class FilesManager : IFilesManager
{
    private const int CREATE_FILE_REQUEST_CODE = 123;
    private static Context _context;
    
    public static void Init(Context context)
    {
        _context = context;
    }

    public async Task SaveFile(string fileName, byte[] content)
    { 
        Intent intent = new Intent(Intent.ActionCreateDocument);
        intent.AddCategory(Intent.CategoryOpenable);
        intent.SetType(your_file_mimeType);
        intent.PutExtra(Intent.ExtraTitle, fileName);

        var activity = (MainActivity)_context;
        var listener = new ActivityResultListener(activity);
        activity.StartActivityForResult(intent, CREATE_FILE_REQUEST_CODE);

        var result = await listener.Task;

        if (result == null)
        {
            // Cancelled by user
            return;
        }

        using (Stream os = _context.ContentResolver.OpenOutputStream(result.Data))
        {
            os?.Write(content);
            os?.Close();
        }
    }

    private class ActivityResultListener
    {
        private readonly TaskCompletionSource<Intent> Complete = new TaskCompletionSource<Intent>();
        public Task<Intent> Task { get { return this.Complete.Task; } }

        public ActivityResultListener(MainActivity activity)
        {
            // subscribe to activity results
            activity.ActivityResult += OnActivityResult;
        }

        private void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            if(requestCode != CREATE_FILE_REQUEST_CODE)
            {
                return;
            }

            // unsubscribe from activity results
            var activity = (MainActivity)_context;
            activity.ActivityResult -= OnActivityResult;

            // process result
            if (resultCode == Result.Ok)
            {
                Complete.TrySetResult(data);
            }
            else
            {
                Complete.TrySetResult(null);
            }
        }
    }
}

In MainActivity.cs add:

public event Action<int, Result, Intent> ActivityResult;

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) 
{
    ActivityResult?.Invoke(requestCode, resultCode, data); 
}

You can call Init method after Forms.Init:

FilesManager.Init(this);

Note: This method requires user interaction. The user will be prompted to choose the location where to save the file.

like image 191
alexgavru Avatar answered Nov 15 '22 01:11

alexgavru


How can I define scoped storage for my app ?Any update how can I download PDF file in my downloads directory ?

Android 10 introduced a new storage paradigm for apps called scoped storage which changes the way apps store and access files on a device's external storage. If you target Android 10 (API level 29) or higher, set the value of requestLegacyExternalStorage to true in your app's manifest file.

<application android:requestLegacyExternalStorage="true" android:label="FormsSample.Android" android:theme="@style/MainTheme"></application>

Then request runtime write and read EXTERNAL_STORAGE permission before access download folder.

public void requestpermission()
{
    if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != (int)Permission.Granted)
    {
        ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.WriteExternalStorage }, 1);
    }


    if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadExternalStorage) != (int)Permission.Granted)
    {
        ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.ReadExternalStorage }, 1);
    }
}

Create interface IAccessFileService in Xamarin.Forms shared code.

public interface IAccessFileService
{
    void CreateFile(string FileName);
}

Implementing IAccessFileService interface on Android platform.You could use File.Exists(xx) to check if the folder exists, and use Directory.CreateDirectory(xx) to create the folder if not.

[assembly: Xamarin.Forms.Dependency(typeof(AccessFileImplement))]
namespace FormsSample.Droid
{
    public class AccessFileImplement : IAccessFileService
    {
        public void CreateFile(string FileName)
        {
            string text = "hello world";
            byte[] data = Encoding.ASCII.GetBytes(text);
            string rootPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, Android.OS.Environment.DirectoryDownloads);
            var filePathDir = Path.Combine(rootPath, "folder");
            if (!File.Exists(filePathDir))
            {
                Directory.CreateDirectory(filePathDir);
            }
            string filePath = Path.Combine(filePathDir, FileName);
            File.WriteAllBytes(filePath, data);
        }

        
    }
}

So using dependencyservice to call android CreateFile method.

private void btn1_Clicked(object sender, EventArgs e)
{
    DependencyService.Get<IAccessFileService>().CreateFile("test.txt");
}
like image 36
Cherry Bu - MSFT Avatar answered Nov 14 '22 23:11

Cherry Bu - MSFT