/AOS-Alarm

๐Ÿ›  AlarmManager์™€ Broadcasting, Notification์„ ํ™œ์šฉํ•œ ์•Œ๋žŒ ์•ฑ

Primary LanguageKotlin

ํ‚ค์›Œ๋“œ

  • AlarmManager
  • Notification
  • getSharedPreference
  • Broadcast Receiver

๊ตฌํ˜„ ๋ชฉ๋ก

  1. TimePicker๋กœ ์‹œ๊ฐ ์„ค์ •ํ•˜๊ธฐ
  2. ์•Œ๋ฆผ ON / OFF

1. ๊ธฐ๋ณธ UI ์„ค์ •ํ•˜๊ธฐ

์ค‘๊ฐ„์— ์•Œ๋ฆผ์œผ๋กœ ์„ค์ •ํ•œ ์‹œ๊ฐ์„ ๋ณด์—ฌ์ฃผ๊ณ  ํ•˜๋‹จ์— ๋‘ ๊ฐœ์˜ ๋ฒ„ํŠผ์„ ๋ฐฐ์น˜ํ•ด์„œ ์•Œ๋ฆผ์„ ์˜จ์˜คํ”„ํ•˜๊ณ  ์‹œ๊ฐ„์„ ์žฌ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜๋ คํ–ˆ๋‹ค.

<?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"
    tools:context=".MainActivity">

    <View
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginHorizontal="50dp"
        android:background="@drawable/background_blackring"
        app:layout_constraintBottom_toTopOf="@id/onOffButton"
        app:layout_constraintDimensionRatio="H, 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:text="09:00"
        android:textSize="50sp"
        app:layout_constraintBottom_toTopOf="@+id/onOffButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="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:textSize="25sp"
        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:layout_marginHorizontal="30sp"
        android:text="@string/on_alarm"
        app:layout_constraintBottom_toTopOf="@+id/changeAlarmButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/changeAlarmButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="30sp"
        android:layout_marginBottom="30sp"
        android:text="@string/time_change"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

ํ…์ŠคํŠธ๋งŒ ๋ณด์—ฌ์ฃผ๊ธฐ์—๋Š” ๋ฐ‹๋ฐ‹ํ•ด์„œ ๋™๊ทธ๋ž€ ํ˜•ํƒœ์˜ ์˜์—ญ ๋‚ด๋ถ€์— ์‹œ๊ฐ์„ ํ‘œ์‹œํ•˜๋„๋ก ํ–ˆ๋‹ค.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="@color/white" />

    <stroke
        android:width="1dp"
        android:color="@color/black" />
    <size
        android:width="250dp"
        android:height="250dp" />
</shape>

drawable ๋กœ shape ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  oval ํƒ€์ž…์„ ์„ค์ •ํ–ˆ๋‹ค. stroke ๋ฅผ ํ†ตํ•ด ์–‡์€ ์„ ์„ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค.

2. ์‹œ๊ฐ ์„ค์ •ํ•˜๊ธฐ

๋ช‡ ์‹œ๊ฐ„ ๋’ค์— ์•Œ๋žŒ์ด ์šธ๋ฆด ๊ฒƒ์ธ์ง€ ์„ธํŒ…ํ•˜๋Š” ๊ฒƒ์€ TimePicker ๋ฅผ ์ด์šฉํ–ˆ๋‹ค.

	private val changeTimeButton: Button by lazy {
        findViewById(R.id.changeAlarmButton)
    }

์šฐ์„  ์‹œ๊ฐ์„ ์„ค์ •ํ•˜๋Š” button ๋ ˆ์ด์•„์›ƒ๊ณผ ์—ฐ๊ฒฐํ•ด์คฌ๋‹ค. ์ดํ›„ TimePicker ๋กค ์„ค์ •ํ•ด์คฌ๋Š”๋ฐ TimePicker์— ๋“ค์–ด๊ฐ€๋Š” ๊ฐ’๋“ค์€ Listener์™€ default๋กœ ์„ค์ •ํ•  hour์™€ minute, Boolean์ด ๋“ค์–ด๊ฐ„๋‹ค.

	private fun initChangeTimeButton() {
        changeTimeButton.setOnClickListener {
            val calendar = Calendar.getInstance() // ํ˜„์žฌ ์‹œ๊ฐ์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์†Œ๋“œ
            TimePickerDialog(this, { picker, hour, minute ->
                val model = savedAlarmModel(hour, minute, false)
                renderView(model)
            }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MILLISECOND), false).show()
        }
	    }

์œ„์—์„œ ์—ฐ๊ฒฐํ•ด์ค€ changeTimeButton ์ด ํ˜ธ์ถœ๋˜๋ฉด ํ˜„์žฌ ์‹œ๊ฐ์„ ๊ฐ€์ ธ์˜จ๋‹ค. Calendar ํด๋ž˜์Šค์˜ getInstance ๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜จ๋‹ค. ์ดํ›„ TimePickerDialog ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด์„œ ํ˜ธ์ถœํ•˜๋Š”๋ฐ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ hour์™€ minute์„ ์„ค์ •ํ•œ ๋‹ค์Œ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ๋ฐ›๋Š”๋‹ค. ๋žŒ๋‹ค์‹์œผ๋กœ ์„ค์ •ํ•œ hour์™€ minute์„ ์ €์žฅํ•˜๊ณ  ์ด๋ฅผ ๋‹ค์‹œ ๋ Œ๋”๋งํ•ด์ฃผ๋Š” ํ•จ์ˆ˜์— ๋„ฃ์–ด์คฌ๋‹ค. ์„ธ ๋ฒˆ์งธ์™€ ๋„ค ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” TimePicker๋ฅผ ์ผฐ์„ ๋•Œ Default๋กœ ๋ณด์—ฌ์ค„ hour์™€ minute๋ฅผ ๋„ฃ์–ด์ค€๋‹ค. ํ˜„์žฌ ์‹œ๊ฐ์„ ๋„ฃ์–ด์คฌ๋‹ค.

savedAlarmModel

๋‹ค์Œ์œผ๋กœ ์•Œ์•„๋ณผ ๊ฒƒ์„ TimePicker์—์„œ ์„ค์ •ํ•œ ์‹œ๊ฐ์„ ์‹ค์ œ ์‹œ๊ฐ์œผ๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” ๋ฉ”์†Œ๋“œ์ธ savedAlarmModel ๋ฉ”์†Œ๋“œ์ด๋‹ค. ์šฐ์„  ๊ทธ ์ „์— ์•Œ๋žŒ ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค AlarmDisplayModel ํด๋ž˜์Šค๋ฅผ ์‚ดํŽด๋ณด์ž.

package com.example.alarm

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) "AM" else "PM"
        }

    val onOffText: String
        get() {
            return if (onOff) "์•Œ๋žŒ ๋„๊ธฐ" else "์•Œ๋žŒ ์ผœ๊ธฐ"
        }

    fun makeDataForDB(): String {
        return "$hour:$minute"
    }
}

getter ๋ฅผ ํ†ตํ•ด์„œ ํŠน์ •ํ•œ ํ˜•ํƒœ๋ฅผ ๊ฐ–๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค. ๋‹ค์‹œ ๋ฉ”์†Œ๋“œ๋กœ ๋„˜์–ด๊ฐ€๋ณด์ž.

	private fun savedAlarmModel(hour: Int, minute: Int, onOff: Boolean): AlarmDisplayModel {
        val model = AlarmDisplayModel(
            hour = hour,
            minute = minute,
            onOff = false
        )

        val sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
        with(sharedPreferences.edit()) {
            putString(ALARM_KEY, model.makeDataForDB())
            putBoolean(ONOFF_KEY, model.onOff)
            commit()
        }

        return model
    }

Data ํด๋ž˜์Šค์— ๋„˜๊ฒจ์ฃผ์–ด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ดํ›„ SharedPreferences ์— ์ €์žฅํ•œ๋‹ค. SharedPreferences ๋Š” ํ•˜๋“œ๋””์Šคํฌ?์™€ ๋น„์Šทํ•˜๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ๊ณต๊ฐ„ ์ •๋„๋กœ ์ดํ•ดํ•˜๋ฉด ๋œ๋‹ค.

![]

์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹๋ฌธ์„œ - SharedPreferences

์œ„์˜ ์‚ฌ์ง„์€ ๊ณต์‹๋ฌธ์„œ์—์„œ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๊ณ  key - value ๋ฅผ ์ด์šฉํ•ด์„œ ๊ฐ’์„ ์ €์žฅํ•˜๊ณ  ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋‹ค. ์—ฌ๊ธฐ์— ์ €์žฅํ•  ๊ฐ’์€ ์•Œ๋žŒ์œผ๋กœ ์„ค์ •ํ•œ ์‹œ๊ฐ ๊ณผ On - Off ์— ๋Œ€ํ•œ ์ •๋ณด์ด๋‹ค. ์šฐ์„  private ๋กœ ๊ณต๊ฐ„์„ ์ƒ์„ฑํ•˜๊ณ  ์ดํ›„์— with ๊ณผ edit ์„ ์ด์šฉํ•ด์„œ ๊ฐ’์„ ์ €์žฅํ•œ๋‹ค. ๊ฐ์ฒด ์ž์ฒด์—์„œ ๋ฐ”๋กœ edit์„ ํ•ด์„œ ์„ค์ •ํ•˜๋Š” ๊ฒฝ์šฐ commit() ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ํ•„์š”๊ฐ€ ์—†์ง€๋งŒ with ์„ ํ†ตํ•ด์„œ ๊ฐ’์„ ์ €์žฅํ•˜๋Š” ๊ฒฝ์šฐ commit() ์„ ๋ฌด์กฐ๊ฑด ํ•ด์ค˜์•ผํ•œ๋‹ค. ๊ทธ๋ž˜์•ผ ๋ณ€๊ฒฝ๋œ ๊ฐ’์ด ์ €์žฅ๋œ๋‹ค.

	companion object {
        private const val ALARM_KEY = "alarm"
        private const val ONOFF_KEY = "onOff"
        private const val SHARED_PREFERENCES_NAME = "time"
        private const val ALARM_REQUEST_CODE = 1000
    }

ํ‚ค์˜ ๊ฒฝ์šฐ ์ •์  ๋ณ€์ˆ˜๋กœ ์„ ์–ธํ•ด์คฌ๋Š”๋ฐ ๊ทธ ์ด์œ ๋Š” ๋ณ€๊ฒฝ๋˜๋ฉด ์•ˆ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

3. ์‹œ๊ฐ ๊ฐ€์ ธ์˜ค๊ธฐ

์œ„์—์„œ ์‹œ๊ฐ์„ ์ €์žฅํ•˜๋Š” ๋ฉ”์†Œ๋“œ๊นŒ์ง„ ์™„๋ฃŒํ–ˆ๋‹ค. ์ด์ œ ์•ฑ์„ ์‹คํ–‰ํ–ˆ์„ ๋•Œ ์ €์žฅ๋œ ๊ณต๊ฐ„์—์„œ ์„ค์ •ํ•œ ์‹œ๊ฐ์„ ๊ฐ€์ ธ์˜ค๋ฉด ๋œ๋‹ค.

	private fun fetchDataFromSharedPreferences(): AlarmDisplayModel {
        val sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
        val timeDBValue = sharedPreferences.getString(ALARM_KEY, "12:00") ?: "12:00"
        val onOffDBValue = sharedPreferences.getBoolean(ONOFF_KEY, false)
        val alarmData = timeDBValue.split(":")
        val alarmModel = AlarmDisplayModel(
            hour = alarmData[0].toInt(),
            minute = alarmData[1].toInt(),
            onOff = onOffDBValue
	        )
			return alarmModel
}

์šฐ์„  sharedPreferences ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  get ์„ ํ†ตํ•ด value ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ์ด ๋•Œ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ default ๊ฐ’์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•ด๋‹น key์— ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ ์ด default ๊ฐ’์„ ๋ฆฌํ„ดํ•˜๊ฒŒ ๋œ๋‹ค. ์ €์žฅ๋œ ๊ฐ’์„ ์ด์šฉํ•ด์„œ AlarmDisplayModel ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ ๋‹ค. ์ตœ์ข…์ ์œผ๋กœ ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•ด์ฃผ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ ์ตœ์ข…์ ์œผ๋กœ ์ด๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์นœ๋‹ค.

4. ์‹œ๊ฐ ๋ณด์—ฌ์ฃผ๊ธฐ

	val model = fetchDataFromSharedPreferences()
  renderView(model)		

// ... ์ค‘๋žต

	private fun renderView(model: AlarmDisplayModel) {
        ampmTextView.apply {
            text = model.ampmText
        }

        timeTextView.apply {
            text = model.timeText
        }

        onOffButton.apply {
            text = model.onOffText
            tag = model
        }
	    }

renderView ๋ฉ”์†Œ๋“œ์—์„  ์‹ค์ œ๋กœ ๋ ˆ์ด์•„์›ƒ๊ณผ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

5. ์•Œ๋žŒ On / Off ์„ค์ •ํ•˜๊ธฐ

์ง€๊ธˆ๊นŒ์ง€ ์•Œ๋žŒ์„ ํ•ด์ค„ ์‹œ๊ฐ์„ ์„ค์ •ํ–ˆ๋‹ค. ์ด์ œ ํ•ด๋‹น ์‹œ๊ฐ์— ์•Œ๋ฆผ์ด ์šธ๋ฆฌ๊ฒŒ๋” ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

	private fun initOnOffButton() {
        val onOffButton = findViewById<Button>(R.id.onOffButton)
        onOffButton.setOnClickListener {

            val model = it.tag as? AlarmDisplayModel ?: return@setOnClickListener
            val newModel = saveAlarmModel(model.hour, model.minute, model.onOff.not())
            renderView(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.setExact(
                    AlarmManager.RTC_WAKEUP,
                    calendar.timeInMillis,
                    pendingIntent
                )

            } else {
                cancelAlarm()
            }

        }
    }

์•Œ๋žŒ์— ๋Œ€ํ•œ ํด๋ž˜์Šค๋Š” BroadcastReceiver ๋ฅผ ์ƒ์†๋ฐ›์•„์„œ ๊ตฌํ˜„ํ–ˆ๋‹ค. BroadcastReceiver๋Š” ๋ง ๊ทธ๋Œ€๋กœ ํŠน์ • ํ–‰์œ„๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ž๋™ํ•˜๋Š” ํด๋ž˜์Šค์ด๋‹ค.

package com.example.alarm

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() {

    companion object {
        const val NOTIFICATION_ID = 100
        const val NOTIFICATION_CHANNEL_ID = "1000"
    }

    override fun onReceive(context: Context, intent: Intent) {
        createNotificationChannel(context)
        notifyNotification(context)
    }

    private fun createNotificationChannel(context: Context) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                "๊ธฐ์ƒ ์•Œ๋žŒ",
                NotificationManager.IMPORTANCE_HIGH
            )

            NotificationManagerCompat.from(context).createNotificationChannel(notificationChannel)
        }
    }

    private fun notifyNotification(context: Context) {
        with(NotificationManagerCompat.from(context)) {
            val build = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
                .setContentTitle("์•Œ๋žŒ")
                .setContentText("์ผ์–ด๋‚  ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค.")
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setPriority(NotificationCompat.PRIORITY_HIGH)

            notify(NOTIFICATION_ID, build.build())

        }

    }

}

์•Œ๋žŒ์ด ์ž‘๋™ํ•ด์•ผํ• ๋•Œ ์ด ํด๋ž˜์Šค๋ฅผ ์ž‘๋™์‹œํ‚ค๋ ค๊ณ  ํ•œ๋‹ค.

Nofication ๊ณผ Broadcasting ์— ๊ด€ํ•œ ๊ณต์‹๋ฌธ์„œ๋ฅผ ๊ฐ€์ ธ์™”๋‹ค.

์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹ ๋ฌธ์„œ - Notification

์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹ ๋ฌธ์„œ - Broadcasting

ํ•ด๋‹น ์‹œ๊ฐ์— ์•Œ๋ฆผ์ด ์šธ๋ฆฌ๋Š” ๊ณณ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.