유니티 퍼미션 체크 적용기. (Unity Permission Check) 2019.11.14 재작성.

 

2019.11.14 재작성.

유니티 안드로이드에서 스크린샷을 저장하기 위해 권한 부분을 다시 작성해야해서 하는 김에 이 글도 재작성 하였다.

 

2019/11/14 - [Unity/프로그래밍] - 유니티 안드로이드 스크린샷 / 스크린캡쳐 (Unity Android screenCapture)

 

유니티 안드로이드 스크린샷 / 스크린캡쳐 (Unity Android screenCapture)

유니티에서 스크린 캡쳐를 하고, 이걸 스크린샷 폴더에 저장하고 싶다. 뭐, 유니티에서 잘 지원해주겠지 하고 막상 시도해보니까 생각보다 막히는 부분이 많았다. 문제점 1. 유니티에서 지원해주는 ScreenCapture...

mentum.tistory.com

 

[주의] OBB를 사용하는 Split 빌드의 경우 반드시 저장소 권한을 획득해야함. 

 

구글 플레이에 앱을 등록하기 위해서는 Permission 체크의 이유를 공시해야한다. 

 ( 해당 공지사항 링크 : https://developer.android.com/training/permissions/requesting )

유니티에서 그냥 빌드하면 권한을 자동으로 물어버리기 때문에 권한을 요청하는 이유를 공시할 수 없게된다.

 

때문에 xml파일을 수정하여 최초 권한 묻는 부분을 disable시켜놓고, 

충분히 이유를 설명한 뒤, 동의한다면 버튼을 눌러달라는 식으로 처리하는게 일반적이다. 

 

방법은 보통 두 개로 나뉜다.

1. 최초 화면에서 모든 필수 권한들의 요청이유를 설명한 뒤,
    동의합니다 버튼을 눌러서 모든 권한을 한번에 요청함     
    -> 게임 등 간단한 앱에서 사용
2. 해당 권한이 필요할때마다 그 부분에 대해서만 요청함
    -> 요청할 사항이 많은 비 게임 앱에서 사용

 

시작하자마자 수 많은 권한을 요청합니다 사용자는 당연히 거절을 누르게 된다.

해당 기능을 사용할 때 요청하는 방식은 처음에는 거절을 눌렀지만 이래서 이게 필요하구나 라는게 인지가 되니 

구현은 어렵지만 사용자 편의를 위한 방식이라 할 수 있다.

 

원래는 유니티 자체적으로 권한을 획득하는 방법이 없어서 외부 플러그인을 사용하여 구현을 하고 있었는데, 유니티 2018.3 부터 내장 기능으로 들어왔다. 점점 쉬워지는 개발.

 

해당 공식 레퍼런스는 아래와 같다.

 

https://docs.unity3d.com/2018.3/Documentation/ScriptReference/Android.Permission.RequestUserPermission.html

 

 

공식 문서에서는 정말 간단하게 나와있는데, 단순하게 할꺼면 위에 나와있는 내용만 적어도 충분하다만,

유저 친화적으로 구현하자면 예나 지금이나 다신묻지않음을 체크한 거절이 문제다.

 

거절하신 유저분들을 앱 설정창까지 모셔 드려야 별 탈없이 앱을 서비스 할 수 있을 것이다.

앱 설정창을 여는 부분은 플러그인이 필요할거라 생각했는데, 이 또한 유니티 내부에서 처리가 가능했다.

 

자세한 코드는 아래와 같다.

아래 코드에서는 안드로이드 다이얼로그, 토스트 메시지 등을 지원하기 위해 Android Goodies라는 플러그인을 썼지만, 글 게시를 위해 주석처리했다.

해당 부분은 UGUI로 별도로 만들어서 처리하길 바란다.

 

 

이 아래의 글은 2018.3 버전 출시 이후에 작성된 옛날 방법이다.

매니페스트 생성 등의 내용만 참고하길 바람.


 

아직까지 여러가지 권한을 요청할 일은 없어서 1번 방법으로 구현하고자 한다.
구현을 위해 깃허브에서 찾은 가장 간단한 플러그인을 사용한다.

 

UnityAndroidPermissions : https://github.com/over17/UnityAndroidPermissions

 

일단 완성본은 파일첨부하니, 다운받아서 빌드해보면 됨.

[테스트 프로젝트] 유니티 2018.2.13f 로 만들어짐.

 

PermissionTest.7z
다운로드

 

 

1. 프로젝트 다운로드 

- 따로 패키지형태로 안빼놔져있으니 Assets 폴더만 zip으로 다운받아서 원래 프로젝트에 합쳐줌

 

 

2. 기본 프로젝트 설정 (빈 프로젝트 일경우)

- 빈 프로젝트라면 아직 빌드관련 설정이 안되있을테니 설정해 줘야 빌드가능.
- 플레이어 세팅 : 패키지명 설정
- 타겟플랫폼 : 안드로이드

 

3. XML 파일 만들기

AndroidManifest 는 빌드를 한번 성공하고 나면 
프로젝트 폴더의 Temp\StagingArea 경로에 생성된다.

해당 xml을 복사해서 Assets\Plugins\Android에 넣고 수정할 부분만 수정해서 사용하자.

 

2-1) 유니티에서 자동으로 권한요청을 하지않도록 추가

<meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="true" />

2-2) 원하는 권한 요청 추가

  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

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

아래는 수정된 XML. 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.회사명.앱명" xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto">
  <supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" />
  <application android:theme="@style/UnityThemeSelector" android:icon="@mipmap/app_icon" android:label="@string/app_name" android:isGame="true" android:banner="@drawable/app_banner">
    <activity android:name="com.unity3d.player.UnityPlayerActivity" android:label="@string/app_name" android:screenOrientation="sensorPortrait" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" android:hardwareAccelerated="false">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
    </activity>
    <!--최초 실행시 스킵 삭제-->
    <meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="true" />
    <meta-data android:name="unity.build-id" android:value="eb4d421b-e861-479e-8b7b-c07511fa2288" />
    <meta-data android:name="unity.splash-mode" android:value="0" />
    <meta-data android:name="unity.splash-enable" android:value="True" />
    
  </application>
  <uses-feature android:glEsVersion="0x00020000" />
  
  <!--권한 요청-->
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <!--권한 요청 종료-->
</manifest>

 

4. 캔버스생성 

 씬 또한 분리시켜서 PermissionCheck 씬, Next 씬 두개를 만들자.
최초 씬을 PermissionCheck 씬으로 설정해두고, 만약 원하는 권한이 전부 있을 경우 Next 씬을 열도록 설정할 것이다.

 일단 기본 만들어져있는 구조에서 스트립트를 꽤나 수정할텐데, 그를 위해서 PermissionCheck씬에서 유아이를 만들어주자.
- 왼쪽 : 최초 실행되었을때, 만약 필수권한이 한개라도 없다면 보여줄 캔버스
- 오른쪽 : 권한이 거부되면 보여줄 팝업창

굳이 최적화를 신경쓸 부분도 아니라서 각각을 별도의 캔버스로 만들어줬다.

 

 

5. PermissionManager 수정

 PermissionManager 스크립트를 그대로 쓰기에는 원하는 기능이랑 많이 달라서 다음과 같이 수정해줬다.

최초에 설명 화면 -> 동의합니다 버튼누름 -> 권한 요청창이 뜸 -> 동의하면 바로 Next씬으로 넘어감
                                                           거절하면 '거절하셨습니다' 팝업을 띄워줌

거절하면서 '다시묻지않음'을 체크하는 경우가 최악인데,
그 경우 앱 설정으로 바로 보내는게 제일 베스트지만 그것까지는 어떻게 처리할지 고민해봐야해서 
일단 앱 설정으로 가달라고 고지하는 방식으로 처리함.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;


public class AndroidPermissionsUsageExample : MonoBehaviour
{
    public string[] WantedPermissions = { "android.permission.READ_EXTERNAL_STORAGE",
                                         "android.permission.CAMERA",
                                         "android.permission.INTERNET",
                                         "android.permission.ACCESS_FINE_LOCATION" };
                   
    private string currentPermission;
    public GameObject canvas_popup;
    public GameObject canvas_denied;

    public void Start()
    {
        canvas_denied.SetActive(false);
        CallPermission();
    }

    // 최초 불려지는 스크립트. 
    // 다음씬에서 사용되기전에 버튼을 누르면 호출하거나 최초 씬의 start에서 호출하면 됨
    public void CallPermission()
    {
        bool isAnyFailed = false;

        // 한개라도 permission이 제대로 안되있다면 true를 반환함
        for (int i = 0; i < WantedPermissions.Length; i++)
        {
            if (CheckPermissions(WantedPermissions[i]) == false)
            {
                isAnyFailed = true;
                //return;
            }
        }

        if (isAnyFailed )
        {
            Debug.LogWarning("권한이 없습니다, 권한 승인을 해주세요");
            // 퍼미션을 왜 요청하는지 설명하는 팝업을 이 때 ON 시켜주면 됨
            // 팝업에는 버튼 하나가 있고 누르면 OnGrantButtonPress()를 호출해야함
            canvas_popup.SetActive(true);
        }
        else
        {
            // 성공한 부분. 권한을 가지고 하고싶은 일을 하면 됨
            Debug.Log("퍼미션 확인 완료..");
            SceneManager.LoadScene("Next");
        }
    }


    // 퍼미션을 체크한다.
    private bool CheckPermissions(string a_permission)
    {
        // 안드로이드가 아니면 ㅍㅊ true를 리턴시킨다.
        if (Application.platform != RuntimePlatform.Android)
        {
            return true;
        }

        return AndroidPermissionsManager.IsPermissionGranted(a_permission);
    }

    // 권한 승인 버튼에 할당합시다.
    public void OnGrantButtonPress()
    {

        for (int i = 0; i < WantedPermissions.Length; i++)
        {
            AndroidPermissionsManager.RequestPermission(new[] { WantedPermissions[i] }, new AndroidPermissionCallback(
            grantedPermission =>
            {
                // 권한이 승인 되었다.
                CallPermission();
            },
            deniedPermission =>
            {
                canvas_denied.SetActive(true);
                // 권한이 거절되었다.
            },
            deniedPermissionAndDontAskAgain =>
            {
                // 권한이 거절된데다가 다시 묻지마시오를 눌러버렸다.
                // 안드로이드 설정창 권한에서 직접 변경 할 수 있다는 팝업을 띄우는 방식을 취해야함. 
                canvas_denied.SetActive(true);
            }));
        }
    }

    // 거절했다면 닫는 기능
    public void PressDeniedCanvasButton()
    {
        canvas_denied.SetActive(false);
    }
}

PermissionManager 스크립트는 게임오브젝트를 생성해서 위의 사진과 같이 할당해줌

canvas_popup에 있는 버튼에는 OnGrantButtonPress()를 할당하고
canvas_denied에 있는 버튼에는 PressDeniedCanvasButton()을 할당함

 

 

6. 빌드 후 확인

반드시 확인해야할 부분은 다음과 같다.

1. 최초 실행 후, '알겠습니다'버튼을 눌렀을 때 권한 요청이 제대로 표기되는가?
2. 권한 요청이 표시 될 때 거절할경우 다시 요청 '알겠습니다'버튼을 누르면 요청이 되는가?
3. 권한 요청을 거절한 뒤, 앱을 껐다가 다시 켜면 요청화면이 제대로 표시되는가?
4. 권한 요청을 모두 승인했다면, 앱을 다시 켰을 때 요청화면없이 바로 Next로 넘어가는가?

 

 

7. 생각해 볼 부분

지금은 '거절함'과 '다시묻지않음'을 같이 처리했다.

다시 묻지않음의 경우, 앱 권한 요청이 실행이 되지않는다는 의미가 아니라 앱이 권한 요청을 보낼 경우 거절을 자동으로 눌러준다고 한다. 

일단은 필요한 방식으로만 처리했는데, 원래 앱에는 필수권한이라는게 있고 선택적 권한이라는게 있다. 필요하다면 스크립트를 수정해야할 듯 하다.

 

 

아래의 내용은 예전 포스트 내용의 백업

 


처음에는 다소니 닷넷에서 구현해놓은 방법으로 구현했으나, 

다소니쪽 플러그인이 1.0으로 업데이트되서 새로운 방법으로도 시도해볼까해서 플러그인을 바꿨음.

기록상으로 글은 남겨두지만, 지금에 와서는 기존 플러그인과 방법이 다르기때문에 참고가 될진 모르겠음.


 

 

Unity에서 Permission 체크를 잘 설명해놓은게 다소니 닷넷블로그 / UniAndroid Permission 깃허브 두 가지였다.

 

일단 한글로 되있어서 편할 것 같아서 다소니 닷넷의 방법으로 해보았는데, 그냥 APK를 넣어서 테스트 할때는 잘 되다가

Split 빌드로 하고 구글 플레이에 등록을 해보면 이미 권한을 가지고 있는 경우가 제대로 체크가 되지않음.

 

결국에는 OBB를 사용했을때는 저장소 권한이 없다면 두번째 씬을 로드하지 못하는데, 

XML에서 제대로 선언되어있지않아서 두번째 씬을 불러오지 못하였던 것.

 

다소니 닷넷 : https://lib.dasony.net/2

 

적용하는 과정에서 시행착오가 몇번 발생해서 막힌 부분을 정리함. 위의 링크 글을 보면서 부연 설명으로 현재 글을 보는 것을 추천.

 

 

1. XML 파일 만들기

 AndroidManifest 는 빌드를 한번 성공하고 나면 

프로젝트 폴더의 Temp\StagingArea 경로에 생성된다.

 

해당 xml을 복사해서 Assets\Plugins\Android에 넣고 수정할 부분만 수정해서 사용하자.

 

 

유니티에서 자동으로 권한요청을 하지않도록 다음과 같이 추가할 것.

 

1
<meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="true" />
cs

 

 

아래는 수정된 XML. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.회사명.앱명" xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto">
  <supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" />
  <application android:theme="@style/UnityThemeSelector" android:icon="@mipmap/app_icon" android:label="@string/app_name" android:isGame="true" android:banner="@drawable/app_banner">
    <activity android:name="com.unity3d.player.UnityPlayerActivity" android:label="@string/app_name" android:screenOrientation="sensorPortrait" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" android:hardwareAccelerated="false">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
    </activity>
    <!--최초 실행시 스킵 삭제-->
    <meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="true" />
    <meta-data android:name="unity.build-id" android:value="eb4d421b-e861-479e-8b7b-c07511fa2288" />
    <meta-data android:name="unity.splash-mode" android:value="0" />
    <meta-data android:name="unity.splash-enable" android:value="True" />
    
  </application>
  <uses-feature android:glEsVersion="0x00020000" />
  
  <!--권한 요청-->
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <!--권한 요청 종료-->
</manifest>
cs

 

 

2. 플러그인 임포트

 - 다른 플러그인 때문에 Google Resolver를 넣진 않음.

 

3. 임시 빌드

 - PermissionTest를 시작씬으로 임시 빌드해서 적용 확인.

 

3. 요청 이벤트 넣기

 예제에서는 PermissionCheck를 초기화한 다음, 특정 버튼을 누르면 CheckPermissions()를 호출하도록 해놨다. PopUp은 콜백을 돌려줄 뿐이라서 CheckPermission을 호출해주지않는다. 따라서 자동으로 호출 되도록 Start에서 초기화직후에 CheckPermissions()를 바로 호출하도록 한다. 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    void Start()
    {
        if (permissionCheck == null)
        {
            permissionCheck = new AndroidPermission();
            permissionCheck.Init();
 
            permissionCheck.OnCheckExplainAction = OnCheckExplain;
            permissionCheck.OnCheckNonExplainAction = OnCheckNonExplain;
            permissionCheck.OnCheckAlreadyAction = OnCheckAlready;
            permissionCheck.OnCheckFailedAction = OnCheckFailed;
 
            permissionCheck.OnResultAction = OnRequestResult;
        }
        
        CheckPermissions();
        
    }
cs
 
3. 요청 이벤트 예외처리 추가
 CheckPermissions()에서 요청이벤트를 했는데, 이미 권한을 동의한다음에 다시 어플리케이션을 실행하면 아무 일도 일어나지않기 때문에 다른씬을 열도록 하는 11~12줄 / 28~32줄을 추가했다.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    public void CheckPermissions()
    {
        if (permissionCheck != null && permissions != null)
        {
            string[] deninedPermissions = permissionCheck.DeninedPermissions(permissions);
 
            if (deninedPermissions != null)
            {
                if (deninedPermissions.Length <= 0)
                {
                    // 추가한 부분.
                    OpenScene();
                    return;
                }
 
                if (popup != null)
                {
                    popup.Show(delegate () {
                        permissionCheck.RequestPermissions(deninedPermissions, MULTIPLE_PERMISSION);
                    });
                }
                else
                {
                    permissionCheck.RequestPermissions(deninedPermissions, MULTIPLE_PERMISSION);
                }
            }
            else
            {
                // 추가한 부분.
                OpenScene();
                return;
            }
        }
    }
cs
 
 
 
4. 요청 승인 이벤트 결과 추가
 결과부분에서 만약 요청이 거부되었다면 어플리케이션이 종료된다. 그러나 전부 승인 되었을 경우에대한 처리도 없었기 때문에 27~30번째 줄을 추가했다.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    public void OnRequestResult(ResultEventArgs args)
    {
        if (args.denined.Length > 0)
        {
            bool forceQuit = false;
 
            for (int i = 0; i < args.denined.Length; i++)
            {
                for (int j = 0; j < needPermissions.Length; j++)
                {
                    if (args.denined[i] == needPermissions[j])
                    {
                        forceQuit = true;
                        break;
                    }
                }
 
                if (forceQuit) break;
            }
 
            if (forceQuit)
            {
                Application.Quit();
            }
        }
        else
        {
            // 추가한 부분.
            OpenScene();
        }
 
    }
cs
 

 

댓글

Designed by JB FACTORY