1. 브로드캐스트 리시버 이해하기
브로드캐스트 리시버는 흔히 이벤트 모델로 실행되는 컴포넌트라고 정의합니다. 여기서 이벤트란 액티비티를 다룰 때 살펴본 사용자 이벤트가 아니라 부팅이 완료되는 것과 같은 시스템의 특정한 상황을 의미합니다. 시스템에 어떤 이벤트가 발생하면 브로드캐스트 리시버를 실행해 앱이 해당 상황에 맞게 동작하도록 할 수 있습니다.
브로드캐스트 리시버는 줄여서 리시버라고도 하며 우리말로 직역하면 '방송 수신기'라고 할 수 있습니다. 즉, 시스템에서 특정한 상황을 알리는 방송을 할 때(이벤트 발생) 이를 받아서 처리하는 수신기를 앱에 장착한다고 생각하면 쉽습니다. 이 수신기도 액티비티처럼 안드로이드의 컴포넌트이므로 인텐트를 시스템에 전달함으로써 실행합니다.
브로드캐스트 리시버 만들기
브로드캐스트 리시버를 만들려면 BroadcastReceiver를 상속받는 클래스를 선언해야 합니다. 브로드캐스트 리시버의 생명주기 함수는 onReceive() 하나뿐입니다. 어디선가 이 리시버를 실행하려고 인텐트를 시작하면 onReceive() 함수가 자동으로 호출됩니다. 그리고 자신을 호출한 인텐트 객체를 매개변수로 전달받습니다.
class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
}
}
onReceive() 함수는 실행한 후 10초 이내에 완료할 것을 권장하므로 오래 걸리는 작업을 담당하기에는 부적절합니다. onReceive() 함수가 실행을 마치면 브로드캐스트 리시버 객체는 소멸합니다. 브로드캐스트 리시버도 컴포넌트이므로 매니페스트 파일에 등록합니다. 브로드캐스트 리시버를 등록하는 태그는 <receiver> 이며 필수 속성은 클래스명을 지정하는 name입니다. 브로드캐스트 리시버를 명시적 인텐트로 실행하려면 클래스명만 등록하고, 암시적 인텐트로 실행하려면 <intent-filter>를 선언해줘야 합니다.
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="true"></receiver>
동적 등록과 해제
리시버를 매니페스트에 등록하지 않고 코드에서 필요한 순간에 동적으로 등록할 수도 있습니다.
다음 코드는 액티비티나 서비스 컴포넌트에서 작성한 코드입니다.
receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
}
}
브로드캐스트 리시버 객체는 액티비티나 서비스에서 생성할 수 있습니다. 그리고 필요한 순간에 다음처럼 registerReceiver() 라는 함수를 이용해 시스템에 등록합니다.
val filter = IntentFilter("ACTION_RECEIVER")
registerReceiver(receiver, filter)
이처럼 브로드캐스트 리비서를 코드에서 등록하면 매니페스트에 <receiver> 태그로 등록하지 않아도 시스템에서 리시버의 존재를 인지합니다.
registerReceiver() 함수로 등록한 리시버는 사용한 후 필요 없으면 해재해 줘야 합니다. 이 때 unregisterReceiver() 함수를 이용합니다.
unregisterReceiver(receiver)
브로드캐스트 리시버 실행하기
브로드캐스트 리시버를 실행하려면 인텐트가 필요합니다. 리시버의 클래스명만 등록했으면 클래스 타입 레퍼런스를 이용해 명시적 인텐트를 실행하고, 인텐트 필터를 등록했으면 암시적 인텐트로 실행합니다.
브로드캐스트 리시버를 실행하는 인텐트는 sendBroadcast() 함수로 시스템에 전달합니다. 그러면 시스템은 브로드캐스트 리시버 객체를 생성하여 실행해줍니다.
val intent = Intent(this, MyReceiver::class.java)
sendBroadcast(intent)
브로드캐스트 리시버는 액티비티와 달리 시스템에 해당 인텐트로 실행될 리시버가 없으면 아무런 일도 일어나지 않습니다. 즉, 인텐트를 시작한 곳에서 오류가 발생하지 않습니다. 또한 실행될 리시버가 여러 개 있으면 모두 실행됩니다. 즉, '없으면 말고 있으면 모두 실행' 하는 형태입니다.
2. 시스템 상태 파악하기
시스템에서 발생하는 인텐트는 여러 종류가 있으며 부팅 완료, 화면 켬/끔, 배터리 상태 등이 대표적입니다.
부팅 완료
만약 앱에서 부팅이 완료될 때 특정한 작업을 수행하고 싶다면 브로드캐스트 리시버를 만들고 매니페스트 파일에 인텐트 필터를 구성해서 등록합니다.
부팅이 완료되면 시스템에서는 android.intent.action.BOOT_COMPLETED 라는 액션 문자열을 포함하는 인텐트가 발생합니다. 이때 실행할 리시버에는 <action>의 name 속성에 똑같은 액션 문자열을 똑같이 등록합니다.
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
그런데 리시버를 실행하려면 권한이 필요하므로 매니페스트에 아래처럼 퍼미션을 추가해줘야 합니다.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
화면 켬/끔
화면을 켜거나 끄는 상황을 감지하는 브로드캐스트 리시버는 매니페스트에 등록하면 실행되지 않습니다. 앞에서 본 것처럼 액티비티나 서비스 컴포넌트의 코드에서 registerReceiver() 함수를 이용해 동적으로 등록해야만 합니다.
화면을 켤 때와 끌 때를 구분해서 브로드캐스트 리시버를 따로 만들어도 되지만 다음처럼 하나의 리비서에서 켜고 끄는 상황을 모두 감지하도록 할 수도 있습니다. 다음 코드에서는 android.intent.action.SCREEN_ON과 android.intent.action.SCREEN_OFF 액션 문자열을 상수 변수로 사용했습니다.
receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_ON -> Log.d("kkang", "screen on")
Intent.ACTION_SCREEN_OFF -> Log.d("kkang", "screen off")
}
}
}
이렇게 하면 onReceive() 함수의 매개변수인 Intent 객체에서 자신을 호출한 인텐트의 액션 문자열을 가져와 화면을 켜거나 끄는 상황을 판단 할 수 있습니다.
이렇게 정의한 브로드캐스트 리시버 객체는 액션 문자열 정보로 구성한 인텐트 필터와 함께 registerReceiver() 함수로 시스템에 등록합니다.
val filter = IntentFilter(Intent.ACTION_SCREEN_ON).apply {
addAction(Intent.ACTION_SCREEN_OFF)
}
registerReceiver(receiver, filter)
registerReceiver() 함수로 등록한 브로드캐스트 리시버는 필요없는 순간이 되면 unregisterReceiver() 함수로 등록을 해제해 줘야 합니다.
unregisterReceiver(receiver)
배터리 상태
배터리 상태는 현재 기기에 전원이 공급되는지, 충전량은 얼마나 되는지 등을 나타냅니다. 이처럼 배터리와 관련된 정보나 상태 변화를 앱에서 감지할 수 있습니다. 안드로이드 시스템에서 배터리 상태가 변경되면 다음 액션 문자열로 인텐트가 발생합니다.
- BATTERY_LOW : 배터리가 낮은 상태로 변경되는 순간
- BATTERY_OKAY : 배터리가 정상 상태로 변경되는 순간
- BATTERY_CHANGED : 충전 상태가 변경되는 순간
- ACTION_POWER_CONNECTED : 전원이 공급되기 시작한 순간
- ACTION_POWER_DISCONNECTED : 전원 공급을 끊은 순간
배터리 관련 인텐트가 발생할 때 실행될 브로드캐스트 리시버는 다음처럼 작성할 수 있습니다.
receiver = object : BroadcastReceiver() {
override fun onReceiver(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_BATTERY_OKAY -> Log.d("kkang", "ACTION_BATTERY_OKEY")
Intent.ACTION_BATTERY_LOW -> Log.d("kkang", "ACTION_BATTERY_LOW")
Intent.ACTION_BATTERY_CHANGED -> Log.d("kkang", "ACTION_BATTERY_CHANGED")
Intent.ACTION_POWER_CONNECTED -> Log.d("kkang", "ACTION_POWER_CONNECTED")
Intent.ACTION_POWER_DISCONNECTED -> Log.d("kkang", "ACTION_POWER_DISCONNECTED")
}
}
}
이렇게 만든 브로드캐스트 리시버와 함께 액션 문자열을 다음처럼 시스템에 등록합니다.
val filter = IntentFilter(Intent.ACTION_BATTERY_LOW).apply {
addAction(Intent.ACTION_BATTERY_OKAY)
addAction(Intent.ACTION_BATTERY_CHANGED)
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
}
registerReceiver(receiver, filter)
이처럼 브로드캐스트 리시버는 시스템에서 상태가 변경되었을 떄 인텐트를 발생시켜줘야만 실행됩니다. 그런데 시스템이 배터리 관련 이벤트를 발생시키지 않아도 현재 배터리 상태가 필요할 때도 있습니다. 즉, 시스템의 상태가 변경되지 않았지만 배터리 정보가 필요한 상황입니다. 이럴 때는 다음처럼 작성할 수 있습니다.
val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = registerReceiver(null, intentFilter)
이 때 첫 번째 매개변수를 null로 지정합니다. 결국 특정 브로드캐스트 리시버를 등록하는 코드가 아니라 배터리와 관련된 정보를 registerReceiver() 함수와 반환값인 인텐트 객체의 엑스트라 정보로 등록하는 코드입니다.
val status = batteryStatus!!.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
// 전원이 공급되고 있다면
val chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
when (chargePlug) {
BatteryManager.BATTERY_PLUGGED_USB -> Log.d("kkang", "usb charge")
BatteryManager.BATTERY_PLUGGED_AC -> Log.d("kkang", "ac charge")
}
} else {
// 전원이 공급되고 있지 않다면
Log.d("kkang", "not charging")
}
현재 전원이 공급되는지는 BatteryManager.EXTRA_STATUS로 얻는 엑스트라값으로 알아낼 수 있습니다. 이 값이 BatteryManager.BATTERY_STATUS_CHARGING 이면 전원이 공급되는 상태입니다. 또한 전원이 공급된다면 BatteryManager.EXTRA_PLUGGED로 엑스트라값을 얻을 수 있으며 이때 엑스트라 값이 BatteryManager.BATTERY_PLUGGED_USB이면 저속 충전 상태를, BatteryManager.BATTERY_PLUGGED_AC이면 고속 충전 상태를 의미합니다.
앞에서는 현재 사용자 기기에 전원이 공급되는지를 알아봤는데 때로는 배터리가 얼마나 충전되었는지 알고 싶은 경우도 있습니다. 이때는 다음처럼 작성합니다.
val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
val batteryPct = level / scale.toFloat() * 100
Log.d("kkang", "batteryPct : $batteryPct")
3. 배터리 정보 앱 만들기
1. 빌드 그레들 파일 수정
android {
...
buildFeatures {
viewBinding = true
}
}
2. 리소스 파일(/res/drawable) 준비하기
3. 브로드캐스트 리시버 만들기
<MyReceiver.kt>
package com.example.myapplication14
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d("kkang","MyReceiver...............")
val manager = context.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
val builder: NotificationCompat.Builder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//26버전 이상
val channelId = "one-channel"
val channelName = "My Channel One"
val channel = NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
//채널에 다양한 정보 설정
description = "My Channel One Description"
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)
}
//채널을 NotificationManager에 등록
manager.createNotificationChannel(channel)
//채널을 이용하여 builder 생성
builder = NotificationCompat.Builder(context, channelId)
} else {
//26버전 이하
builder = NotificationCompat.Builder(context)
}
builder.run {
//알림의 기본 정보
setSmallIcon(android.R.drawable.ic_notification_overlay)
setWhen(System.currentTimeMillis())
setContentTitle("홍길동")
setContentText("안녕하세요")
}
manager.notify(11, builder.build())
}
}
4. 메인 액티비티 작성하기
<MainActivity.kt>
package com.example.myapplication14
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.os.BatteryManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.myapplication14.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//add......................
val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) {
if (it.all { permission -> permission.value == true }) {
val intent = Intent(this, MyReceiver::class.java)
sendBroadcast(intent)
} else {
Toast.makeText(this, "permission denied...", Toast.LENGTH_SHORT).show()
}
}
registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))!!.apply {
when(getIntExtra(BatteryManager.EXTRA_STATUS, -1)){
BatteryManager.BATTERY_STATUS_CHARGING -> {
when(getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)){
BatteryManager.BATTERY_PLUGGED_USB -> {
binding.chargingResultView.text = "USB Plugged"
binding.chargingImageView.setImageBitmap(
BitmapFactory.decodeResource(
resources, R.drawable.usb
))
}
BatteryManager.BATTERY_PLUGGED_AC -> {
binding.chargingResultView.text = "AC Plugged"
binding.chargingImageView.setImageBitmap(
BitmapFactory.decodeResource(
resources, R.drawable.ac
))
}
}
}
else -> {
binding.chargingResultView.text = "Not Plugged"
}
}
val level = getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = getIntExtra(BatteryManager.EXTRA_SCALE, -1)
val batteryPct = level / scale.toFloat() * 100
binding.percentResultView.text = "$batteryPct %"
}
binding.button.setOnClickListener {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
"android.permission.POST_NOTIFICATIONS"
) == PackageManager.PERMISSION_GRANTED
) {
val intent = Intent(this, MyReceiver::class.java)
sendBroadcast(intent)
} else {
permissionLauncher.launch(
arrayOf(
"android.permission.POST_NOTIFICATIONS"
)
)
}
}else {
val intent = Intent(this, MyReceiver::class.java)
sendBroadcast(intent)
}
}
}
}
5. 매니페스트 파일 수정하기
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyFirstApplication">
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="true"></receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
6. 앱 실행 및 테스트
'Android' 카테고리의 다른 글
[깡쌤의 안드로이드 프로그래밍 with 코틀린][5. 컴포넌트 이해하기] 16. 콘텐츠 프로바이더 컴포넌트 (0) | 2024.12.25 |
---|---|
[깡쌤의 안드로이드 프로그래밍 with 코틀린][5. 컴포넌트 이해하기] 15. 서비스 컴포넌트 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][5. 컴포넌트 이해하기] 13. 액티비티 컴포넌트 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][4. 구글의 라이브러리로 화면 구성하기] 12. 머티리얼 라이브러리 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][4. 구글의 라이브러리로 화면 구성하기] 11. 제트팩 라이브러리 (0) | 2024.12.25 |