예를들어,
3시간 뒤에 알람이 울리도록 한다고 하면,
알람이 울리려면 앱이 계속 실행되어 있으면서
3시간이 되었는지 체크해야 한다.
하지만 일반적인 방법으로 했을 경우,
3시간 동안 앱이 살아있을 것이라는 보장이 없다.
따라서 Background작업을 통해 시간을 확인하는 기능을 넣는 것으로
앱이 살아있는지 여부와 상관없이 알람을 세팅해주는 것이다.
[ 메인 화면 ]
[ 시간 재설정 버튼 클릭 ]
--> as 를 통해 형변환을 할 수 있다.
as 연산자는 해당 값을 as로 지정한 타입으로 형변환함
예시 코드 )
val ss : Float = 0.2F
val a = ss as Double
// ss의 타입을 Double로 형변환
// 만약 형변환이 불가능할 경우 오류 발생
하지만 만약 해당 타입으로 바꿀 수 없으면 ClassCastException이 발생시킴
따라서 as 로 형변환 시키기 전에
is 연산자로 형변환이 가능한지 체크하는 과정이 필요했었음ㅡㅡ,
kotlin에서는 이런 과정을 생략할 수 있는 as? 연산자를 제공하고 있음
예시
val ss : Float = 0.2F
val a = ss as? Double ?: 66
// ss를 Double로 형변환,
// 형변환 불가능이라면 null반환
// 엘비스 연산자에 의해 as?가 null반환이면 66이 반환
// 결과적으로 형변환이 불가능하면 a = 66이 됨
흔히 코딩을 하다보면 context 객체를 보게되고, 대부분은 this를 통해 context를 가져오게 될 것이다.
Context는
실행하고 있는 앱의 상태, 맥락과 같은 의미를 담고 있다.
안드로이드 앱이 해당 환경에서
글로벌 정보나
안드로이드 API나
시스템이 관리하고 있는 정보( Resource 파일에 대한 접근, SharedPreference 등등 )과
같은 기능들을 저장해놓은,
그리고 거기에 접근을 할 때 필요한 객체이다.
액티비티는 Context를 상속받고 있음
따라서 액티비티에서는 실행하고 있는 환경 자체가
SharedPreference나 Resource와 같은 영역에 대한 접근이 용이한 상태
그래서 결과적으로 액티비티 자체를 Context와 동일시하는 것이 가능하므로,
액티비티에서 this를 통해 Context를 대신할 수 있는 것
위에 설명했듯이 액티비티는 Context와 동일시 가능하지만,
다른 환경에서는 그것이 불가능하다.
예를 들어,
이번 앱에서 사용하는 BroadcastReceiver()를 구현하는 class의 경우
( 아래에 AlarmReceiver.kt로 생성된 파일 ),
BroadcastReceiver()는 액티비티 환경이 아니기 때문에 Context를 따로 받아와야한다. ( 파라미터로 받아오든, 메소드로 받아오든 )
( 당연하게도 해당 환경에서 this를 통한 접근도 불가능 )
결과적으로
데이터에 접근하는 영역에 있어서는 대부분의 경우 Context가 필수적이며,
하지만 이미 만들어져 있어서 오버라이드하여 사용하는 함수들의 절대적인 대다수( 전부 그럴지는 모르겠음 )는
Context가 필요할 경우, 파라미터로 받아오는 부분이 이미 되어있기 때문에
새로운 모듈을 만드는 것이 아닌 이상 굳이 이 부분에 대해 신경쓸 필요는 없을 것이다.
단, 액티비티가 아닌 영역( 즉, Context를 상속하지 않는 영역 )에서는 this를 통해 Context에 접근할 수 없다는 것만 알고 있으면 된다.
View를 상속하는 컴포넌트들에는 모두 공통적으로 tag라는 속성이 존재한다.
이 속성은 View에서 다른 역할을 하는 속성이 아닌,
일종의 임시 데이터 저장고의 역할을 하는 속성이다.
tag속성은 Object 타입이기 때문에 모든 타입이 들어갈 수 있다 !!!!!!
예를들어
......
private fun renderView(model: AlarmDisplayModel) {
val model = 20
findViewById<Button>(R.id.onOffButton).apply {
tag = model
}
}
......
val onOffButton = findViewById<Button>(R.id.onOffButton)
onOffButton.setOnClickListener {
val model = it.tag
}
위의 코드를 보면 renderView 메소드 내부에 정의된 model변수는 지역변수이므로,
외부에서 정의된 메소드인 onOffButton.setOnClickListener{} 에서는 해당 변수에 접근해서 값을 가져올 수 없음
위의 코드에서는 tag속성을 사용하여,
onOffButton의 tag속성에 renderView메소드의 지역변수인 model의 값을 저장하였다가
onOffButton.setOnClickListener{} 메소드에서 tag속성에서 꺼내어 사용하였다.
--> 앱을 사용하는 도중에 Background에서 처리해야하는 작업
Thread
Handler
Kotlin coroutines
--> Kotlin에서 지원하는 비동기 프로그램
--> 앱이 종료되었거나 다시 실행되었더라도 예약된 작업을 실행시키는 작업
--> push수신과 같은 영역에서 이용이 됨
--> 반복된 시간이나 지정된 간격, 지정된 시간에 PendingIntent를 통해 지정된 작업이 실행되도록 함
Intent는 액티비티 간에 데이터를 옮기거나, 액티비티간에 연결고리의 역할을 한다.
여기서 Pending Intent는 이런 Intent를 즉시 실행시키는 것이 아니라 잠시 Pending( 보류 ) 해놓았다가 실행시키는 기능을 Pending Intent라고 한다.
혹은 9장에서 나온 것처럼 Intent를 실행시킬 권한을 제 3자인 Pending Intent에게 넘겨줘서
해당 Pending Intent가 Intent를 실행해야겠다고 판단이 들면 실행시킬 수 있도록 만드는 기능이라고 이해할 수도 있다.
기기가 켜져 있는지,
배터리가 얼마나 남았는지,
Wifi를 켰는지 안켰는지 등
시스템에서 일어나는 작업들을 catch해서 작업을 진행해야되는 경우가 있을 수 있다.
예를 들어,
Wifi가 꺼졌을 떄는 다른 알림을 줘야한다든지,
Wifi가 켜졌을 때 사용자의 동작을 catch해서 다른 작업을 해야된든지 하는 앱들이 있을 수 있다.
그런데 이렇게 catch하는 부분들에 대해 실시간으로 Wifi가 켜졌는지 꺼졌는지 확인하는 것은
작업이 많이 필요하고 배터리 소모가 그만큼 심해질 수밖에 없다.
또한 이런 앱들이 많아진다면 그만큼 기기의 부담이 커질 것이다.
따라서 안드로이드에서는 이런 부분들을 앱에서 처리하는 것이 아니라
시스템상에서 해당 작업( 예를들어, Wifi를 켜고 끄는 등등 시스템적인 작업들 )이 있을 때마다
Broadcast를 통해서 해당 작업의 완료를 전파한다.
이제 앱에서는 이렇게 Broadcast로 전파한 내용에 대해
이러이러한 전파내용은 받겠다라고 설정한 Broadcast receiver만 등록하면,
시스템의 동작에 대한 catch가 가능해지는 것이다.
--> 예를 들어, Wifi가 켜졌는지에 대한 Broadcast를 수신하겠다고 Broadcast receiver를 등록하면,
해당 앱에서는 Wifi가 켜졌을 때, Broadcast receiver를 통해 catch하여 대응할 수 있는 것이다.
AlarmManager는 안드로이드에서 제공하는 SystemService의 일환으로
AlarmManager에 설정한 시간이 되면, 설정한 Broadcast receiver에 Broadcasting을 해준다.
private val editHour:EditText by lazy {
findViewById<EditText>(R.id.editHour)
}
private val editMinute:EditText by lazy {
findViewById<EditText>(R.id.editMinute)
}
......
// TODO AlarmReceiver를 생성하는 부분
val pendingIntent = PendingIntent.getBroadcast(
this, ALARM_REQUEST_CODE,
Intent(this, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT
)
//TODO 이 부분 이후부터가 알람 Broadcast를 구성하는 부분
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, editHour.text.toInt())
set(Calendar.MINUTE, editMinute.text.toInt())
// 캘린터에 세팅한 시간이 지금시간보다 이전이라면, 내일 해당 시간이라는 의미이므로 Day를 +1 해줌
if (before(Calendar.getInstance())) {
add(Calendar.DATE, 1)
}
}
// 시스템의 알람서비스를 가져와서 AlarmManager로 형변환
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
// 아래에 설명있음 -> 기기의 수면모드를 회피하기 위한 메소드
// alarmManager.setAndAllowWhileIdle()
// alarmManager.setExactAndAllowWhileIdle()
alarmManager.setInexactRepeating(
// 시간을 어떻게 셀 것인지 정함 ㅡ,
// RTC_WAKEUP -> 실제 시간 기준으로 wakeup ( 절대시간 )
// ELAPSED_REALTIME_WAKEUP -> 스마트폰이 부팅된 이후부터 시간을 기준으로 wake upㅡ, 좀더 권장됨,, 이유는 확인해봐야할듯
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_DAY,
pendingIntent
)
......
companion object {
private const val ALARM_REQUEST_CODE = 1000
}
이 부분에서 Pending Intent의 getBroadcast()메소드를 사용하여
Broadcast Receiver를 실행시키고 있음
// TODO AlarmReceiver를 생성하는 부분
val pendingIntent = PendingIntent.getBroadcast(
this, ALARM_REQUEST_CODE,
Intent(this, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT
)
......
companion object {
private const val ALARM_REQUEST_CODE = 1000
}
AlarmReceiver.kt파일에 코딩한 BroadcastReceiver를 실행시키고 있음
PendingIntent.getBroadcast()는
첫번째 파라미터로 Context를 받고,
두번째 파라미터로 Intent 요청에 대한 식별코드를 받고 (임의로 설정),
세번째 파라미터로 실행시킬 Intent를 받고 ( Intent에 BroadcastReceiver가 정의된 파일 설정 )
네번째 파라미터로 Flag를 받는다.
여기서 네번째 파라미터인 Flag는
이미 해당 BroadcastReceiver가 실행되어 존재할 경우
어떻게 할 것인가에 대한 부분이다.
--> BroadCastReceiver에 대한 자세한 설명은 더 아래에 있음
이 부분에서 Calendar 클래스를 사용하여
AlarmManager에 Setting하기 위한 시간을 구성하고 있음
private val editHour:EditText by lazy {
findViewById<EditText>(R.id.editHour)
}
private val editMinute:EditText by lazy {
findViewById<EditText>(R.id.editMinute)
}
......
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, editHour.text.toInt())
set(Calendar.MINUTE, editMinute.text.toInt())
// 캘린터에 세팅한 시간이 지금시간보다 이전이라면, 내일 해당 시간이라는 의미이므로 Day를 +1 해줌
if (before(Calendar.getInstance())) {
add(Calendar.DATE, 1)
}
}
여기서는 구현하지 않았지만, 기기의 수면모드를 회피하는 방법
// alarmManager.setAndAllowWhileIdle()
// alarmManager.setExactAndAllowWhileIdle()
6버전 이상부터는 사용자가 기기를 장시간 사용하지 않을 경우( 잔다던가 하는 이유로 ),
수면모드에 돌입함
수면모드에 돌입하면, 배터리 소모를 줄이기 위해 Background 작업이 모두 중단됨
--> 즉, 알람도 멈춤
이를 방지하고ㅡ, 반드시 알람이 울리도록 하기 위해서는
위의 2개의 메소드 중 하나를 사용해야 함
또는 정확한 시간에 Push알림을 보내는 것으로 기기를 수면모드에서 깨워서, push알림을 받은 순간부터 알람을 Background에 재설정하는 식으로 구현하는 방법도 있음
이 부분에서 AlarmManager를 이용하여 Alarm Service를 Broadcast로 등록한다.
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setInexactRepeating(
// 시간을 어떻게 셀 것인지 정함 ㅡ,
// RTC_WAKEUP -> 실제 시간 기준으로 wakeup ( 절대시간 )
// ELAPSED_REALTIME_WAKEUP -> 스마트폰이 부팅된 이후부터 시간을 기준으로 wake upㅡ, 좀더 권장됨,, 이유는 확인해봐야할듯
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_DAY,
pendingIntent
)
SystemService에서 알람서비스를 가져와서 AlarmManager로 형변환시켜주고 있음
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
AlarmManager를 통해 Broadcast에 알람을 등록하고 있음
alarmManager.setInexactRepeating(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_DAY,
pendingIntent
)
AlarmManager는 Broadcast에 알람을 등록하기 위한 여러 메소드를 가지고 있음
왜 1번 같은 정확한 시간이 아닌 3번의 비 정확한 시간을 사용하는 것일까?
그 이유는 정확한 시간을 사용하기 위해서는 기기가 더 빈번하게 체크를 수행해야하기 때문
--> 고로 기기에 더 많은 부담이 생김
따라서 아주 정확해야하는 상황이 아니라면 비정확한 알람을 사용하는 것을 추천한다.
이번에 사용한 setInexactRepeating()메소드는
첫번째 파라미터로 시간을 세는 기준??,
두번째 파라미터로 Calendar를 통해 설정한 시간,
세번째 파라미터로 알람의 반복 주기,
네번째 파라미터로 PendingIntent
( 해당 알람으로 Broadcasting할 때ㅡ, 전달받기 위한 BroadcastReceiver를 담은 PendingIntent )를 받는다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.aop_part3_chapter11">
<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.Aop_part3_chapter11">
<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>
<receiver android:name=".AlarmReceiver"
android:exported="false"/>
</application>
</manifest>
해당 부분에서 BroadcastReceiver 등록
<receiver android:name=".AlarmReceiver"
android:exported="false"/>
package com.example.aop_part3_chapter11
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(){
// PendingIntent를 통해 BroadcastReceiver가 등록된 이후에,
// AlarmManager에 의한 Broadcast가 발생하면 호출되는 메소드
override fun onReceive(context: Context, intent: Intent) {
creatNotificationChannel(context)
notifyNotification(context)
}
private fun creatNotificationChannel(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())
}
}
companion object {
private const val NOTIFICATION_CHANNEL_ID = "1000"
private const val NOTIFICATION_ID = 100
}
}
이 부분을 보면 해당 클래스는 BroadcastReceiver()를 상속받음
class AlarmReceiver : BroadcastReceiver(){
--> BroadcastReceiver를 구현하는 클래스
이 부분을 보면 해당 클래스는 Context를 this 키워드로 불러오지 않음
--> BroadcastReceiver()는 액티비티가 아니며,
따라서 Context의 상속을 받지 않으므로 액티비티에서 사용했던 것과는 다르게
Context를 따로 구해서 사용해야 한다.
( 여기서는 PendingIntent가 해당 Receiver를 실행할때 Context를 전달해줌 )
creatNotificationChannel(context)
notifyNotification(context)
BroadcastReceiver가 재정의해야하는 onReceive() 메소드
--> PendingIntent를 통해 BroadcastReceiver가 등록된 이후에,
AlarmManager에 의한 Broadcast가 발생하면 호출되는 메소드
override fun onReceive(context: Context, intent: Intent) {
creatNotificationChannel(context)
notifyNotification(context)
}
--> PendingIntent를 이용한 BroadcastReceiver 등록에 대한 부분은 위쪽에 정리해놓았다.
val pendingIntent = PendingIntent.getBroadcast(
this,
ALARM_REQUEST_CODE,
Intent(this, AlarmReceiver::class.java),
PendingIntent.FLAG_NO_CREATE
)
pendingIntent?.cancel()
위의 코드와 같이 PendingIntent로 해당 식별코드( 위 코드의 ALARM_REQUEST_CODE )의 BroadcastReceiver를 불러온 다음
cancel() 메소드를 사용하여 해제하면된다.
TimaPicker라고 하는 시간을 설정하는 기능을 Dialog에 넣어준 것
예시
// 현재 시간을 가져옴
val calendar = Calendar.getInstance()
TimePickerDialog(
this,
{ picker, hour, minute ->
// TimePickDialog를 통해 사용자가 시간을 설정하면 호출되는 리스너
// 첫번째 파라미터로 TimePickDialog에 TimePick을 하게 해주는 시계인 TimePicker가 들어오고, 두번째 파라미터로 사용자가 선택한 시간이, 세번쨰 파라미터로 사용자가 선택한 분이 들어온다.
val model = saveAlarmModel(hour, minute, false)
// 데이터를 저장한다.
renderView(model)
// 뷰를 업데이트 한다.
// 기존에 있던 알람을 삭제한다.
cancelAlarm()
},
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE),
false
).show()
첫번째 파라미터로 Context
두번째 파라미터로 TimaPickerDialog에 대한 리스너
-> 위에 예시에서는 람다함수로 구성
세번째 파라미터로 TimePicker에 처음에 나타날 시간
네번째 파라미터로 TimePicker에 처음에 나타날 분
다섯번째 파라미터로 기준을 24시로 할지 12시로 할지 여부( true면 24시간 )롤 받는다.
이번 앱에서의 data class 예시이다.
package com.example.aop_part3_chapter11
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"
}
}
데이터 클래스에서 데이터를 가져갈 때,
데이터를 다루기 쉽게 데이터를 가공하여 내보내는 변수들을 정의
예를 들어 hour가 9일 경우 "09"로 텍스트에 나타낼 것이므로,
9를 "09"로 바꾸어 반환시켜주는 변수를 정의
--> 위 코드에서 timeText변수의 getter를 변환시켜서 구현
혹은 데이터의 값에 따라 달리지는 내용을 여기서 다뤄서 사용하기 편하게 만듬
예를 들어 hour의 값에 따라 am,pm이 바뀌는 것
--> 위 코드에서 ampmText변수의 getter를 변환시켜서 구현
결국 데이터 클래스를 정의할 때,
데이터 클래스의 데이터들을 다루는 부분
( 가공하여 반환하거나, 데이터를 분석하여 결과값을 반환하거나 등등 )들을
여기서 정의해주면 사용할 때 편하다.
package com.example.aop_part3_chapter11
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.TimePickerDialog
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.TimePicker
import org.w3c.dom.Text
import java.util.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//todo step0 뷰를 초기화 해주기
initOnOffButton()
initChamgeAlarmTimeButton()
//todo step1 데이터 가져오기
val model = fetchDataFromSharedPreferences()
//todo step2 뷰에 데이터 그려주기
renderView(model)
}
private fun initOnOffButton() {
val onOffButton = findViewById<Button>(R.id.onOffButton)
onOffButton.setOnClickListener {
// renderView메소드에서 OnOffButton의 tag에 저장해놓았던 AlarmDisplayModel을 가져와서 as를 통해 형변환
// ?를 통해 NullSafe 해주고, Null일 경우 대비해 앨비스 연산자로 리턴시킴
val model = it.tag as? AlarmDisplayModel ?: return@setOnClickListener
val newModel = saveAlarmModel(model.hour, model.minute, model.onOff.not())
renderView(newModel)
// 데이터를 확인을 한다.
//온인지 오프인지에 따라 작업을 다르게 처리한다.
if (newModel.onOff) {
// 켜진경우 알람을 등록
// TODO AlarmReceiver를 생성하는 부분
val pendingIntent = PendingIntent.getBroadcast(
this, ALARM_REQUEST_CODE,
Intent(this, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT
)
//TODO 이 부분 이후부터가 알람 Broadcast를 구성하는 부분
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, newModel.hour)
set(Calendar.MINUTE, newModel.minute)
// 캘린터에 세팅한 시간이 지금시간보다 이전이라면, 내일 해당 시간이라는 의미이므로 Day를 +1 해줌
if (before(Calendar.getInstance())) {
add(Calendar.DATE, 1)
}
}
// 시스템의 알람서비스를 가져와서 AlarmManager로 형변환
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
// alarmManager로 실행시키는 메소드는 여러개 있는데 이번에는 정확한 시간에 그리고 반복해서 나타나는 메소드로 실행
// 첫번쨰 파라미터로 알람이 울리는 시간에 대한 기준을 받고, 두번째 파라미터로 언제 알람이 발동할 것인지에 대한 값을 받고( 위에서 Calendar객체에 세팅한 시간 ), 세번째 파라미터로 반복 주기를 받고, 네번째 파라미터로 Broadcast를 보내줄 PendingIntent를 받는다.
// 이렇게 되면 하루에 한번씩 PendingIntent가 실행되고, 아래의 캘린더에 등록한 시간대로 실행된다.
alarmManager.setInexactRepeating(
// 시간을 어떻게 셀 것인지 정함 ㅡ,
// RTC_WAKEUP -> 실제 시간 기준으로 wakeup ( 절대시간 )
// ELAPSED_REALTIME_WAKEUP -> 스마트폰이 부팅된 이후부터 시간을 기준으로 wake upㅡ, 좀더 권장됨,, 이유는 확인해봐야할듯
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_DAY,
pendingIntent
)
} else {
// 꺼진 경우 알람을 등록
cancelAlarm()
}
}
}
private fun initChamgeAlarmTimeButton() {
val changeAlarmButton = findViewById<Button>(R.id.changeAlarmTimeButton)
changeAlarmButton.setOnClickListener {
// 현재 시간을 가져옴
val calendar = Calendar.getInstance()
// TimaPickDialogㅡ, TimePicker를 Dialog로 띄워줘서 사용자가 시간을 설정할 수 있게 만듬
// 첫번째 파라미터로 현재 위치, 두번째 파라미터로 TimaPickerDialog에 대한 리스너, 세번째 파라미터로 TimePicker에 처음에 나타날 시간, 네번째 파라미터로 TimePicker에 처음에 나타날 분, 다섯번째 파라미터로 기준을 24시로 할지 12시로 할지 여부( true면 24시간 )롤 받는다.
TimePickerDialog(
this,
{ picker, hour, minute ->
// TimePickDialog를 통해 사용자가 시간을 설정하면 호출되는 리스너
// 첫번째 파라미터로 TimePickDialog에 TimePick을 하게 해주는 시계인 TimePicker가 들어오고, 두번째 파라미터로 사용자가 선택한 시간이, 세번쨰 파라미터로 사용자가 선택한 분이 들어온다.
val model = saveAlarmModel(hour, minute, false)
// 데이터를 저장한다.
renderView(model)
// 뷰를 업데이트 한다.
// 기존에 있던 알람을 삭제한다.
cancelAlarm()
},
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE),
false
).show()
}
}
private fun saveAlarmModel(
hour: Int,
minute: Int,
onOff: Boolean
): AlarmDisplayModel {
val model = AlarmDisplayModel(
hour = hour,
minute = minute,
onOff = onOff
)
val sharePreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
// 이 경우 with함수를 사용하여 sharePreferences의 edit()을 해줬기 때문에 apply가 자동으로 되지 않는다. 따라서 commit()을 명시해줘야한다.
with(sharePreferences.edit()) {
putString(ALARM_KEY, model.makeDataForDB())
putBoolean(ONOFF_KEY, model.onOff)
commit()
}
return model
}
private fun fetchDataFromSharedPreferences(): AlarmDisplayModel {
val sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
// getString으로 받아오면 nullable하게 되므로, 엘비스 연산자를 이용해서 nullsafe로 만듬
val timeDBValue = sharedPreferences.getString(ALARM_KEY, "9:30") ?: "9:30"
val onOFfDBValue = sharedPreferences.getBoolean(ONOFF_KEY, false)
val alarmData = timeDBValue.split(":")
val alarmModel = AlarmDisplayModel(
hour = alarmData[0].toInt(),
minute = alarmData[1].toInt(),
onOff = onOFfDBValue
)
// 보정 보정 예외처리 --> 실제 알람이 백그라운드에 있는지 Broadcast를 통해 확인하여 레이아웃의 OnOff와 비교해야함
//PendingIntent를 통해 Broadcast를 가져옴ㅡ, 현재 알람이 벡그라운드에 세팅되어 있는지 확인
// Flagㅡ, 여러가지 종류가 있음, 우리가 이번에 이용할 것은 2종류 1. 없으면 없는대로 놔두는 것( PendingIntent.FLAG_NO_CREATE ), 2. 없으면 만들고 있으면 업데이트 하는 것 ( PendingIntent.FLAG_UPDATE_CURRENT )
// 아래의 코드에선 PendingIntent.FLAG_NO_CREATE로 Flag를 설정했으므로, 해당하는 데이터가 없다면 null이 반환됨
val pendingIntent = PendingIntent.getBroadcast(
this,
ALARM_REQUEST_CODE,
Intent(this, AlarmReceiver::class.java),
PendingIntent.FLAG_NO_CREATE
)
if ((pendingIntent == null) and alarmModel.onOff) {
// 알람은 꺼져있는데, 데이터를 켜져있는 경우
alarmModel.onOff = false
} else if ((pendingIntent != null) and alarmModel.onOff.not()) {
// 알람은 켜져있는데, 데이터는 꺼져있는 경우
// 알람을 취소함
pendingIntent?.cancel()
}
return alarmModel
}
private fun renderView(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.onOffText
tag = model
// tag는 View에서 사용할 수 있는 무엇이든 넣을 수 있는 임시저장소 느낌임ㅡ, 나중에 다시 사용하기 위해 model객체를 tag에 임시 저장
}
}
private fun cancelAlarm() {
val pendingIntent = PendingIntent.getBroadcast(
this,
ALARM_REQUEST_CODE,
Intent(this, AlarmReceiver::class.java),
PendingIntent.FLAG_NO_CREATE
)
pendingIntent?.cancel()
}
companion object {
private const val SHARED_PREFERENCES_NAME = "time"
private const val ALARM_KEY = "alarm"
private const val ONOFF_KEY = "onOff"
private const val ALARM_REQUEST_CODE = 1000
}
}
<?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_margin="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/onOffButton"
app:layout_constraintDimensionRatio="1:1"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_white_ring"
/>
<TextView
android:id="@+id/timeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09:30"
android:textColor="@color/white"
android:textSize="50sp"
app:layout_constraintBottom_toTopOf="@id/ampmTextView"
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: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:text="@string/on_alram"
app:layout_constraintBottom_toTopOf="@id/changeAlarmTimeButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/changeAlarmTimeButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/change_time"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
package com.example.aop_part3_chapter11
data class AlarmDisplayModel(
val hour: Int,
val minute: Int,
var onOff: Boolean
) {
// 데이터 클래스에서 데이터를 가져갈 때, 데이터를 다루기 쉽게
// 데이터를 가공하여 내보내는 변수들을 정의
// 예를 들어 hour가 9일 경우 "09"로 텍스트에 나타낼 것이므로, 9를 "09"로 바꾸어 반환시켜주는 변수 ㅡㅡ, 아래의 timeText변수
//
// 혹은 데이터의 값에 따라 달리지는 내용을 여기서 다뤄서 사용하기 편하게 만듬
// -> 예를 들어 hour의 값에 따라 am,pm이 바뀌는 것 ㅡㅡ, 아래의 ampmText변수
//
// 결국 데이터 클래스를 정의할 때, 데이터 클래스의 데이터들을 다루는 부분( 가공하여 반환하거나, 데이터를 분석하여 결과값을 반환하거나 )들을 여기서 정의해주면 사용할 때 편하다.
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"
}
}
package com.example.aop_part3_chapter11
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(){
// PendingIntent를 통해 Broadcast가 수신되었을 때 호출되는 메소드
override fun onReceive(context: Context, intent: Intent) {
// 이전에서는 액티비티 자체가 context라고 볼 수 있었기 때문에 Context대신에 this를 사용할 수 있었지만,
// BroadcastReceiver는 액티비티가 아니기 때문에 context를 따로 받아와야함
creatNotificationChannel(context)
notifyNotification(context)
}
private fun creatNotificationChannel(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())
}
}
companion object {
private const val NOTIFICATION_CHANNEL_ID = "1000"
private const val NOTIFICATION_ID = 100
}
}
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="250dp"
android:height="250dp"/>
<stroke
android:width="1dp"
android:color="@color/white"/>
<solid
android:color="@color/background_black"/>
</shape>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.aop_part3_chapter11">
<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.Aop_part3_chapter11">
<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>
<receiver android:name=".AlarmReceiver"
android:exported="false"/>
</application>
</manifest>
지리네요 포스터가!!
이거 sdk 31 버전넘어오면서 pendingInten 부분 분기 처리 하셔야하는데 혹시 하셨나요? 전 못해서 ㅠㅠ