Notification Sound on Android 8.0

도입

Android 8.0 에 오면서부터 Notification Channel 라는 개념이 도입되면서, 알림을 각각 ‘채널’ 로 분류하여 각 채널마다 중요도를 설정, 표시하는 기능이다.

평소대로 ID 를 만들어 채널을 설정하고 알림을 띄웠지만, NotificationCompat.setSound 에 소리를 설정해도 기본 알림 소리만 나왔다. 약 30분 동안 삽질한 결과, NotificationChannel 클래스 자체에 setSound 라는 메서드가 있었다.

해결 방법

Uri soundUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.noti_sound);

if (Build.VERSION.SDK_INT >= 26) {
    CharSequence name = getString(R.string.app_name);
    String description = getString(R.string.app_name);
    int importance = NotificationManager.IMPORTANCE_HIGH;

    NotificationChannel mChannel = new NotificationChannel(id, name, importance);
    mChannel.setDescription(description);

    mChannel.enableVibration(true);
    AudioAttributes audioAttributes = new AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_NOTIFICATION)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build();

    mChannel.setSound(soundUri, audioAttributes);

    manager.createNotificationChannel(mChannel);
}

단 주의할 점은 생성된 알림 채널은 앱을 지우지 않는 이상 지워지지 않는다는 것이다. 즉, 채널 id를 새로 생성하는 수 밖에 없다.

Android 8.0 Oreo 대응 메뉴얼

안드로이드 Oreo (8.0, API 26) 부터는 기존 버전과 다르게 두 가지의 섹션으로 나뉘는데, 앱의 Target 버전이 26 미만인데도 영향을 받는 모든 API 레벨을 대상으로 하는 앱 과 앱의 Target 버전이 26 이상에서만 영향을 받는 Android 8.0를 대상으로 하는 앱 으로 나뉩니다.

모든 API를 대상으로 하는 앱 변경 사항

백그라운드 실행 제한

백그라운드에서 실행될 때 마다 앱은 기기의 제한된 리소스(RAM)을 사용합니다. 이 경우 사용자 환경이 손상될 수 있으며, 게임 재생이나 동영상 보기 등 리소스를 많이 소모하는 앱은 특히 그렇습니다. Android 8.0는 사용자 경험을 개선하기 위해 백그라운드에 실행되는 앱의 동작을 제한합니다.
이 백그라운드 제한은 두 가지의 제한 방법으로 나뉩니다.

백그라운드 서비스 제한: 앱이 유휴 상태일 경우 백그라운드 서비스의 사용이 제한됩니다. 이 기능은 사용자에게 잘 보이는 포그라운드 서비스 에는 적용되지 않습니다.
브로드캐스트 제한: 더 이상 암시적 브로드캐스트를 메니페스트에 등록할 수 없습니다. 하지만 여전히 런타임 상에서 등록하는 것은 작동이 됩니다.

기본적으로 이들 제한은 Oreo를 대상으로 하는 앱에만 적용되나, 그러나 앱이 Oreo를 대상으로 하지 않더라도 사용자가 설정 화면에서 이들 앱에 대한 제한을 활성화 할 수 있습니다.
대부분의 경우 JobScheduler 작업을 통하여 이러한 제한을 해결할 수 있습니다.

백그라운드 서비스 제한

시스템은 포그라운드 앱과 백그라운드 앱을 구분합니다. 다음의 경우에만 앱이 포그라운드 영역에 있다고 간주됩니다.

  • 액티비티가 시작되거나(onCreate), 일시 중지되거나(onPause) 상관 없이 사용자의 화면에 보이는 액티비티가 있는 경우
  • 포그라운드 서비스가 있는 경우
  • 앱의 콘텐츠 제공자 중 하나를 사용하여 앱에 또 다른 포그라운드 앱이 연결된 경우. (예, IME, 배경화면 서비스, 알림 리스너, 음성 또는 텍스트 서비스 )
  • 백그라운드 서비스이나 bindService 를 통해 바인드된 서비스가 있을 경우

위의 어떤 조건에도 해당하지 않는 경우 앱은 백그라운드에 있는 것으로 간주됩니다.
앱이 포그라운드에 있는 동안에는 이 앱이 포그라운드 및 백그라운드 서비스를 자유롭게 생성하고 실행할 수 있고, 포그라운드에서 백그라운드로 이동한다고 해도 몇 분 정도의 기간 동안(평균 5분) 은 앱이 서비스를 생성하고 사용하는 것이 여전히 허용됩니다. 이 기간이 끝나면 앱이 유후 상태로 간주되며 이 경우 서비스는 자기 자신의 서비스를 중지합니다.
다만 다음의 경우에는 앱이 유휴 상태로 간주되더라도 몇 분 동안 임시 허용 목록에 들어갈 수 있습니다.

  • 우선순위가 높은 FCM 메세지 처리
  • SMS / MMS 메세지와 같은 브로드캐스트 수신
  • 알림에서 PendingIntent 실행

포그라운드 서비스의 조건

Android Oreo 이후부터는 백그라운드 서비스에 대한 포그라운드 서비스 승격이 startForeground() 로 알림 영역에 Ongoing 알림을 표시하면 되었으나 Android Oreo 부터는 Context.startForegroundService() 를 호출한 뒤 5초 이내에 startForeground를 호출해야 합니다. 만약 그렇지 않을 경우 시스템은 서비스를 중지하고 앱을 ANR (Android Not Responding) 상태로 간주합니다.

브로드캐스트 제한

더 이상 암시적 브로드캐스트를 메니페스트에 등록할 수 없습니다. 그 대신 런타임에 등록해서 여전히 사용할 수 있습니다. 예를 들어, 충전기에 연결되었을 때 작업을 수행한다고 해야 되면 JobScheduler 를 통하여 작업을 수행하도록 예약할 수 있습니다.
현재 Oreo 에서는 많은 암시적 브로드캐스트 들이 예외 목록에 들어갔지만 추후 버전부터는 예외 목록이 줄어들 예정입니다. 해당 목록은 여기서 볼 수 있습니다.

마이그레이션 가이드

백그라운드 서비스 제한

  • 앱이 백그라운드에 있는 동안 포그라운드 서비스를 생성해야 하는 경우, 백그라운드 서비스를 생성하고 이 서비스를 포그라운드로 승격시키려고 시도하는 대신 새 NotificationManager.startServiceInForeground() 메서드를 사용합니다.
  • 서비스가 사용자에게 보이는 경우, 이 서비스를 포그라운드 서비스로 만듭니다. 예를 들어, 오디오를 재생하는 서비스는 항상 포그라운드 서비스여야 합니다. startService() 대신 NotificationManager.startServiceInForeground()를 사용하여 서비스를 생성합니다.
  • 서비스의 기능을 예약된 작업으로 복제하는 방법을 찾습니다. 서비스가 사용자에게 즉시 보이는 작업을 수행하고 있지 않은 경우, 대신 예약된 작업을 사용할 수 있어야 합니다.
  • 네트워크 이벤트 발생 시 선택적으로 애플리케이션을 깨우려면, 백그라운드에서 폴링을 수행하는 대신 FCM을 사용합니다.
  • 애플리케이션이 자연스럽게 포그라운드가 될 때까지 백그라운드 작업을 연기합니다.

브로드캐스트 제한

  • 앱 매니페스트에 정의된 브로드캐스트 수신기를 검토합니다. 암시적 브로드캐스트에 대한 수신기가 매니페스트에 선언된 경우, 이 수신기를 교체해야 합니다. 가능한 해결책은 다음과 같습니다.
  • 수신기를 매니페스트에 선언하는 대신, 런타임에 Context.registerReceiver()를 호출하여 수신기를 생성합니다.
  • 예약된 작업을 사용하여 암시적 브로드캐스트를 트리거했던 조건을 확인합니다.

백그라운드 위치 제한

Android 8.0는 소비전력 절감을 위해 앱의 대상 SDK 버전에 상관없이 백그라운드 앱이 사용자의 현재 위치를 검색할 수 있는 빈도를 제한합니다.

시스템은 포그라운드 앱과 백그라운드 앱을 구분합니다. 다음의 경우에만 앱이 포그라운드 영역에 있다고 간주됩니다.

  • 액티비티가 시작되거나(onCreate), 일시 중지되거나(onPause) 상관 없이 사용자의 화면에 보이는 액티비티가 있는 경우
  • 포그라운드 서비스가 있는 경우
  • 앱의 콘텐츠 제공자 중 하나를 사용하여 앱에 또 다른 포그라운드 앱이 연결된 경우. (예, IME, 배경화면 서비스, 알림 리스너, 음성 또는 텍스트 서비스 )
  • 백그라운드 서비스이나 bindService 를 통해 바인드된 서비스가 있을 경우

이에 백그라운드에서 실시간 위치를 가져와야 하는 앱은 매 시간 몇 차례만 위치 업데이트가 제공됩니다. 단 앱이 포그라운드에 있을 때는 제한이 걸리지 않습니다.

경고창

SYSTEM_ALERT_WINDOW 와 같은 다른 앱 위에 오버레이 되어 그려지는 앱의 경우 TYPE_APPLICATION_OVERLAY 유형으로 표시되는 오버레이의 뒤에 표시되게 됩니다. TYPE_APPLICATION_OVERLAY 는 아래 특징을 가지고 있습니다.
앱의 경고 창은 항상 주요 시스템 창(예: 상태 표시줄 및 IME) 아래에 나타납니다.
시스템에서 화면 표시를 개선하기 위해 TYPE_APPLICATION_OVERLAY 창 유형을 사용하는 창을 이동하거나 크기를 조정할 수 있습니다.
알림 창을 열면, TYPE_APPLICATION_OVERLAY 창 유형을 사용해 나타나는 경고 창을 앱이 표시하지 못하도록 차단하는 설정을 사용자가 액세스할 수 있습니다.

포착되지 않는 예외 로그 기록

기본 Thread.UncaughtExceptionHandler를 호출하지 않는 Thread.UncaughtExceptionHandler를 앱이 설치하는 경우, 시스템은 포착되지 않는 예외가 발생할 때 앱을 중단하지 않습니다. Android 8.0부터, 시스템에서는 이런 상황이 발생할 때 예외 스택 추적을 로그에 기록합니다. 플랫폼의 이전 버전에서는 예외 스택 추적을 기록하지 않았습니다.
사용자설정 Thread.UncaughtExceptionHandler 구현은 항상 기본 핸들러를 호출하는 것이 좋습니다. 이 권장 사항을 따르는 앱은 Android 8.0의 변경 사항에 영향을 받지 않습니다.

Android 8.0를 대상으로 하는 앱 변경 사항

경고창

더 이상 SYSTEM_ALERT_WINDOW를 사용해 오버레이를 표시할 때 아래 유형을 사용할 수 없습니다.

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_OVERLAY
  • TYPE_SYSTEM_ERROR

뷰 포커스

클릭 가능한 View 객체는 이제 기본적으로 포커스도 가능합니다. View 객체를 클릭할 수는 있지만 포커스를 받을 수 없도록 하려면, View를 포함하는 레이아웃 XML 파일에서 android:focusable 속성을 false로 설정하거나, 앱의 UI 로직에서 setFocusable()로 false를 전달합니다.

권한

Android 8.0 이전에는 앱이 런타임에 권한을 요청하고 권한이 허용된 경우, 시스템 역시 같은 권한 그룹에 속하고 매니페스트에서 등록된 나머지 권한을 앱에 잘못 허용했습니다.
Android 8.0를 대상으로 하는 앱의 경우 이 동작이 수정되었습니다. 앱에는 명시적으로 요청한 권한만 허용됩니다. 하지만 사용자가 앱에 권한을 허용하고 나면 해당 권한 그룹에서 권한에 대한 이후의 모든 요청이 자동으로 허용됩니다.
예를 들어, 앱이 매니페스트에 READ_EXTERNAL_STORAGE 와 WRITE_EXTERNAL_STORAGE를 모두 나열한다고 가정합니다. 앱이 READ_EXTERNAL_STORAGE를 요청하고 사용자가 이를 허용합니다. 기존에는 시스템이 WRITE_EXTERNAL_STORAGE를 동시에 허용합니다. 같은 STORAGE 권한 그룹에 속하고 매니페스트에도 등록되기 때문입니다. 앱이 Android 8.0를 대상으로 지정하는 경우에는 시스템이 READ_EXTERNAL_STORAGE 만 허용합니다.
따라서 런타임 권한 실행 시 같은 그룹의 다른 퍼미션도 같이 허용을 하도록 변경해야 합니다.

알림 (Notification)

NotificationManager.Builder(Context) 가 Deprecated 됨에 따라 알림채널을 생성해서 넣어야 제대로 표시됩니다. (알림 채널을 생성하지 않으면 알림이 표시되지 않습니다.)

String id = "PyxisPub on UzukiLive";
NotificationManager notificationManager = RichUtils.getNotificationManager(this);

if (Build.VERSION.SDK_INT >= 26) {
    CharSequence name = getString(R.string.app_name);
    String description = getString(R.string.app_name);
    int importance = NotificationManager.IMPORTANCE_LOW;
    NotificationChannel mChannel = new NotificationChannel(id, name, importance);
    mChannel.setDescription(description);
    notificationManager.createNotificationChannel(mChannel);
}

NotificationCompat.Builder builder = new NotificationCompat.Builder(this, id);

 

 

 

Android – styles.xml로 커스텀 폰트 전역적용

Android Oreo에서 새로 나온 기능 중 Fonts in XML (커스텀 폰트 in XML)이 있다.

즉, 다른 리소스 (.jpg, .png 와 같은 이미지 리소스, strings.xml 같은 문자열 리소스 등) 과 같이 폰트 자체를 그 하나의 리소스로 취급, 폴더로 관리할 수 있게 하는 것이다.

font 폴더를 만들고 그 안에 ttf, otf 폰트 파일을 넣으면 R.font.goyang 로서 바로 사용할 수 있다.

파일을 더블 클릭하면 익숙한 로렘 입숨을 보여주는데, 표시되는 곳에 추가로 글자를 작성할 수 있어 언어가 제대로 표시하는지도 테스트 할 수 있다.

커스텀 폰트 를 위한 미리보기

사용법도 매우 간단하다.

<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="@font/goyang"/>

또는

Typeface typeface = getResources().getFont(R.font.goyang);
textView.setTypeface(typeface);

이런 식으로, 더 이상 assets에 넣고 AssetsManager로부터 불러와서 LruCache에 넣고 캐시를 해서 적용시키는 그런 작업이 필요 없게 되었다.

지금 하는 프로젝트는 기본 앱 폰트가 노토 산스였는데, android:fontFamily 를 사용하면 스타일에 정의하는 것 만으로도 전체에 적용될 것 같았다.

0. 들어가기 앞서..

구글이 배포하는 노토 산스를 그대로 넣으면 앱의 용량이 매우 늘어나게 된다.

안 그래도 PDF 모듈 등 다소 JNI 떡칠 + protobuf 떡칠이 되어있어 앱의 용량이 기하급수적인데, 여기에 폰트까지 합치면… 생각하기도 무섭다.

그래서, 한글 웹 폰트용으로 경량화된 NotoSansKR-Hestia 의 otf 파일만 가져왔다. (실제는 한글 웹 폰트 경량화해 사용하기 에서 온 것이다.)

어디선가 파란 줄의 여신님이 벨 군~ 하면서 달려오는 것 같지만, 무시하기로 한다.

1. Noto Sans Font-Family 만들기

<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:app="http://schemas.android.com/apk/res-auto"
             xmlns:tools="http://schemas.android.com/tools">
    <font
        android:font="@font/noto_hestia_normal"
        android:fontStyle="normal"
        android:fontWeight="400"
        app:font="@font/noto_hestia_normal"
        app:fontStyle="normal"
        app:fontWeight="400"
        tools:ignore="UnusedAttribute"/>

    ...
</font-family>

Android Oreo 보다 낮은 버전에서도 폰트를 제대로 불러오기 위해, 별도의 app namespace를 사용했다. (구글의 설계상 API 14부터 되는 것 같다.)

android:font 에는 사용할 실제 폰트 리소스를, fontStyle에는 해당 폰트 스타일(normal 등) , fontWeight는 글꼴의 굵기이다.

적절히 이 3개 값을 설정하고 똑같은 값을 app namespace에도 대응시킨다.

이렇게 사용할 폰트를 정리하고, styles.xml을 수정하면 된다.

2. Styles.xml 적용하기

적용시킬 위젯은 총 5개로, 텍스트뷰, 버튼, 에디트 텍스트, 라디오 버튼, 체크 박스 등이다.

각각의 DeviceDefault 테마를 상속받아 fontFamily를 적용시킨다.

<style name="NotoSansTextViewStyle" parent="@android:style/Widget.DeviceDefault.TextView">
        <item name="android:fontFamily">@font/noto_serif</item>
</style>

<style name="NotoSansButtonStyle" parent="@android:style/Widget.DeviceDefault.Button.Borderless">
        <item name="android:fontFamily">@font/noto_serif</item>
</style>

<style name="NotoSansEditTextStyle" parent="@android:style/Widget.DeviceDefault.EditText">
        <item name="android:fontFamily">@font/noto_serif</item>
</style>

<style name="NotoSansRadioButtonStyle" parent="@android:style/Widget.DeviceDefault.CompoundButton.RadioButton">
        <item name="android:fontFamily">@font/noto_serif</item>
</style>

<style name="NotoSansCheckboxStyle" parent="@android:style/Widget.DeviceDefault.CompoundButton.CheckBox">
        <item name="android:fontFamily">@font/noto_serif</item>
</style>

3. 테마 Styles.xml 적용하기

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
 <!-- Customize your theme here. -->
 <item name="colorPrimary">@color/colorPrimary</item>
 <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
 <item name="colorAccent">@color/colorAccent</item>
 <item name="android:buttonStyle">@style/NotoSansButtonStyle</item>
 <item name="android:editTextStyle">@style/NotoSansEditTextStyle</item>
 <item name="android:radioButtonStyle">@style/NotoSansRadioButtonStyle</item>
 <item name="android:checkboxStyle">@style/NotoSansCheckboxStyle</item>
 <item name="android:textViewStyle">@style/NotoSansTextViewStyle</item>
</style>

각각 buttonStyle, editTextStyle, radioButtonStyle, checkboxStyle, textviewStyle 에 위에 만든 스타일을 대응시킨다.

마무리

이거 하나 때문에 프로젝트 시작 단계에서 베타 버전의 Android Oreo를 타겟으로 하고 개발했던 것은 숨겨진 진실이기도 하다.