Android 13 알림 권한 요청(android.permission.POST_NOTIFICATIONS) 구현 방법 및 예제(areNotificationsEnabled(), 앱 알림 설정창 Settings.ACTION_APP_NOTIFICATION_SETTINGS인텐트 호출 방법)

아직은 아니지만 2023년 11월 부터는 구글플레이에 앱을 등록시 안드로이드 13을 타켓팅 해야한다. 그렇지 않으면 마켓에 앱을 등록할 수 없다. 여러가지 변화 중에 안드로이드 13 부터는 알림 메세지를 보냈을 때 사용자가 거부 또는 허용할 수 있도록 권한 허용을 요구해야한다. 사용자가 요구하지 않은 알림에 대한 스트레스 해소로 보인다.

무엇보다 포그라운드 서비스의 동작이 필수적으로 필요한 앱이라면 무조건 권한 허용 받아야한다.

<Android 13 Status Bar >

안드로이드 13 알림 권한 요청 방법

다음 예제는 알림권한 요청 후 허용과 비허용에 따른 처리 예제이다.

1. Manifest.xml 파일에 android.permission.POST_NOTIFICATIONS 권한을 추가해준다.

<manifest ...>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <application ...>
        ...
    </application>
</manifest>

2. build.gradle(:app) 파일에서 compileSdkVersion과 targetSdkVersion을 33으로 상향시켜준다.

android {
    compileSdkVersion 33  // ANDROID 13
    // buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "smart.app......."
        minSdkVersion 21
        targetSdkVersion 33
        versionCode 12
        versionName "1.1.2"
        vectorDrawables.useSupportLibrary = true   //벡터이미지 사용 유무를 설정
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

3. MainActivity.java (앱의 시작뷰) 에서 안드로이드 13 티라미수 버전을 체크 메소드를 구현해주었다.

public static final int MY_PERMISSION_REQUEST_NOTIFICATION = 1020;
.....생략

if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){
    checkAndroid13();
}else if(android.os.Build.VERSION.SDK_INT >=Build.VERSION_CODES.S){
    checkAndroid12();
}
else {
    this.backgroudServiceRestart();
}

4.  알림권한이 활성화 여부를 체크하는 함수를 호출하여 확인 후 권한이 있으면 백그라운드 서비스를 시작하고 그렇지 않으면  알림다이얼로그를 호출하여 사용자에게 권한 허용을 요구한다.

private void checkAndroid13(){
    // 노티 권한 활성화 체크
    if(NotificationManagerCompat.from(GeneralActivity.this).areNotificationsEnabled()) {
        this.backgroudServiceRestart();
    }else {
        callNotiPermissionDialog();
    }
}


	
	
private void callNotiPermissionDialog() {
    try{
        if (!GeneralActivity.this.isFinishing()) {
            final Dialog personDialog = new Dialog(GeneralActivity.this);
            //        //setting custom layout to dialog
            personDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
            personDialog.setContentView(R.layout.easy_dialog_noti_guide);
            if(personDialog.getWindow()!=null)
                personDialog.getWindow().setBackgroundDrawable(new ColorDrawable(0));        //Android: how to create a transparent dialog-themed activity
            personDialog.setCancelable(false);
            Button bunConfirm = (Button) personDialog.findViewById(R.id.bunConfirm);
            bunConfirm.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (v.getId() == R.id.bunConfirm) {
                        if (!GeneralActivity.this.isFinishing() && personDialog != null && personDialog.isShowing()) {
                            personDialog.dismiss();

                            //https://developer.android.com/develop/ui/views/notifications/notification-permission
                            if (ActivityCompat.checkSelfPermission(GeneralActivity.this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {  //권한 허용상태인지 체크
                                // requestPermissions 을 통해 권한 요청
                                ActivityCompat.requestPermissions(GeneralActivity.this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, MY_PERMISSION_REQUEST_NOTIFICATION);
                            }
                        }

                    }
                }
            });


            if(!GeneralActivity.this.isFinishing() && personDialog!=null && !personDialog.isShowing()) {
                personDialog.show();
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}

<easy_dialog_noti_guide.xml 레이아웃>

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/profileChangeLayout"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_guide_shape_radius"
    android:gravity="center">

    <LinearLayout
        android:id="@+id/topLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"

        android:orientation="horizontal">

        <TextView
            android:id="@+id/txt_title_auth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:text="@string/cont_68"
            android:textColor="@color/colorxml_color_41"
            android:textSize="20dp"
            android:textStyle="bold"/>
    </LinearLayout>


    <LinearLayout
        android:id="@+id/secondLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_white_shape_radius2"
        android:gravity="left|center_horizontal"
        android:orientation="vertical"
        android:layout_below="@+id/topLayout">

        <TextView
            android:id="@+id/contentTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginTop="20dp"
            android:textColor="@color/colorxml_color_41"
            android:textSize="16sp"
            android:text="@string/cont_67"/>

        <LinearLayout
            android:id="@+id/btnLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center|center_horizontal"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginTop="20dp"
            android:layout_marginBottom="20dp"
            android:orientation="horizontal">
            <Button
                android:id="@+id/bunConfirm"
                android:layout_width="180dp"
                android:layout_height="50dp"
                android:layout_marginLeft="10dp"
                android:background="@drawable/btn_top_area_selector"
                android:text="@string/btn_setting_text2"
                android:textSize="14sp"
                android:textColor="@color/colorxml_color_41"/>
        </LinearLayout>
    </LinearLayout>




</RelativeLayout>

5.  퍼미션 콜백인 onRequestPermissionsResult 에서 승인이 된경우 백그라운드서비스를 시작해주고 미승인한 경우 다시 알림팝업을 노출하여 설정창으로 이동하도록 작성되었다.

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if(requestCode != -1) {      // && (requestCode&0xffff0000) != 0  //java.lang.IllegalArgumentException: Can only use lower 8 bits for requestCode 오류
        switch (requestCode) {
            case MY_PERMISSION_REQUEST_NOTIFICATION:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(GeneralActivity.this, "승인됨", Toast.LENGTH_LONG).show();
                    this.backgroudServiceRestart();
                } else {
                    checkNotiPostPermissions();
                    Toast.makeText(GeneralActivity.this, "미승인", Toast.LENGTH_LONG).show();
                }
                break;
            case MY_PERMISSION_REQUEST_STORAGE2: //안드로이드 P 이후의 권헌 처리
                //Toast.makeText(GeneralActivity.this, R.string.info_auth_text, Toast.LENGTH_LONG).show();
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    setSongMode(1);
                    showMainAlbumImage();
                    CallMusicListActivity();
                } else {
                    getSongProperty();
                    checkPermissions();
                    //this.callGuideDialogUsingTimer(getResources().getString(R.string.info_info_text), getResources().getString(R.string.info_auth_text), 7000);
                }
                break;
            case MY_PERMISSION_REQUEST_STORAGE:
                //Caused by java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
                //smart.app.battery.mobile.charger.MainActivity.onRequestPermissionsResult (MainActivity.java)
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
                    setSongMode(1);
                    showMainAlbumImage();
                    // 허용
                    CallMusicListActivity();
                } else {
                    //비허용
                    //Toast.makeText(this, R.string.info_auth_text, Toast.LENGTH_LONG).show();
                    //finish();
                    //Log.d("test", "Permission always deny");
                    // permission denied, boo! Disable the
                    // functionality that depends on this permission.

                    //this.callGuideDialogUsingTimer(getResources().getString(R.string.info_info_text), getResources().getString(R.string.info_auth_text), 7000);
                    getSongProperty();
                    checkPermissions();
                }
                break;

            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

6.  알림팝업을 다시 호출하는 메소드를 구현 해서 알림 설정창으로 이동시킨다.

@TargetApi(Build.VERSION_CODES.TIRAMISU)
private void checkNotiPostPermissions() {
    boolean isNotiPostRationale = shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS);
    int hasPermission = ActivityCompat.checkSelfPermission(GeneralActivity.this, android.Manifest.permission.POST_NOTIFICATIONS);

    if ( hasPermission == PackageManager.PERMISSION_DENIED && isNotiPostRationale) {
        Toast.makeText(GeneralActivity.this, getResources().getString(R.string.cont_67), Toast.LENGTH_LONG).show();
//            this.callGuideDialogUsingTimer(getResources().getString(R.string.info_info_text), getResources().getString(R.string.cont_67), 7000);
        showDialogForNotiPermissionSetting();
    } else if ( hasPermission == PackageManager.PERMISSION_DENIED && !isNotiPostRationale)
        showDialogForNotiPermissionSetting();
    else if ( hasPermission == PackageManager.PERMISSION_GRANTED ) {
//            Toast.makeText(GeneralActivity.this, "승인됨", Toast.LENGTH_LONG).show();
        this.backgroudServiceRestart();
    }
}

7. 알림 설정창에서 Settings.ACTION_APP_NOTIFICATION_SETTINGS 인텐트를 호출해주고 결과를 리턴받는다.

Intent appDetail = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
appDetail.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
//appDetail.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivityForResult(appDetail, 555);


	
	
showDialogForNotiPermissionSetting 구현한 전체 코드는 다음과 같다.
public void showDialogForNotiPermissionSetting(){
    try {
        if (!GeneralActivity.this.isFinishing()) {
            final Dialog guideDialog = new Dialog(GeneralActivity.this);
            guideDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
            guideDialog.setContentView(R.layout.easy_question_dialog);
            guideDialog.getWindow().setBackgroundDrawable(new ColorDrawable(0));        //Android: how to create a transparent dialog-themed activity
            //guideDialog.setCanceledOnTouchOutside(false);
            //guideDialog.setCancelable(false);
            //String title = getResources().getString(R.string.ic_action_name2);
            //String msg = getResources().getString(R.string.txt_8);

            TextView txtContent = (TextView) guideDialog.findViewById(R.id.txtContent);
            //TextView txtTitle = (TextView) guideDialog.findViewById(R.id.txtTitle);
            //txtTitle.setText(title);
            txtContent.setText(getResources().getString(R.string.cont_67));


            Button buttonCancel = (Button) guideDialog.findViewById(R.id.buttonCancel);
            buttonCancel.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (v.getId() == R.id.buttonCancel) {
                        if (!GeneralActivity.this.isFinishing() && guideDialog != null && guideDialog.isShowing()) {
                            guideDialog.dismiss();
                        }
                    }
                }
            });

            Button buttonConfirm = (Button) guideDialog.findViewById(R.id.buttonConfirm);
            buttonConfirm.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (v.getId() == R.id.buttonConfirm) {
                        if (!GeneralActivity.this.isFinishing() && guideDialog != null && guideDialog.isShowing()) {
                            guideDialog.dismiss();
                        }
                        try {
                            Intent appDetail = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
                            appDetail.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
//                                appDetail.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
                            startActivityForResult(appDetail, 555);
                        }catch (Exception e){
                            //e.printStackTrace();
                        }
                    }
                }
            });

            if(!GeneralActivity.this.isFinishing() && guideDialog != null && !guideDialog.isShowing()) {
                guideDialog.show();
            }

        }
    }catch (Exception e){

    }

}

<easy_question_dialog.xml 레이아웃>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/profileChangeLayout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_white_shape_radius"
    android:gravity="center"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:orientation="horizontal">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/baseline_eco_white_24"/>
        <TextView
            android:id="@+id/txtContent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="2dp"
            android:textColor="@color/colorxml_color_41"
            android:textSize="16sp"
            android:textStyle="bold"/>
    </LinearLayout>


    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="3dp"
        android:layout_marginBottom="3dp"
        android:background="@color/main_rectangle_bg_color"/>

    <LinearLayout
        android:id="@+id/bottomLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginBottom="20dp"
        android:orientation="horizontal">
        <Button
            android:id="@+id/buttonCancel"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_weight="1"
            android:background="@drawable/m_btn_selector"
            android:text="@string/btn_cancel_text"
            android:textColor="@color/colorxml_color_41"
            android:textSize="16sp" />

        <Button
            android:id="@+id/buttonConfirm"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_weight="1"
            android:background="@drawable/m_btn_selector"
            android:text="@string/info_ok_text"
            android:textColor="@color/colorxml_color_41"
            android:textSize="16sp" />
    </LinearLayout>




</LinearLayout>

7. onActivityResult 콜백메소드에서 노티 권한 허용여부를 체크 후 다시 백그라운드 서비스를 시작한다.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    switch (requestCode){
        case 555:
            // 노티 권한 활성화 체크
            if(NotificationManagerCompat.from(GeneralActivity.this).areNotificationsEnabled()) {
                this.backgroudServiceRestart();
            }
            break;
        case 999:
            textSong.setText(getMusicTitle());
            showMainAlbumImage();
            break;
        case 1212:
            getListViewData();
            break;
        case 119:
            setStatusBatteryToggle();
            backgroudServiceRestart();
            break;
        case 1818:
            //안드로이드 12 대응
            if(android.os.Build.VERSION.SDK_INT >= 30) {
                PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
                if (pm != null) {
                    if (pm.isIgnoringBatteryOptimizations(getPackageName())) {
                        this.backgroudServiceRestart();
                    } else {
                        //토스트메세지
                        CautionToast();

                        this.backgroudServiceRestart();
                        //다시호출
                        //callBatteryManagerDialog();

                    }
                }
            }

            setStatusBatteryToggle();
            break;
    }
    //}

}

안드로이드 버전이 새롭게 출시될 때 마다 앱 업데이트는 항상 필수가 되는 것 같다.

[참고자료]

[관련 자료]

카테고리 글 더 보기

error: Content is protected !!