Android 13 알림 권한 요청(android.permission.POST_NOTIFICATIONS) 구현 방법 및 예제(areNotificationsEnabled(), 앱 알림 설정창 Settings.ACTION_APP_NOTIFICATION_SETTINGS인텐트 호출 방법)
아직은 아니지만 2023년 11월 부터는 구글플레이에 앱을 등록시 안드로이드 13을 타켓팅 해야한다. 그렇지 않으면 마켓에 앱을 등록할 수 없다. 여러가지 변화 중에 안드로이드 13 부터는 알림 메세지를 보냈을 때 사용자가 거부 또는 허용할 수 있도록 권한 허용을 요구해야한다. 사용자가 요구하지 않은 알림에 대한 스트레스 해소로 보인다.
무엇보다 포그라운드 서비스의 동작이 필수적으로 필요한 앱이라면 무조건 권한 허용 받아야한다.
안드로이드 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; } //} }
안드로이드 버전이 새롭게 출시될 때 마다 앱 업데이트는 항상 필수가 되는 것 같다.
[참고자료]
- https://developer.android.com/develop/ui/views/notifications/notification-permission
- https://developer.android.com/reference/android/app/NotificationManager#areNotificationsEnabled()
- https://developer.android.com/about/versions/13/changes/notification-permission?hl=ko
- https://stackoverflow.com/questions/32366649/any-way-to-link-to-the-android-notification-settings-for-my-app
[관련 자료]
- https://stackoverflow.com/questions/73067939/start-foreground-service-after-notification-permission-was-disabled-causes-crash
- https://stackoverflow.com/questions/72388741/android-manifest-post-notifications-missing-import
- https://stackoverflow.com/questions/73671885/is-there-a-difference-between-arenotificationsenabled-and-checkselfpermissi