Turn Activity when screen locked

도입

최근 알람앱을 개발하고 있는데, 알람 시간이 되면 Notification 가 울리는 것이 아닌 액티비티가 표시되는 것이 조건이었다.

원래의 해결 방법이라면, 해당 Activity 의 onCreate(Bundle) 메서드에서 setContentView 메서드가 실행되기 전 아래의 코드를 삽입하는 것으로 마무리 된다.

 window.addFlags(
            WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
            WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
            WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)

하지만 위 방법의 경우 Oreo 8.0 ~ 8.1 에서 deprecated 되었고, 이에 따라 분기처리 할 필요성이 있었다.

Oreo 8.1 에서 바뀐 부분

window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)

위 두 개 코드가 Deprecated 되었는데, 의도하지 않은 두 번의 라이프 사이클 이벤트를 방지하기 위해서 라고 한다. 이 코드 대신 사용할 수 있는 코드는 다음과 같다.

setShowWhenLocked(true)
setTurnScreenOn(true)

둘 다 Activity 에 있는 메서드이며, 각각 FLAG_SHOW_WHEN_LOCKEDFLAG_TURN_SCREEN_ON 과 같은 효과를 낸다.

Oreo 8.0 에서 바뀐 부분

window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)

위 코드가 Deprecated 되었는데, 이 이유로는 해당 플래그가 있는 액티비티가 포커스 되었을 때 Keyguard가 해제되므로 의도하지 않은 터치를 막을 수 없기 때문이라고 한다.

대신, KeyguardManager 를 통하여 keyguard 를 dismiss 하는 요청을 해야 한다.

val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager?
keyguardManager?.requestDismissKeyguard(this, null)

전체 코드

위 사항을 전부 고려하면 아래와 같은 코드가 나온다.

fun Activity.setTurnScreenOnLock() {
    val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager?
    when {
        android.os.Build.VERSION.SDK_INT >= 27 -> {
            setShowWhenLocked(true)
            setTurnScreenOn(true)
            keyguardManager?.requestDismissKeyguard(this, null)
        }
        android.os.Build.VERSION.SDK_INT == 26 -> {
            window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
            window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
            keyguardManager?.requestDismissKeyguard(this, null)
        }
        else -> {
            window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
            window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
            window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
        }
    }

    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

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);