Android 알림 이벤트 처리

timothy jeong·2021년 11월 5일
1

Android with Kotlin

목록 보기
19/69

알림 구성

알림 터치 이벤트

알림은 사용자에게 앱의 상태를 간단하게 알려주는 기능을 하는데, 사용자가 더 많은 정보를 요구할 수 있다. 그래서 대부분 앱은 사용자가 알림을 터치했을 때 앱의 액티비티 화면을 실행한다. 그런데 이렇게 하려면 알림의 터치 이벤트를 구현해야 한다.

알림은 안드로이드 시스템의 영역이므로 원래 터치 이벤트를 처리하듯 onTouchEvent() 함수로는 처리할 수 없다. 앱에서는 사용자가 알림을 터치했을 때 실행해야 하는 정보를 Notification 객체에 담아 두고, 실제 이벤트가 발생하면 Notification 객체에 등록된 이벤트 처리 내용을 시스템이 실행하는 구조로 처리한다.

사용자가 알림을 터치했을 때 앱의 액티비티를 실행하려고 하면 인텐트(Intent) 를 이용해야 한다. 간단하게 말하면 인텐트는 앱의 컴포넌트를 실행하는데 필요한 정보 이다.

사용자가 알림 내용을 클릭했을 때 앱의 컴포넌트를 실행하려면 먼저 인텐트를 준비해야한다. 이 인텐트가 있어야 알림에서 원하는 컴포넌트를 실행할 수 있다. 그런데 인텐트는 앱의 코드에서 준비하지만 이 인텐트로 실제 컴포넌트를 실행하는 시점은 앱에서 정할 수 없다. 따라서 인텐트를 준비한 후 Notification 객체에 담아서 이벤트가 발생할 때 인텐트를 실행해 달라고 시스템에 의뢰해야한다. 이러한 의뢰는
PendingIntent클래스를 이용한다.

PendingIntent 클래스는 컴포넌트 별로 실행을 의뢰하는 함수를 제공한다. 각 함수의 세번째 매개변수에 인텐트 정보를 등록한다.

getActivity(context: Context, requestCode: Int, intent: Intent, flags: Int)
getBroadcast(context: Context, requestCode: Int, intent: Intent, flags: Int)
getService(context: Context, requestCode: Int, intent: Intent, flags: Int)

인텐트를 만들기 위해서는 Intent 클래스의 생성자를 이용한다.

val intent = Intent(packageContext: Context!, cls: Class<*>!)
// 이를 실제로 적용해보면 

val intent = Intent(this, MainActivity::class.java)

두번째 인자에 .java 를 붙이는 이유는 자바로 작성된 API를 코틀린코드에서 사용하기 때문이다. 코틀린 클래스는 kClass<> 라고 표시가 붙는다. Intent 주생성자는 Class<> , 즉 Java 클래스를 인자로 받기 때문에 뒤에 .java 를 붙인 것이다.

Intent 를 만들었으면, 이를 PendingIntent 클래스를 만들어야 한다. 이는 다음과 같이 만든다.

 val pendingIntent = PendingIntent.getActivities(this, 10,
     intent.toTypedArray(), PendingIntent.FLAG_IMMUTABLE)

모든 코드르 합치면 아래처럼 된다.

        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        val notificationBuilder: NotificationCompat.Builder = getNotificationBuilder(this, manager)
        notificationBuilder.setSmallIcon(R.drawable.ic_launcher_foreground)
        notificationBuilder.setWhen(System.currentTimeMillis())
        notificationBuilder.setContentTitle("hello")
        notificationBuilder.setContentText("This is Notification")

        val intentList = mutableListOf<Intent>()
        intentList.add(Intent(this, MainActivity::class.java))
        val pendingIntent = PendingIntent.getActivities(this, 11, intentList.toTypedArray(), PendingIntent.FLAG_IMMUTABLE)

        notificationBuilder.setContentIntent(pendingIntent)

        button.setOnClickListener {manager.notify(11, notificationBuilder.build())}

액션

알림에는 터치 이벤트 이외에도 액션을 최대 3개까지 추가할 수 있다. 알림에서 간단한 이벤트는 액션으로 처리된다. 알람 앱의 알람 취소, 전화 앱의 전화 수신이나 거부 등이 대표적인 예이다.

이러한 액션도 사용자 이벤트 처리가 목적이다. 따라서 알림 터치 이벤트와 마찬가지로 사용자가 액션을 터치할 때 실행할 인텐트 정보를 PendingIntent 로 구성 해서 등록해야 한다. 실제 사용자가 액션을 터치하면 등록된 인텐트가 시스템에서 실행되어 이벤트가 처리되는 구조이다.

액션을 등록할 때는 addAction() 함수를 이용한다. 매개변수로 액션의 정보를 담는 Action 객체를 전달한다. Action 객체는 Action.Builder 로 만든다.

addAction(action: Notification.Action!): Notification.Builder
Bulder(icon: Int, title: CharSequence!, intent: PendingIntent!)

액션 빌더의 생성자에 아이콘 정보와 액션 문자열, 그리고 사용자가 액션을 클릭했을 때 이벤트를 위한 PendingIntent 객체를 전달한다. 이때 액티비티가 아니라 리시버와 브로드캐스트를 이용한다.

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

프로그레스

앱에서 어떤 작업이 이루어지는데 시간이 걸린다면 보통 알림을 이용해 일의 진행 상황을 알려준다. 서버로 파일을 올리거나 다운 받을 때가 대표적인 예이다.

알림은 프로그래스 화면을 따로 준비하지 않고, setProgress() 함수만 추가해 주면 자동으로 나온다.

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

두번째 매개변수가 진행값이다. 처음에는 현잿값을 지정한 후 스레드 같은 프로그램을 사용해 진행값을 계속 바꾸면서 상황을 알려주면 된다. 그리고 만약 세 번째 매개변숫값이 true이면 프로세스 바는 왼쪽에서 오른쪽으로 계속 흘러가듯 표현된다.

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

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

원격 입력

원격 입력(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 은 API 레벨 20에서 추가되었다. 따라서 앱의 minSdk Version을 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, PnedingIntent.FLAG_IMMUTABLE)

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

액션을 처리하는 코드와는 다르게 addRemoteInput() 이라는 함수가 더해졌다. 이 함수의 매개변수로 RemoteInput 객체를 전달한다.

이상의 코드로 알림에서 사용자의 입력을 처리할 수 있다. 그런데 전송할 때 실행되는 브로드캐스트 리시버에서 사용자가 입력한 글을 받을 때는 당므과 같은 코드를 이용한다.

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

getCharSequence() 가 매개변수로 받는 식별자가 RemoteInput 을 만들때 넣은 식별자와 같아야 한다. 또한 브로드캐스트 리시버에서 사용자의 입력 글을 받은 후 알림을 갱신해 줘야하낟. 이때 RemoteInput 의 알림을 띄울 때 사용했던 알림 객체의 식별값을 지정해야한다.

만약 카카오톡 처럼 메시지를 받으면 답장을 보내는 기능을 만든다고 하면, 아래와 같이 직접 Receiver 를 만들고 Intent 에 해당 Receiver 를 넣고, PendingIntent 는 getBroadcast 를 통해 구현해야한다.

class ReplyReceiver:BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        val replyTxt = RemoteInput.getResultsFromIntent(intent)?.getCharSequence("key_text_reply")
        Log.d("Info", "replyTxt : $replyTxt")

        val manager = context?.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
        manager.cancel(11)
    }
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.notificationButton.setOnClickListener {
            val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            val builder: NotificationCompat.Builder

            // notification channel 생성
            if (Build.VERSION.SDK_INT >= 26) {
                val channelId = "one-channel"
                val channelName = "My Channel One"

                // channel 세부 정보 구성
                val channel = NotificationChannel(
                    channelId, channelName, NotificationManager.IMPORTANCE_HIGH
                ).apply {
                    description = "My Channel One DESC"
                    setShowBadge(true)

                    val uri: Uri =RingtoneManager
                        .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)

                    val audioAttributes = AudioAttributes.Builder()
                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                        .setUsage(AudioAttributes.USAGE_ALARM)
                        .build()
                    setSound(uri, audioAttributes)
                    enableVibration(true)
                }
                manager.createNotificationChannel(channel)
                builder = NotificationCompat.Builder(this, channelId)
            } else {
                builder = NotificationCompat.Builder(this)
            }

            builder.run {
                setSmallIcon(R.drawable.small)
                setWhen(System.currentTimeMillis())
                setContentTitle("title")
                setContentText("Hello~~")
                setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.big))
            }

            val KEY_TEXT_REPLY = "key_text_reply"
            var replyLabel = "답장"
            var remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run {
                setLabel(replyLabel)
                build()
            }
            val replyIntent = Intent(this, ReplyReceiver::class.java)
            val replyPendingIntent  = PendingIntent.getBroadcast(this, 30, replyIntent, PendingIntent.FLAG_MUTABLE)

            builder.addAction(
                NotificationCompat.Action.Builder(
                    R.drawable.send,
                    "답장",
                    replyPendingIntent
                ).addRemoteInput(remoteInput).build()
            )
            manager.notify(11, builder.build())
        }
    }
}
profile
개발자

0개의 댓글