알림 앱 만들기
Backgraound 작업 종류
- Immeditate tasks (즉시 실행해야 하는 작업)
- Thread
- Handler
- Kotlin coroutines
- Deferred tasks (지연된 작업)
- WorkManager
- Exact tasks (정시 실행 작업)
- AlarmManager (이번 실습에서 채택)
AlarmManager
지정된 시간에 pending event를 발생시킴
시간은 Real time과 Elapsed time 모두 지정 가능
Broadcast Receiver
안드로이드 휴대폰의 상태, 베터리 등 시스템 정보를 받아들이는 기능을 함
시스템 말고 외부 앱과 브로드캐스팅도 가능함
AlarmManager를 통해 정해진 시간에 Pending Event를 발생시키고 Broadcast Receiver를 통해 이를 수신해 알람을 띄운다.
- UI 구성
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundBlack"
tools:context=".MainActivity">
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="50dp"
android:background="@drawable/background_white_ring"
app:layout_constraintBottom_toTopOf="@id/onOffButton"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/timeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="09:03"
android:textColor="@color/white"
android:textSize="50sp"
app:layout_constraintBottom_toTopOf="@id/ampmTextView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/ampmTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="am"
android:textColor="@color/white"
android:textSize="25sp"
app:layout_constraintBottom_toTopOf="@id/onOffButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/timeTextView" />
<Button
android:id="@+id/onOffButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/backgroundBlack"
android:text="@string/onAlarm"
app:layout_constraintBottom_toTopOf="@id/changeTimeButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/changeTimeButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/backgroundBlack"
android:text="@string/changeTimeText"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginBottom="20dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
background_white_ring.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<size
android:height="250dp"
android:width="250dp"/>
<stroke
android:width="1dp"
android:color="@color/white"/>
</shape>
- AlarmDisplayModel 구현
AlarmDisplayModel.kt
data class AlarmDisplayModel(
val hour:Int,
val minute:Int,
var onOff:Boolean
){
val timeText:String
get(){
val h = "%02d".format(if(hour<12) hour else hour-12 )
val m = "%02d".format(minute)
return "$h:$m"
}
val ampmText:String
get(){
return if(hour>12) "pm" else "am"
}
val onOffButtonText:String
get(){
return if(onOff) "알람 끄기" else "알람 켜기"
}
fun makeData() = "$hour:$minute"
}
- 시간 설정 버튼 구현 (SharedPreferences, Calander, TimePickerDialog)
Data Store는 사용하지 않고 SharedPrefreneces를 사용
Data Store와 SharedPrefreneces 차이
initChangeTimeButton() 함수는 시간 설정 버튼을 초기화 해줌(TimePickerDialog를 띄우고 설정된 시간을 saveAlarm() 를 통해 저장)
MainActivity.kt
// MainActivity OnCreate 밑
private fun initChangeTimeButton() {
val changeTimeButton = findViewById<Button>(R.id.changeTimeButton)
val calendar = Calendar.getInstance()
changeTimeButton.setOnClickListener{
TimePickerDialog(this, { picker, hour, minute->
// 기존 알람을 삭제하는 코드, 밑에서 구현
// pending event가 설정되어있다면 삭제!
val pendingIntent = PendingIntent.getBroadcast(this, ALARM_REQUEST_CODE, Intent(this, AlarmReceiver::class.java), PendingIntent.FLAG_NO_CREATE)
pendingIntent?.cancel()
val model = saveAlarm(hour,minute, false)
renderModel(model) // 밑에서 구현
}, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), false).show()
}
}
private fun saveAlarm(hour:Int, minute:Int, onOff:Boolean): AlarmDisplayModel{
val model = AlarmDisplayModel(
hour=hour,
minute=minute,
onOff=onOff
)
// SharedPreferences 사용, Data store은 이번에 사용하지 않음
val sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
// Scope Function with() 실행, sharedPreferences.edit() 의 함수들을 스코프 내에서 실행
with(sharedPreferences.edit()){
putString(ALARM_KEY, model.makeData())
putBoolean(ON_OFF_KEY, model.onOff)
commit()
}
return model
}
companion object{
private const val SHARED_PREFERENCES_NAME = "time"
private const val ALARM_KEY = "alarm"
private const val ON_OFF_KEY = "onOff"
private const val ALARM_REQUEST_CODE = 1000
}
- Alarm Receiver 구현
BroadcastReceiver 를 상속받아 Broadcast를 수신
onReceive() 를 오버라이딩해 수신 동작을 설정
createNotificationChannel() 함수는 sdk 26 이상 사용자를 위해 알람 채널을 생성
notifyNotification() 함수는 알람을 생성해줌
AlarmReceiver.kt
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
class AlarmReceiver:BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
createNotificationChannel(context!!)
notifyNotification(context!!)
}
private fun createNotificationChannel(context: Context) {
// sdk 26이상부터는 채널이 필요함
if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.O){
// 채널 id, 이름, 중요도
val notificationChannel = NotificationChannel(
CHANNEL_ID,
"기상 알림",
NotificationManager.IMPORTANCE_DEFAULT
)
NotificationManagerCompat.from(context).createNotificationChannel( notificationChannel )
}
}
private fun notifyNotification(context: Context){
with(NotificationManagerCompat.from(context)){
val builder = NotificationCompat.Builder(context,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("알람")
.setContentText("일어날 시간입니다.")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// 주의 !!!!!!!!! 채널 아이디가 아니라 Noti ID
notify(NOTIFICATION_ID, builder.build())
}
}
companion object{
private const val CHANNEL_ID = "1000"
private const val NOTIFICATION_ID = 100
}
}
Manifest.xml
<!--리시버 컴포넌트 추가-->
<receiver
android:name=".AlarmReceiver"
android:exported="false"/>
- UI render 및 데이터 가져오기 구현
fetchDataFromSharedPreferences() 함수는 SharedPreferences에 저장되어있는 알람 정보와 Pending Event 정보를 동기화, 불러오기 해줌
renderModel() 함수는 model 정보를 바탕으로 UI를 그려주고 On/Off 버튼에 model 정보를 tag로 삽입
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initOnOffButton()
initChangeTimeButton()
val model = fetchDataFromSharedPreferences()
renderModel(model)
}
...
private fun fetchDataFromSharedPreferences(): AlarmDisplayModel {
val sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
// nullable 반환이라 null safe 필요
val timeValue = sharedPreferences.getString(ALARM_KEY,"09:30") ?: "09:30"
val onOffValue = sharedPreferences.getBoolean(ON_OFF_KEY, false)
val alarmData = timeValue.split(':')
val model = AlarmDisplayModel(alarmData[0].toInt(), alarmData[1].toInt(),onOffValue)
// 예외처리
val pendingIntent = PendingIntent.getBroadcast(this, ALARM_REQUEST_CODE, Intent(this, AlarmReceiver::class.java), PendingIntent.FLAG_NO_CREATE)
if(pendingIntent== null && model.onOff){
// pending event는 없는데 알람이 켜져있는 경우 알람 끄기
model.onOff = false
}
else if( pendingIntent != null && model.onOff.not()){
// pending event는 있는데 알람이 꺼저있는 경우 pending event 끄기
pendingIntent.cancel()
}
return model
}
private fun renderModel(model: AlarmDisplayModel){
findViewById<TextView>(R.id.ampmTextView).apply{
text = model.ampmText
}
findViewById<TextView>(R.id.timeTextView).apply{
text = model.timeText
}
findViewById<Button>(R.id.onOffButton).apply{
text = model.onOffButtonText
// 현재 model을 전역변수로 설정하지 않았다 따라서 버튼에 태그를 달아서 오브젝트를 서정, 불러오기 가능
tag = model
}
}
...
}
- On/Off 버튼 구현
initOnOffButton() 함수로 새로운 버튼의 기능을 설정
onOffButton는 눌릴 때 마다 새로운 model을 생성, 저장한다.
알람이 켜졌을 땐 alarmManager을 통해 알람을 설정, 꺼졌을 땐 알람을 취소한다.
MainActivity.kt
// onCreate 밑
private fun initOnOffButton() {
val onOffButton = findViewById<Button>(R.id.onOffButton)
onOffButton.setOnClickListener {
val model = it.tag as? AlarmDisplayModel ?: return@setOnClickListener
val newModel = saveAlarm(model.hour, model.minute, model.onOff.not())
renderModel(newModel)
if(newModel.onOff){
// 알람이 켜진 경우 -> 알람을 등록
val calendar = Calendar.getInstance().apply{
set(Calendar.HOUR_OF_DAY, newModel.hour)
set(Calendar.MINUTE, newModel.minute)
// 현재 시각보다 이전의 시간이면 다음날로 설정
if(before(Calendar.getInstance())){
add(Calendar.DATE, 1)
}
}
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(this, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(this, ALARM_REQUEST_CODE,intent, PendingIntent.FLAG_UPDATE_CURRENT)
alarmManager.setInexactRepeating(
AlarmManager.RTC_WAKEUP, // 시간 방식
calendar.timeInMillis, // 원하는 시간
AlarmManager.INTERVAL_DAY, // 하루 한번
pendingIntent // pending event 등록
)
}else{
Log.d("tag","cancel")
// 알람이 꺼진 경우 -> 알람 삭제
cancelAlarm()
}
}
}