본문 바로가기
Kotlin

[Kotlin / Android] 10. 다이얼로그와 알림 이용하기 - 알림 띄우기

by 쀼르쀼르 2024. 3. 20.

10-5 알림 띄우기


 

 

알림 채널

 

상태 바는 화면 상단의 한 줄을 의미하며

배터리, 네트워크, 시간 등 시스템의 상태 정보가 출력됨

 

이 상태 바에 앱의 정보를 출력하는 것을 알림(notification) 이라고 함

 

상태 바

 

API Level 33 버전부터는 앱에서 알림 띄우기 위해

사용자에게 퍼미션(허가)를 요청해야 함

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

 

원래 상태 바는 시스템에서 관리하는 곳이며 앱이 직접 제어할 수 없음

→ 앱에서 시스템에 의뢰하면 시스템에서 관리하는 상태 바에 앱 알림 출력할 수 있음

 

따라서 앱의 화면을 구성하거나 사용자 이벤트를 처리하는 프로그래밍과는

구조가 다르며 알림을 위해 제공하는 API를 이용해야 함

 

알림을 위한 API

 

알림은 NotificationManagernotify( ) 함수로 발생함

 

notify( ) 함수에는

NotificationCompat.Builder가 만들어 주는 Notification 객체를 대입하며

이 객체에는 알림 정보가 저장됨

 

NotificationCompat.Builder를 만들 때 NotificationChannel 정보를 대입해 줘야 함

 


정리하자면

NotificationChannel로 알림 채널을 만들고

이 채널 정보를 대입해 NotificationCompat.Builder를 만든 다음,

이 빌더로 Notification 객체를 만듦

 

Notification 객체를NotificationManagernotify( ) 함수에 대입하는 구조임


 

Notification을 만들려면 NotificationCompat.Builder가 필요함

빌더를 만드는 방법이 API 레벨 26(Android 8) 버전에 변경됨

26 버전 이전까지는 다음 생성자를 이용함

  • Builder(context: Context!)

 

 

26 버전 이전까지는 빌더 만들 때 NotificationChannel 정보가 필요 없었음

 

26 버전에서 이 생성자는 deprecated됨

 

빌더를 만들 때 다음 생성자를 사용해야 함

NotificationChannel을 만들고

이 채널의 식별값을 빌더의 생성자 매개변수에 지정해 줘야 함

  • Builder(context: Context!, channelId: String!)

 

 

API 레벨 26버전에서 채널이라는 개념 추가됨

→ 앱의 알림을 채널로 구분하겠다는 의도
    환경 설정에서 어떤 앱의 알림을 받을지 말지 설정할 수 있음

 

API 레벨 24의 알림 설정 화면

 

26 버전 이전까지는 채널이라는 개념 없어서

사용자가 설정에서 알림 안 받겠다고 하면

이 앱에서 띄우는 모든 알림 발생하지 않았음

 

26 버전부터는 채널 개념 도입하여

앱의 알림을 채널별로 구분할 수 있음

받고 싶은 채널의 알림만 선택해서 설정할 수 있음

 

API 레벨 31의 알림 설정 화면

 

하나의 앱에 채널을 2개 만들었는데 채널별로 알림 설정할 수 있음

 

· 알림 채널 생성자

NotificationChannel(id: String!, name: CharSequence!, importance: Int)

 

 

매개변수

채널의 식별값설정 화면에 표시할 채널 이름을 문자열로 지정

 

세 번째 매개변수

이 채널에서 발생하는 알림의 중요도이며 다음의 상수로 지정함

 

알림의 중요도 상수

중요도 상수 설명
NotificationManager.IMPORTANCE_HIGH 긴급 상황으로 알림음이 울리며 헤드업으로 표시
NotificationManager.IMPORTANCE_DEFAULT 높은 중요도이며 알림음이 울림
NotificationManager.IMPORTANCE_LOW 중간 중요도이며 알림음이 울리지 않음
NotificationManager.IMPORTANCE_MIN 낮은 중요도이며 알림을도 없고 상태 바에도 표시되지 않음

 

 

채널의 각종 정보는 함수나 프로퍼티로 설정할 수 있음

  • fun setDescription(description: String!) : Unit: 채널의 설명 문자열
  • fun setShowBadge(showBadge: Boolean): Unit: 홈 화면의 아이콘에 배지 아이콘 출력 여부
  • fun setSound(sound: Uri!, audioAttributes: AudioAttributes!): Unit: 알림음 재생
  • fun enableLights(lights: Boolean): Unit: 불빛 표시 여부
  • fun setLightColor(argb: Int): Unit: 불빛이 표시된다면 불빛의 색상
  • fun enableVibration(vibration: Boolean): Unit: 진동을 울릴지 여부
  • fun setVibrationPattern(vibrationPattern: LongArray!): Unit: 진동을 울린다면 진동의 패턴

 

setDescription( ) 함수에 전달하는 문자열은

설정 화면에서 채널을 설명하는 곳에 보임

 

setShowBadge(true)로 설정하면 홈 화면의 앱 아이콘에

확인하지 않은 알림 개수가 표시된 배지 아이콘이 보임

 

배지 아이콘

 

 

배지 아이콘에 표시되는 숫자는

사용자가 확인하지 않은 알림 개수가 자동으로 표시되는 것

만약 사용자가 알림을 확인하면 배지 아이콘이 자동으로 사라짐

→ 배지 아이콘 코드에서 setShowBadge(true)로 지정했어도

    사용자가 폰 설정에서 앱 배지 아이콘 출력하지 않겠다고 설정하면 안나옴

 

 

enableVibration( ) 함수를 이용해

알림이 발생할 때 진동이 울리게 할 수 있음

 

setVibrationPattern( ) 함수를  함께 이용해

진동 패턴을 설정해 특정 패턴으로 울리게 할 수 있음


 

이러한 내용 반영하여

알림 빌더(Notification.Builder) 작성할 수 있음

 

· 알림 빌더 작성

val manager = getSystemService(NOTIFICATION_SERVICE) as NofiticationManager
val builder: NotificationCompat.Builder

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val channelId = "one-channel"
    val channelName = "My Channel One"
    val channel = NotificationChannel(
        channelId,
        channelName,
        NotificationManager.IMPORTANCE_HIGH
    )
    
    // 채널에 다양한 정보 설정
    channel.description = "My Channel One Description"
    channel.setShowBadge(true)
    val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
    val audioAttributes = AudioAttributes.Builder()
        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
        .setUsage(AudioAttributes.USAGE_ALARM)
        .build()
    channel.setSound(uri, audioAttributes)
    channel.enableLights(true)
    channel.lightColor = Color.RED
    channel.enableVibration(true)
    channel.vibrationPattern = longArrayOf(100, 200, 100, 200)
    
    // 채널을 NotificationManager에 등록
    manager.createNotificationChannel(channel)
    
    // 채널을 이용해 빌더 생성
    builder = NotificationCompat.Builder(this, channelId)
} else {
    builder = NotificationCompat.Builder(this)
}

 


 

 

알림 객체

 

알림 빌더를 만들었으면이 빌더를 이용해 Notification 객체를 만들어야 함

 

이 객체에 출력할 이미지, 문자열 등의 정보를 담음

 

상태 바의 알림 이미지

 

스몰 아이콘(small icon)

- 앱에서 알림이 발생하면 상태 바에 출력되는 이미지

 

상태 바를 끌어내리면 알림 정보가 보임

 

알림 정보

 

알림은 스몰 아이콘과 발생 시각, 제목, 내용 등으로 구성됨

이러한 알림 정보를 Notification 객체에 설정해야 함

 

· 알림 객체 설정

builder.setSmallIcon(android.R.drawable.ic_notification_overlay)
builder.setWhen(System.currentTimeMillis())
builder.setContentTitle("Content Title")
builder.setContentText("Content Message")

 

빌더의 세터 함수를 이용해 알림의 구성 정보를 설정함

그 다음 NotificationManager 클래스의 notify( ) 함수를 이용해 알림을 띄움

 

· 알림 발생

manager.notify(11, builder.build())

 

builder.build( ) 함수가 Notification 객체를 만들고 이로써 알림이 발생함

 

첫 번째 매개변숫값알림을 식별하는 데 사용하는 숫자이며 개발자가 임의로 지정함

 

식별값사용자 폰에 발생한 알림을 코드에서 취소할 때 사용

 

이때 cancel( ) 함수를 이용하며 개변수로 취소할 알림의 식별값을 전달

 

· 알림 취소

manager.cancel(11)

 

사용자가 알림을 터치하면 이벤트가 발생할 수 있음

알림은 화면에서 자동으로 사라짐(취소)

 

사용자가 알림을 손가락으로 밀어서(스와이프) 취소할 수 있음

 

터치나 스와이프를 하더라도 알림 사라지지 않게 하려면빌더의 세터 함수로 지정해야 함

 

· 알림 취소 막기

builder.setAutoCancel(false)
builder.setOngoing(true)

 

setAutoCancel(false)로 지정하면

알림을 터치할 때 이벤트는 발생하지만 알림이 사라지지는 않음

 

setOngoing(true)로 지정하면

사용자가 알림을 스와이프해도 사라지지 않음

 

만약 2가지 모두 설정했다면 사용자가 알림을 취소할 수 없으며

코드에서 특정 순간에 cancel( ) 함수로 취소해야 함

 


 

 

알림 구성

 

1 ) 알림 터치 이벤트

알림은 사용자에게 앱의 상태를 간단하게 알려주는 기능을 함

사용자가 더 많은 정보를 요구할 수 있음

 

그래서 대부분 앱은 사용자가 알림을 터치했을 때 앱의 액티비티 화면을 실행

이렇게 하려면 알림의 터치 이벤트를 구현해야 함

 

 

but

알림은 앱이 관할하는 화면이 아니며시스템에서 관리하는 상태 바에 출력하는 정보

→ 알림에서 발생한 터치 이벤트는 앱의 터치 이벤트로 처리할 수 없음   
    즉,  onTouchEvent( ) 함수로는 처리할 수 없음

 

 


 

앱에서는

사용자가 알림을 터치했을 때

실행해야 하는 정보를 Notification 객체에 담아 두고,

실제 이벤트가 발생하면 Notification 객체에 등록된 이벤트 처리 내용을

시스템이 실행하는 구조로 처리함

 


 

 

알림 터치 이벤트

 

사용자가 알림을 터치하면

앱의 액티비티 또는 브로드캐스트 리시버를 실행해야 함

 

이를 실행하려면 인텐트(Intent)를 이용해야 함

 

인텐트가 있어야 알림에서 원하는 컴포넌트를 실행할 수 있음

그런데 인텐트는 앱의 코드에서 준비하지만

이 인텐트로 실제 컴포넌트를 실행하는 시점은 앱에서 정할 수 없음


 

따라서

인텐트를 준비한 후 Notification 객체에 담아서 이벤트가 발생할 때

인텐트를 실행해 달라고 시스템에 의뢰해야 함


이러한 의뢰는

PendingIntent 클래스를 이용함

 

PendingIntent 클래스는

컴포넌트별로 실행을 의뢰하는 함수를 제공

 

 

각 함수의 세 번째 매개변수인텐트 정보를 등록

  • static fun getActivity(context: Context!, requestCode: Int, intent: Intent!, flags: Int):
    PendingIntent!
  • static fun getBroadcast(context: Context!, requestCode: Int, intent: Intent!, flags: Int):
    PendingIntent!
  • static fun getService(context: Context!, requestCode: Int, intent: Intent!, flags: Int):
    PendingIntent!

 

이 함수들의 네 번째 매개변수flag값으로

똑같은 알림이 발생했을 때 어떻게 처리해야 하는지를 나타냄

 

 

여기에 입력할 상수 변수로는

FLAG_IMMUTABLE, FLAG_CANCEL_CURRENT, FLAG_MUTABLE,

FLAG_NO_CREATE, FLAG_ONE_SHOW, FLAG_UPDATE_CURRENT

가운데 하나를 지정함

 

 

API 레벨 31을 대상으로 한다면

FLAG_MUTABLE과 FLAG_IMMUTABLE 중 하나를 지정해 줘야 함

 

 

· 알림 객체에 액티비티 실행 정보 등록

val intent = Intent(this, DetailActivity::class.java)
val pendingIntent = 
    PendingIntent.getActivity(this, 10, intent,
        PendingIntent.FLAG_IMMUTABLE)
builder.setContentIntent(pendingIntent) // 터치 이벤트 등록

 

알림을 터치했을 때

DetailActivity라는 액티비티의 실행 정보를 Notification 객체에 등록하는 코드임

터치 이벤트 등록은 빌더의 setContentIntent( ) 함수를 이용함

 


 QnA 

클래스 타입의 레퍼런스를 등록할 때 뒤에 '. java'를 꼭 추가해야 할까?
어떤 코틀린 코드에서는 없는 경우도 있는데?

클래스 타입의 레퍼런스를 등록할 때 '.java'를 추가해야 하는 경우는
자바로 작성된 API를 코틀린에서 이용할 때임

but 자바에서는 클래스 타입 레퍼런스를 Class<*>로 표현하지만
코틀린에서는 KClass<*>로 표현함

만약 코틀린에서 KClass<*>로 선언된 API를 이용한다면
'.java'가 없어도 됨

즉, DetailActivity::class처럼 작성함
but 코틀린에서 자바 API를 이용할 때는
.java를 추가해서 DetailActivity::class.java로 작성해야 함

앞에서 예로 든 인텐트는 자바 API이며
두 번째 매개변수가 Class<*>로 선언되었으므로
DetailActivity::class.java처럼 작성함
Intent(packageContext: Context!, cls: Class<*>!)​

 


 

2) 액션

알림은 액션을 최대 3개까지 추가할 수 있음

알림에서 간단한 이벤트는 액션으로 처리함

 

대표적으로

알람 앱의 알람 취소, 전화 앱의 전화 수신이나 거부

 

액션

 

 

액션도 사용자 이벤트 처리가 목적임

사용자가 액션 터치할 때 실행한 인텐트 정보를

PendingIntent로 구성해서 등록해야 함

 

액션을 등록할 때는 addAction( ) 함수를 이용함

 

· 액션 등록 함수

open fun addAction(action: Notification.Action!): Notification.Builder

 

매개변수로 액션의 정보를 담는 Action 객체를 전달함

Action 객체는 Action.Builder로 만듦

 

· 액션 빌더 생성자

Builder(icon: Int, title: CharSequence!, intent: PendingIntent!)

 

액션 빌더의 생성자에 아이콘 정보와 액션 문자열,

사용자가 액션 클릭했을 때 이벤트를 위한 PendingIntent 객체를 전달함

 

· 액션 등록하기

val actionIntent = Intent(this, OneReceiver::clas.java)
val actionPendingIntent = PendingIntent.getBroadcast(this, 20, actionIntent,
    PendingIntent.FLAG_IMMUTABLE)
builder.addAction(
    NotificationCompat.Action.Builder(
        android.R.drawable.stat_notify_more,
        "Action",
        actionPendingIntent
    ).build()
)

 

 실행 결과 

 

3) 원격 입력

원격 입력(remoteInput)이란

알림에서 사용자 입력을 직접 받는 기법임

 

원래 사용자 입력 받으려면

에디트 텍스트 같은 뷰가 있는 화면을 제공해야 하는데,

 

간단한 입력은 앱의 화면을 통하지 않고

원격으로 액션에서 직접 받아 처리할 수 있음

 

RemoteInput

 

원격 입력도 액션의 한 종류임

RemoteInput에 사용자 입력을 받는 정보를 설정한 후 액션에 추가하는 구조임

 

· 원격 입력

val KEY_TEXT_REPLY = "key_text_reply"
var replyLabel: String = "답장"
var remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run {
    setLabel(replyLabel)
    build()
}

 

 

RemoteInput.Builder에 정보를 설정한 후 remoteInput 객체를 생성함

 

빌더에 들어가는 정보는

RemoteInput의 입력을 식별하는 값과 입력란에 출력되는 힌트 문자열임

 

식별값은 개발자가 임의로 작성 가능하며 사용자가 입력한 글을 가져올 때 사용됨


but

RemoteInput은 API 레벨 20에서 추가됨

앱의 minSdk를 20 아래로 설정했다면 API 레벨 호환성 고려해서 작성해야 함


 

앱의 API 레벨로 if~else 문 작성해서 처리해도 되지만호환성 돕는 라이브러리 있음

 

RemoteInput이 정의된 라이브러리 임포트할 때

android.app.RemoteInput이 아닌

androidx.core.app.RemoteInput을 이용하면 됨

 

원격 입력 라이브러리

 

RemoteInput도 액션이므로액션 터치 이벤트를 처리하기 위한 PendingIntent를 준비해야 함

 

· 인텐트 준비

val replyIntent = Intent(this, ReplyReceiver::class.java)
val replyPendingIntent = PendingIntent.getBroadcast(this, 30, replyIntent,
    PendingIntent.FLAG_MUTABLE)

 

 

RemoteInput은 알림에서 사용자의 입력을 받는 것이므로

이벤트로 액티비티를 실행해 앱의 화면이 출력되게 하지 않고

브로드캐스트 리시버를 실행해 백그라운드에서 사용자 입력을 처리하는게 일반적

 

 

알림에 액션을 등록하면서 RemoteInput 정보를 함께 설정함

· 원격 입력 액션 등록하기

builder.addAction(
    NotificationCompat.Action.Builder(
        R.drawable.send,
        "답장",
        replyPendingIntent
    ).addRemoteInput(remoteInput).build()
)

 

 실행 결과 

 

 

builderaddRemoteInput( ) 이라는 함수를 하나 더 사용함

이 함수의 매개변수로 RemoteInput 객체를 전달하면 됨

 

알림에서 사용자 입력은 앞의 코드로 처리하면 됨

 

but

전송할 때 실행되는 브로드캐스트 리시버에서

사용자가 입력한 글을 받을 때는 다음과 같은 코드를 사용함

 

· 브로드캐스트 리시버에서 사용자가 입력한 글을 받는 코드

val replyTxt = RemoteInput.getResultsFromIntent(intent)
    ?.getCharSequence("key_text_reply")

 

 

이 코드에서 중요한 부분은

getCharSequence( ) 함수의 매개변수로 지정한 문자열

 

이 문자열이 RemoteInput을 만들 때 지정한 식별값과 같아야 함

또한 브로드캐스트 리시버에서 사용자의 입력 글을 받은 후 알림을 갱신해 줘야 함

 

이때 RemoteInput의 알림을 띄울 때 사용했던 알림 객체의 식별값을 지정함

 

· 알림 갱신

manager.notify(11, builder.build())

 

이 작업은

알림에 글을 잘 받았다는 신호를 보내는 것임

이렇게 신호를 보내야 알림에서 글을 입력하는 부분이 사라짐

 

 

4) 프로그레스

앱에서 어떤 작업이 이루어지는 데 시간이 걸린다면

보통 알림을 이용해 일의 진행 상황을 프로그레스에 바로 알려줌

 

대표적인 예가

앱에서 서버로 파일을 올리거나 내려받는 경우

 

페이스북의 알림

 

 

알림의 프로그레스 바는 화면을 따로 준비하지 않고

빌더에 setProgress( ) 함수만 추가해 주면 자동으로 나옴

 

· 프로그레스 바 함수

open fun setProgress(max: Int, progress: Int, indeterminate: Boolean): Notification.Builder

 

 

첫 번째 매개변수프로그레스 바의 최댓값이며

두 번째 매개변수진행값

 

처음에 현잿값을 지정한 후 스레드(thread) 같은 프로그램을 사용해

진행값을 계속 바꾸면서 상황을 알려 주면 됨

 

세 번째 매개변숫값true이면

프로그레스 바는 왼쪽에서 오른쪽으로 계속 흘러가듯이 표현됨

 

indeterminate값이 true인 경우

· 프로그레스 바의 진행값을 증가시키는 스레드

builder.setProgress(100, 0, false)
manager.notify(11, builder.build())

thread {
    for (i in 1..100) {
        builder.setProgress(100, i, false)
        manager.notiry(11, builder.build())
        SystemClock.sleep(100)
    }
}

 

 실행 결과 

 

 


 

 

알림 스타일

 

1 ) 큰 이미지 스타일

알림에 큰 이미지를 출력할 때는 BigPictureStyle을 이용함

 

화면 캡처 알림

 

· 큰 이미지 스타일

val bigPicture = BitmapFactory.decodeResource(resources, R.drawable.test)
val bigStyle = NotificationCompat.BigPictureStyle()
bigStyle.bigPicture(bigPicture)
builder.setStyle(bigStyle)

 

 

BigPictureStyle 객체의 bigPicture 프로퍼티에 출력할 이미지를

비트맵(bitmap) 형식으로 지정하며,

이렇게 만든 BigPicture 객체를 빌더의 setStyle( ) 함수에 지정함

 

 

2 ) 긴 텍스트 스타일

알림에 긴 문자열 출력해 사용자가 앱을 실행하지 않아도

많은 정보를 알 수 있게 할 수 있음

 

대표적으로

이메일 앱은 이메일을 수신했을 때 알림으로

제목과 발신자뿐만 아니라 일부 내용도 보여 줌

 

긴 문자열 알림은 BigTextStyle을 이용함 

 

아웃룩 앱 알림

 

· 긴 텍스트 스타일

val bigTextStyle = NotificationCompat.BigTextStyle()
bigTextStyle.bigText(resources.getString(R.string.long_text))
builder.setStyle(bigTextStyle)

 

 

3 ) 상자 스타일

상자 스타일 알림은 문자열을 목록으로 출력하는 InboxStyle을 이용함

하나의 알림에 문자열을 여러 개 나열할 때 유용함

 

· 상자 스타일

val style = NotificationCompat.InboxStyle()
style.addLine("1코스 - 수락.불암산코스")
style.addLine("2코스 - 용마.아차산코스")
style.addLine("3코스 - 고덕.일자산코스")
style.addLine("4코스 - 대모.우면산코스")
builder.setStyle(style)

 실행 결과 

 

4 ) 메시지 스타일

메시지 스타일 알림은 여러 사람이 주고받은 메시지를 구분해서 출력할 때 사용함

메시지 스타일에 보일 메시지는 각각 Message 객체로 표현됨

 

· 메시지 객체

Message(text: CharSequence, timestamp: Long, sender: Person?)

 

 

하나의 메시지는 3가지 정보로 표현됨

 

첫 번째 매개변수메시지 내용

두 번째 매개변수메시지가 발생한 시각

 

but

메시지 스타일은 어떤 사람이 보낸 메시지인지 설정해야 함

 

이를 세 번째 매개변수Person 객체로 표현함

Person은 알림에 출력될 한 사람의 정보를 담는 클래스임

 

· Person 객체 생성

val sender1: Person = Person.Builder()
    .setName("kkang")
    .setIcon(IconCompat.createWithResource(this, R.drawable.person1))
    .build()
    
val sender2: Person = Person.Builder()
    .setName("kim")
    .setIcon(IconCompat.createWithResource(this, R.drawable.person2))
    .build()

 

 

Person 객체는 Person.Builder로 만들며 

빌더의 setName( ), setIcon( ) 함수를 이용해

사람의 이름과 프로필 사진을 등록할 수 있음

 

Person은 API 레벨 28 버전에 추가된 클래스이므로

API 레벨 호환성을 위해 androidx.core.app.Person 라이브러리를 임포트해야 함

 

이렇게 만든 Person 객체를 Message에 대입해서 이용함

 

· 메시지 객체 생성

val message1 = NotificationCompat.MessagingStyle.Message(
    "hello",
    System.currentTimeMillis(),
    sender1
)
val message2 = NotificationCompat.MessagingStyle.Message(
    "world",
    System.currentTimeMillis(),
    sender2
)

 

 

Message 객체를 MessageStyle에 대입함

 

· 메시지 스타일 만들기

val messageStyle = NotificationCompat.MessagingStyle(sender1)
    .addMessage(message1)
    .addMessage(message2)
builder.setStyle(messageStyle)

 

 실행 결과