Benjamin Zach
Benjamin Zach

Reputation: 1601

Unity + Android: How to ask for multiple permissions?

I'm working on a Unity (version 2019.1.11) project with target platform Android and I need to have the following permissions for my app to work:

android.permission.CAMERA
android.permission.RECORD_AUDIO
android.permission.WRITE_EXTERNAL_STORAGE
android.permission.READ_PHONE_STATE

The Unity documentation specifies this way to ask for Android permissions. My goal is to have an initial check for all required permissions during the start of the app, like this:

private void AskPermissions()
{
#if UNITY_ANDROID
    if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
    {
        Permission.RequestUserPermission(Permission.Microphone);
    }
    if (!Permission.HasUserAuthorizedPermission(Permission.Camera))
    {
        Permission.RequestUserPermission(Permission.Camera);
    }
    if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
    {
        Permission.RequestUserPermission(Permission.ExternalStorageWrite);
    }
    if (!Permission.HasUserAuthorizedPermission("android.permission.READ_PHONE_STATE"))
    {
        Permission.RequestUserPermission("android.permission.READ_PHONE_STATE");
    }
#endif
}

However, this does not work: The app only shows the dialog for the first permission that is not authorized and not for those not authorized that are being checked afterwards.

How can I ensure to always check for all permissions?

Upvotes: 5

Views: 13453

Answers (4)

Manjia
Manjia

Reputation: 175

Android Runtime Permissions are widely known to be troublesome, in your case the problem is that Android's permission request UI panel grabs the foreground on your first Permission.RequestUserPermission, suspending your App and preventing the following code to be executed

The cleanest solution I came out so far to handle Runtime Permissions is to run an infinite loop into a coroutine that greedily checks for the permissions you need and prompts the user to grant each one if it is not

It may sound ugly at first glance but consider that when a permission is asked the loop is interrupted because of the same reason why your code does not work: because a permission request UI panel grabs the foreground, and your app gets suspended, when the focus is given back your loop starts again checking for the missing permissions, plus you can slow down your loop with a yield return new WaitForSeconds(0.2f)

This is the code with some decoration to notify the user that it cannot proceed without granting all the permissions needed:

private bool _locationPermissionAsked;
private bool _microphonePermissionAsked;
private bool _cameraPermissionAsked;
private bool _storagePermissionAsked;

private void Start()
{
#if UNITY_ANDROID && !UNITY_EDITOR
    StartCoroutine(RequestPermissionsRoutine());        
#else
    /***** Ready to run you app *****/
#endif
}

private IEnumerator RequestPermissionsRoutine()
{
    yield return new WaitForEndOfFrame();
    while (true)
    {
        // For each permission you need, build a block like the following, it could 
        // have been done dynamically but given the small number of possible options 
        // I preferred to keep it extended
        if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation) && !_locationPermissionAsked)
        {
            // This flag keeps track of the user choice against the permission panel
            //
            // if he choose to NOT grant the permission we skip the permission request
            // because we are gonna notify him that he needs to grant the permission later 
            // using a message in our App
            _locationPermissionAsked = true; 
            
            // You can even ask permissions using android literal definition instead of Unity's Permission.FineLocation
            yield return Permission.RequestPermission("android.permission.ACCESS_FINE_LOCATION").WaitForCompletion();
            continue;
        }

        if (!Permission.HasUserAuthorizedPermission(Permission.Microphone) && !_microphonePermissionAsked)
        {
            _microphonePermissionAsked = true;
            yield return Permission.RequestPermission(Permission.Microphone).WaitForCompletion();
            continue;
        }

        if (!Permission.HasUserAuthorizedPermission(Permission.Camera) && !_cameraPermissionAsked)
        {
            _cameraPermissionAsked = true;
            yield return Permission.RequestPermission(Permission.Camera).WaitForCompletion();
            continue;
        }

        if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite) && !_storagePermissionAsked)
        {
            _storagePermissionAsked = true;
            yield return Permission.RequestPermission(Permission.ExternalStorageWrite);
            continue;
        }

        // This is the part where we check if all the permissions were granted
        // and allow the user to run the App ( else condition )
        // or prompt him to grant the permissions he denied
        //
        // Note that this code is never reached before each permission have been asked 
        // once, because of the "continue;"s used before
        if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation) ||
            !Permission.HasUserAuthorizedPermission(Permission.Microphone) ||
            !Permission.HasUserAuthorizedPermission(Permission.Camera) ||
            !Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
        {
            if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation))
            {
                /***** Tell the user to grant FineLocation Permission *****/
            }

            if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
            {
                /***** Tell the user to grant Microphone Permission *****/
            }

            if (!Permission.HasUserAuthorizedPermission(Permission.Camera))
            {
                /***** Tell the user to grant Camera Permission *****/
            }

            if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
            {
                /***** Tell the user to grant ExternalStorageWrite Permission *****/
            }

            /***** This is where all permissions have been asked once  *****/
            /***** and one or more were NOT granted,                   *****/
            /***** you can write the code to handle this fallback here *****/
        }
        else
        {
            // I like to give some time to the Android looper before running my App, just to be safe
            yield return new WaitForSeconds(1f); 
            
            /***** Ready to run you App *****/
        }
        
        // Slow down the loop by a little bit, not strictly needed but not harmful either
        yield return new WaitForSeconds(0.2f);
    }
}

This works even in scenarios where, for instance, the user denies one permission and then force-kills your app, or some other App grabs the foreground unexpectedly and various other risky cases I came across so far

I'll add a little plus: If you are using Google ARCore be aware that it overrides the original Unity's permission request mechanism (https://github.com/google-ar/arcore-unity-sdk/issues/151) encouraging developers to use its own GoogleARCore.AndroidPermissionsManager instead of UnityEngine.Android.Permission, as a consequence UnityEngine.Android.Permission will not work, you can still use this script but you'd need to replace all the "Permission.RequestUserPermission" with "AndroidPermissionsManager.RequestUserPermission" (you don't need to replace the "Permission.HasUserAuthorizedPermission"s, they still work), or even better, I can do it for you:

private IEnumerator RequestPermissionsRoutine()
{
    yield return new WaitForEndOfFrame();
    while (true)
    {
        if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation) && !_locationPermissionAsked)
        {
            _locationPermissionAsked = true; 
            yield return AndroidPermissionsManager.RequestPermission("android.permission.ACCESS_FINE_LOCATION").WaitForCompletion();
            continue;
        }

        if (!Permission.HasUserAuthorizedPermission(Permission.Microphone) && !_microphonePermissionAsked)
        {
            _microphonePermissionAsked = true;
            yield return AndroidPermissionsManager.RequestPermission(Permission.Microphone).WaitForCompletion();
            continue;
        }

        if (!Permission.HasUserAuthorizedPermission(Permission.Camera) && !_cameraPermissionAsked)
        {
            _cameraPermissionAsked = true;
            yield return AndroidPermissionsManager.RequestPermission(Permission.Camera).WaitForCompletion();
            continue;
        }

        if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite) && !_storagePermissionAsked)
        {
            _storagePermissionAsked = true;
            yield return AndroidPermissionsManager.RequestPermission(Permission.ExternalStorageWrite);
            continue;
        }

        if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation) ||
            !Permission.HasUserAuthorizedPermission(Permission.Microphone) ||
            !Permission.HasUserAuthorizedPermission(Permission.Camera) ||
            !Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
        {
            if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation))
            {
                /***** Tell the user to grant FineLocation Permission *****/
            }

            if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
            {
                /***** Tell the user to grant Microphone Permission *****/
            }

            if (!Permission.HasUserAuthorizedPermission(Permission.Camera))
            {
                /***** Tell the user to grant Camera Permission *****/
            }

            if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
            {
                /***** Tell the user to grant ExternalStorageWrite Permission *****/
            }
            /***** This is where all permissions have been asked once  *****/
            /***** and one or more were NOT granted,                   *****/
            /***** you can write the code to handle this fallback here *****/
        }
        else
        {
            yield return new WaitForSeconds(1f); 
            
            /***** Ready to run you App *****/
        }
        
        yield return new WaitForSeconds(0.2f);
    }
}

Upvotes: 2

Habibur Rahman Ovie
Habibur Rahman Ovie

Reputation: 281

In my case, I have used the unity`s OnApplicationFocus callback function. when permission window opens then app loses its focus mode and when permission window closed (user accept or reject permission) then app again gets its focus mode. each time OnApplicationFocus callback called.

It seems dirty but it works fine. You also have to add permissions in the manifest file.

Please see this GitHub link for see the full project

public class AndroidPermissionHandler : MonoBehaviour
{
    bool isItPermissionTime = false;
    string nextPermission;
    Stack<string> permissions = new Stack<string>();

void Start()
{
    OpenAllPermissions();
}

public void OpenAllPermissions()
{
    isItPermissionTime = true;
    CreatePermissionList();

}
void CreatePermissionList()
{
    permissions = new Stack<string>();
    permissions.Push(Permission.ExternalStorageWrite);
    permissions.Push(Permission.Camera);
    permissions.Push(Permission.CoarseLocation);
    AskForPermissions();
}
 void AskForPermissions ()
{
    if (permissions == null || permissions.Count <= 0)
    {
        isItPermissionTime = false;
        return;
    }
    nextPermission = permissions.Pop();

    if (nextPermission == null)
    {
        isItPermissionTime = false;
        return;
    }
    if (Permission.HasUserAuthorizedPermission(nextPermission) == false)
    {
        Permission.RequestUserPermission(nextPermission);
    }
    else
    {
        if (isItPermissionTime == true)
            AskForPermissions();
    }
    Debug.Log("Unity>> permission " + nextPermission + "  status ;" + Permission.HasUserAuthorizedPermission(nextPermission));
}

private void OnApplicationFocus(bool focus)
{
    Debug.Log("Unity>> focus ....  " + focus + "   isPermissionTime : " + isItPermissionTime);
    if (focus == true && isItPermissionTime == true)
    {
        AskForPermissions();
    }
}

Upvotes: 2

Roman
Roman

Reputation: 41

I encountered the same problem with a project i am working on. An easy fix is to use a coroutine to ask for permissions and yield return wait for seconds in it, between each permission. Or simply ask for permissions in different places of the application. For example, ask only for camera permission when the user is about to use it, or the write external storage when it is about to get access. Since these are sensitive permissions, it's better for the user to know the reason of why is he asked to grant these permissions beforehand.

IEnumerator Start() {
        // Ask for camera permission
        if(!Permission.HasUserAuthorizedPermission(Permission.Camera)) {
            Permission.RequestUserPermission(Permission.Camera);
        }
        yield return new WaitForSeconds(2.5f);
        // Ask for external storage permission
        if(!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite)) {
            Permission.RequestUserPermission(Permission.ExternalStorageWrite);
        }
}

The code above is working. You can adjust the wait for seconds value, and add the permissions you need. Note that i ask for the permissions only if they have not been already granted.

Cheers!

Upvotes: 0

Benjamin Zach
Benjamin Zach

Reputation: 1601

It seems that Permission.RequestUserPermission works somehow asynchronously and will not show a dialog when there's already a dialog shown - and therefore simply skips all other permissions after finding the first not authorized one.
I could bypass the problem like this:

private IEnumerator AskForPermissions()
{
#if UNITY_ANDROID
    List<bool> permissions = new List<bool>() { false, false, false, false };
    List<bool> permissionsAsked = new List<bool>() { false, false, false, false };
    List<Action> actions = new List<Action>()
    {
        new Action(() => {
            permissions[0] = Permission.HasUserAuthorizedPermission(Permission.Microphone);
            if (!permissions[0] && !permissionsAsked[0])
            {
                Permission.RequestUserPermission(Permission.Microphone);
                permissionsAsked[0] = true;
                return;
            }
        }),
        new Action(() => {
            permissions[1] = Permission.HasUserAuthorizedPermission(Permission.Camera);
            if (!permissions[1] && !permissionsAsked[1])
            {
                Permission.RequestUserPermission(Permission.Camera);
                permissionsAsked[1] = true;
                return;
            }
        }),
        new Action(() => {
            permissions[2] = Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite);
            if (!permissions[2] && !permissionsAsked[2])
            {
                Permission.RequestUserPermission(Permission.ExternalStorageWrite);
                permissionsAsked[2] = true;
                return;
            }
        }),
        new Action(() => {
            permissions[3] = ermission.HasUserAuthorizedPermission("android.permission.READ_PHONE_STATE");
            if (!permissions[3] && !permissionsAsked[3])
            {
                Permission.RequestUserPermission("android.permission.READ_PHONE_STATE");
                permissionsAsked[3] = true;
                return;
            }
        })
    };
    for(int i = 0; i < permissionsAsked.Count; )
    {
        actions[i].Invoke();
        if(permissions[i])
        {
            ++i;
        }
        yield return new WaitForEndOfFrame();
    }
#endif
}

Upvotes: 5

Related Questions