3

I'm trying to have the app user select a folder to save pictures taken from the camera. If I need to use a different NuGet package than I'm using or a built-in feature, I can do that. The one I'm using was the first one I found that had a folder picker and I could get to work.

I've read through dozens of articles, Answers, Questions, tech docs, walkthroughs, watched videos, and more to try to get access to pick a folder for camera images to be saved. Unfortunately, nothing seems to be working, as I keep getting an Exception saying I don't have the "Storage" permission, even though I've tried several ways to get them. I'm also requesting pretty much every permission related to "Storage", even one that I don't think is a real permission, but it was referenced in one of those documents I read.

This works on 2 real devices running Android 9 (API 28) and Android 12 (API 31), but not on the Android 13 (API 33) device. I'm using the latest CommunityToolkit NuGet package for the FolderPicker.

Looking at the Permissions specifically for the app, there are no Permissions listed in the "Denied" section.

I know this has nothing to do with the custom way I'm checking permissions, since I use it for requesting Bluetooth and other permissions.

The "FolderPickerResult" line of code down near the bottom is what returns an exception. It doesn't throw it, but returns it to the "result" object/variable. The exception message is "Storage Permission is not granted."

This is the whole object that is returned:

{
    FolderPickerResult {
        Folder = ,
        Exception = Microsoft.Maui.ApplicationModel.PermissionException: Storage permission is not granted.at CommunityToolkit.Maui.Storage.FolderPickerImplementation.InternalPickAsync(String initialPath, CancellationToken cancellationToken)in / _ / src / CommunityToolkit.Maui.Core / Essentials / FolderPicker / FolderPickerImplementation.android.cs: line 18 at CommunityToolkit.Maui.Storage.FolderPickerImplementation.PickAsync(String initialPath, CancellationToken cancellationToken)in / _ / src / CommunityToolkit.Maui.Core / Essentials / FolderPicker / FolderPickerImplementation.shared.cs: line 11,
        IsSuccessful = False
    }
}

As per the title, I'm using C# .Net 7 in a Maui project.

So, my questions are: What am I missing/doing wrong? Why doesn't this work when everything I'm reading says it should?

Here's my code, as minimal as possible:
AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.XXX.XXX" android:versionCode="1">
    <application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true" android:label="XXX">
        <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"></meta-data>
        </provider>
    </application>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_STORAGE_PERMISSION" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_MEDIA" />
    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <queries>
        <intent>
        <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
    </queries> 
</manifest>

GalleryPermissions.cs

using static Microsoft.Maui.ApplicationModel.Permissions;

namespace Namespace;

internal class GalleryPermissions : BasePlatformPermission
{
#if ANDROID
    public override (string androidPermission, bool isRuntime)[] RequiredPermissions =>
        new List<(string permission, bool isRuntime)>
        {
            ("android.permission.WRITE_EXTERNAL_STORAGE", true),
            ("android.permission.READ_EXTERNAL_STORAGE", true),
            ("android.permission.MANAGE_EXTERNAL_STORAGE", true),
            ("android.permission.MANAGE_MEDIA", true),
            ("android.permission.READ_MEDIA_IMAGES", true),
            ("android.permission.READ_MEDIA_AUDIO", true),
            ("android.permission.READ_MEDIA_VIDEO", true),
            ("android.permission.READ_STORAGE_PERMISSION", true),
            ("android.permission.MEDIA_CONTENT_CONTROL", true)
        }.ToArray();
#endif
}

MainPage.xaml.cs

using CommunityToolkit.Maui.Storage;

    protected override async void OnAppearing()
    {
        base.OnAppearing();

        PermissionStatus storageWriteStatus = await CheckPermission<Permissions.StorageWrite>();  // always "denied"
        PermissionStatus storageReadStatus = await CheckPermission<Permissions.StorageRead>();  // always "denied"

        PermissionStatus galleryStatus = await CheckPermission<GalleryPermissions>();  // always "denied"
        ...
    }

    private static async Task<PermissionStatus> CheckPermission<TPermission>() where TPermission : Permissions.BasePermission, new()
    {
        PermissionStatus status = await Permissions.CheckStatusAsync<TPermission>();

        if (status != PermissionStatus.Granted)
        {
            status = await Permissions.RequestAsync<TPermission>();
        }

        return status;
    }

    private async void SelectButton_Clicked(object sender, EventArgs e)
    {
        CancellationTokenSource source = new();

        // SaveLocation.Text is originally string.Empty, but even if I put in a breakpoint and set 
        // it to the same "/storage/emulated/0/DCIM/Camera" that I get returned on other devices, 
        // I still get an Exception and result.IsSuccessful is `false`
        FolderPickerResult result = await FolderPicker.PickAsync(SaveLocation.Text, source.Token);
        //FolderPickerResult result = await FolderPicker.Default.PickAsync(SaveLocation.Text, source.Token); // This is the same as the line above
        if (result.IsSuccessful)
        {
            SaveLocation.Text = result.Folder.Path;
        }
        else
        {
            // On API 33, this always prints "Storage Permission is not granted.", except when 
            // I try to remove permissions I've read aren't needed for API 33.
            await Toast.Make($"{result.Exception.Message}").Show(source.Token);
            Error.Text = result.Exception.Message;
        }
    }

Here are screenshots from the actual, real Android device I'm using to debug my app and running Android 13 (API 33), which happens to be a Samsung Galaxy A03s Tracfone. I've included the Permissions page for the App.

Error message App Permissions page

computercarguy
  • 2,173
  • 1
  • 13
  • 27
  • which line causes the exception, and what is the **specific exception** thrown? – Jason May 30 '23 at 19:26
  • Perhaps this will help: [android-13-how-to-request-write-external-storage](https://stackoverflow.com/questions/73620790/android-13-how-to-request-write-external-storage) – Ryan Wilson May 30 '23 at 19:31
  • @Jason, I've updated the question with all the infor about the exception, but the "FolderPickerResult" down near the bottom is what returns an exception. It doesn't throw it, but returns it in the "result" object. And the exception message is "Storage Permission is not granted." It's apparently a custom exception. – computercarguy May 30 '23 at 19:37
  • https://github.com/CommunityToolkit/Maui/blob/8868aaf2c05132634d908810fa0ab2ab51bfeaee/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs – Jason May 30 '23 at 19:40
  • @RyanWilson, I've tried removing "READ_EXTERNAL_STORAGE" and "WRITE_EXTERNAL_STORAGE", but API 33 still seems to want them, as I get those in the "result.Exception.Message" instead of the generic message about "Storage". – computercarguy May 30 '23 at 19:41
  • @Jason, I forgot to include that I'm also checking for `Permissions.StorageRead` and `Permissions.StorageWrite` explicitly. I've added that to the question. I'm still reading the rest of the code in the link you sent. – computercarguy May 30 '23 at 19:46
  • What other shared folders have you tried? Maybe apps are no longer permitted to access `../DCIM/Camera` folder, via a folder path? Maybe that can only be accessed via a `contentResolver` or a `mediaUri`? – ToolmakerSteve May 30 '23 at 20:12
  • @ToolmakerSteve, I've tried sending the FolderPicker an empty.String, but that still returns the Exception. I figure the FolderPicker would default to whatever storage area the app is allowed to access. I'm not sure what else I can try. I'm definitely open to suggestions. – computercarguy May 30 '23 at 20:15
  • I have not worked with files outside of app's directory. [Access documents and other files from shared storage](https://developer.android.com/training/data-storage/shared/documents-files) is an Android doc that mentions contentResolver and mediaUri. Maybe read up on java/kotlin APIs, then do some experiments to see what you can access, in Android-specific code. c# APIs should be equivalent; just slightly different naming conventions. – ToolmakerSteve May 30 '23 at 20:20
  • @ToolmakerSteve, I'm not sure how I'd test any of that other than what I've already done with the other 2 devices (API 28 and 31) I have this working on. Apparently, C# .Net and/or Maui do this under the hood, so I don't have to worry about it. I've written an (one) Android specific app using Java and Android Studio, so I know how to use Intents and their "extras", at least minimally. It just seems as if that's not necessary here, unless I've missed the point you were trying to make. – computercarguy May 30 '23 at 20:34
  • 1
    The point I am making is that Android uses a completely different access mechanism starting in API 33. (Or I should say, in 33 they stopped supporting the older mechanism, that you see working in older versions.) So no, .Net or Maui does NOT do this under the hood, AFAIK. – ToolmakerSteve May 30 '23 at 20:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253887/discussion-between-computercarguy-and-toolmakersteve). – computercarguy May 30 '23 at 20:41

2 Answers2

2

This is an issue for the CommunityToolkit.Maui package. According to the resource code on the github,if the value of var status = await Permissions.RequestAsync<Permissions.StorageRead>().WaitAsync(cancellationToken).ConfigureAwait(false); is PermissionStatus.Denied, this exception error will be thrown.

And for the target android 13, the <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> has been removed, so the dialog about permission request will never appear on the android 13 or higher. The value of the Permissions.RequestAsync<Permissions.StorageRead> will always be PermissionStatus.Denied.

So you can change your application's target android version to the android 12 to make the READ_EXTERNAL_STORAGE permission request alert still appear on the device with android 13 or higher. Add the following code in the AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />

And you can also post a new issue for the CommunityToolkit.Maui package on the github.

Liyun Zhang - MSFT
  • 8,271
  • 1
  • 2
  • 14
  • Unfortunately, I don't have the option to target a lower API level than 33. This is a new app and Google requires new apps to target API 33. https://support.google.com/googleplay/android-developer/answer/11926878?hl=en – computercarguy May 31 '23 at 17:04
  • 1
    Starting August 31, 2023, google requests new app's target android version must be 33 or higher. But now is June. @computercarguy – Liyun Zhang - MSFT Jun 01 '23 at 01:07
  • {facepalm} I don't even know what to say..... I think I've been working on this too hard and didn't read the dates like I should have. Adding that 2nd line to the manifest did make it work, since I already had that first line. I think I need a vacation. – computercarguy Jun 01 '23 at 22:37
1

You may try this temporary workaround:

#if ANDROID
public async Task<bool> RequestPermissionAsync()
{
    var activity = Platform.CurrentActivity ?? throw new NullReferenceException("Current activity is null");

    if (ContextCompat.CheckSelfPermission(activity, Manifest.Permission.ReadExternalStorage) == Permission.Granted)
    {
        return true;
    }

    if (ActivityCompat.ShouldShowRequestPermissionRationale(activity, Manifest.Permission.ReadExternalStorage))
    {
        await Toast.Make("Please grant access to external storage", ToastDuration.Short, 12).Show();
    }

    ActivityCompat.RequestPermissions(activity, new[] { Manifest.Permission.ReadExternalStorage }, 1);

    return false;
}

#endif

Then you can use it like this:

#if ANDROID

        await RequestPermissionAsync();

#endif

        var folder = await mFolderPicker.PickAsync(cancelToken);
        folder.EnsureSuccess();

Make sure you import the following:

#if ANDROID
using Android;
using Android.Content.PM;
using AndroidX.Core.App;
using AndroidX.Core.Content;
#endif

Ref: https://github.com/CommunityToolkit/Maui/issues/998#issuecomment-1531142942

Martin King
  • 37
  • 1
  • 6
  • Unfortunately, this didn't make any difference. I put in a breakpoint to make sure this code was being hit, and I didn't get any prompts asking for permission. I'm still getting the Exception message about "Storage Permission is not granted." when I click the folder selection button. – computercarguy May 31 '23 at 16:57