1. API 레벨 호환성 고려하기
build.gradle 에서 설정하는 targetSdkVersion과 minSdkVersion은 API 레벨을 의미하며 앱 개발에 큰 영향을 주는 중요한 요인입니다.
minSdkVersion 16
targetSdkVersion 30
만약 위와 같이 설정했다면 targetSdkVersion에 설정한 30 버전의 API로 앱을 개발한다는 의미입니다. 그런데 minSdkVersion을 16으로 지정했으므로 이 앱은 16버전(안드로이드 4.1 젤리빈) 기기부터 설치할 수 있습니다. 결국 이 앱은 30 버전의 API로 개발하지만 16버전 기기에서도 오류를 발생하지 않고 동작해야 합니다.
따라서 앱을 개발할 때 minSdkVersion 설정값보다 상위 버전에서 제공하는 API를 사용한다면 호환성을 고려해야 합니다. 예를 들어 안드로이드 API 문서에서 makeText()라는 함수를 찾으면 오른쪽 그림처럼 소개하고 있습니다.
함수 이름 아래 'Added in API level 1' 이라고 표시된 정보는 이 함수가 API 레벨 1에 추가되었다는 것을 나타냅니다. 따라서 makeText() 함수는 minSdkVersion을 16으로 설정한 앱에서 API 레벨 호환성 문제가 발생하지 않습니다.
그런데 addCallback()이라는 함수는 API 레벨이 30입니다. 즉, 30 버전에서 추가된 함수라는 의미입니다. addCallback() 함수는 30 버전 하위에서는 제공하지 않으므로 이 함수를 사용해 앱을 개발하면 30 버전 하위 기기에서 오류가 발생합니다.
이처럼 API 레벨 호환성 문제가 발생하는 클래스나 함수를 사용하면 안드로이드 스튜디오에서 다음처럼 경고나 오류 메시지를 표시합니다. 메시지에는 앱의 minSdkVersion이 16으로 지정되었으므로 addCallback() 함수를 사용하면 문제가 발생한다는 의미입니다. 이처럼 API 레벨 호환성에 문제가 있는 API를 사용할 때는 @ 기호로 시작하는 애노테이션을 추가해 오류를 해결할 수 있습니다.
@RequiresApi(Build.VERSION_CODES.R)
fun showToast() {
...
toast.addCallback(
...
)
toast.show()
}
API 레벨 호환성에 문제가 있는 API를 사용한 함수나 클래스 선언부 위에 @RequiresApi 애너테이션을 추가하면 안드로이드 스튜디오에서 오류가 발생하지 않습니다. @RequiresApi 애노테이션 대신 @TargetApi 애너테이션을 이용해도 됩니다.
@TargetApi(Build.VERSION_CODES.R)
fun showToast() {
...
toast.addCallback(
...
)
toast.show()
}
그런데 API 호환성 애너테이션은 안드로이드 스튜디오에서 오류를 무시하는 설정일 뿐이며 앱이 실행될 때 API 레벨 호환성 문제를 막으려면 직접 코드로 처리해줘야 합니다. 예를 들어 addCallback() 함수는 다음처럼 R 버전에서만 실행되게 할 수 있습니다.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
toast.addCallback(
...
)
}
Build.VERSION.SDK_INT는 앱이 실행되는 기기의 API 레벨입니다. if 문에서 이 값을 이용해 특정 버전에서만 실행하도록 작성할 수 있습니다. 위 코드에서 addCallback() 함수는 API 레벨 30에 추가되었으므로 R버전, 즉 30버전 이상인 기기에서만 실행됩니다.
2. 퍼미션 설정하기
퍼미션이랑 앱의 특정 기능에 부여하는 접근 권한을 말합니다. 내가 개발하는 앱이 다른 앱이나 안드로이드 시스템에서 보호하는 특정 기능을 이용할 때 퍼미션 사용을 설정해야 합니다. 마찬가지로 내가 만든 기능을 다른 앱에서 사용할 수 없도록 보호하고 권한을 얻은 앱에서만 허용하고 싶을 떄 퍼미션을 설정합니다.
퍼미션 설정과 사용 설정
퍼미션을 이해하고자 두 앱을 연동하는 상황을 가정해 보겠습니다. 아래 그림처럼 A 앱과 B 앱이 있고 A 앱의 컴포넌트를 B 앱에서 사용하는 상황을 예로 들겠습니다.
B앱에서 A 앱의 컴포넌트와 연동하는 코드만 잘 구현했다면 A 앱의 컴포넌트를 B앱에서 얼마든지 사용할 수 있습니다. 그런데 만약 A 앱의 컴포넌트에 퍼미션을 설정하면 B 앱에서 연동할 떄 문제가 발생합니다.
A앱의 개발자가 매니페스트 파일에 <permission> 태그로 퍼미션을 설정하면 이를 이용하는 B 앱의 코드를 아무리 잘 구현하더라도 실행되지 않습니다. 이때는 B 앱의 매니페스트 파일에 <uses-permission> 태그로 해당 퍼미션을 이용하겠다고 설정해줘야 합니다.
- <permission> : 기능을 보호하려는 앱의 매니페스트 파일에 설정합니다.
- <uses-permission> : 퍼미션으로 보호된 기능을 사용하려는 앱의 매니페스트 파일에 설정합니다.
이처럼 앱의 컴포넌트를 보호하고 싶을 떄 매니페스트 파일에서 퍼미션을 설정할 수 있으며, 이렇게 퍼미션으로 보호받는 앱을 이용하는 외부 앱은 매니페스트 파일에 해당 퍼미션을 사용하겠다고 설정해야 합니다.
매니페스트 파일에 퍼미션을 설정할 떄는 <permission> 태그와 다음 속성을 이용합니다.
- name : 퍼미션의 이름입니다.
- label, description : 퍼미션을 설명합니다.
- protectionLevel : 보호 수준입니다.
<permission android:name="com.example.permission.TEST_PERMISSION"
android:label="Test Permission"
android:description="@string/permission_desc"
android:protectionLevel="dangerous" />
name 속성값은 개발자가 정하는 이름으로, 퍼미션을 구별하는 식별자 역할을 합니다. label과 description 속성값은 이 퍼미션을 이용하는 외부 앱에서 권한 인증 화면을 출력할 퍼미션의 정보입니다. 그리고 protectionLevel 속성값은 보호 수준을 의미하며 다음과 같은 값을 지정할 수 있습니다.
- normal : 낮은 수준의 보호입니다. 사용자에게 권한 허용을 요청하지 않아도 됩니다.
- dangerous : 높은 수준의 보호입니다. 사용자에게 권한 허용을 요청해야 합니다.
- signature : 같은 키로 인증한 앱만 실행합니다.
- signatureOrSystem : 안드로이드 시스템 앱이거나 같은 키로 인증한 앱만 실행됩니다.
예를 들어 어떤 앱의 매니페스트 파일에 다음처럼 <uses-permission> 을 2개 설정했다고 가정해 봅시다.
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FILE_LOCATION" />
ACCESS_NETWORK_STATE와 ACCESS_FINE_LOCATION은 시스템에 선언된 퍼미션으로 각각 네트워크에 접근하고 사용자 위치를 추적하는 데 필요합니다. ACCESS_NETWORK_STATE는 protectionLevel이 normal 이고 ACCESS_FINE_LOCATION은 dangerous 로 설정되어 있습니다.
이렇게 퍼미션을 2개 사용하는 것으로 설정했지만 이 앱을 설치한 후 앱의 권한 화면을 보면 보호 수준이 dangerous로 설정된 퍼미션만 나옵니다.
매니페스트 파일에 <permission>을 설정했다고 해서 컴포넌트가 보호되지는 않습니다. <permission>을 설정한 다음 이 퍼미션으로 보호하려는 컴포넌트에 적용해야 합니다. 퍼미션을 컴포넌트에 적용할 때는 android:permission 속성을 이용합니다.
<activity android:name=".OneActivity" android:permission="com.example.TEST_PERMISSION">
<intent-filter>
<action android:name="android.intent.action.PICK" />
</intent-filter>
</activity>
이제 이 컴포넌트는 com.example.TEST_PERMISSION에 의해 보호되며 이 컴포넌트를 이용하는 곳에서는 매니페스트 파일에 <uses-permission>을 선언해 줘야 정상으로 실행됩니다.
<uses-permission android:name="com.example.permission.TEST_PERMISSION" />
외부 앱과 연동할 떄 퍼미션 사용을 설정해야 하지만 안드로이드 시스템에서 보호하는 기능을 사용할 때도 매니페스트 파일에 퍼미션 사용 설정을 해야 합니다. 시스템이 보호하는 기능은 대표적으로 다음과 같습니다.
퍼미션 권한 | 설명 |
ACCESS_FINE_LOCATION | 위치 정보 접근 |
ACCESS_NETWORK_STATE | 네트워크 정보 접근 |
ACCESS_WIFI_STATE | 와이파이 네트워크 정보 접근 |
BATTERY_STATS | 배터리 정보 접근 |
BLUETOOTH_ADMIN | 블루투스 장치를 검색하고 페어링 |
CAMERA | 카메라 장치에 접근 |
INTERNET | 네트워크 연결 |
READ_EXTERNAL_STORAGE | 외부 저장소에서 파일 읽기 |
WRITE_EXTERNAL_STORAGE | 외부 저장소에 파일 쓰기 |
READ_PHONE_STATE | 전화기 정보 접근 |
SEND_SMS | 문자 메시지 발신 |
RECEIVE_SMS | 문자 메시지 수신 |
RECEIVE_BOOT_COMPLETED | 부팅 완료 시 실행 |
VIBRATE | 진동 울리기 |
퍼미션 허용 확인
퍼미션은 API 레벨 1 버전부터 있었던 내용이지만 API 레벨 23(Android 6) 버전부터 정책이 바뀌었습니다. API 레벨 23 이전에는 앞에서 살펴본 바와 같이 개발자가 매니페스트 파일에 <uses-permission> 으로 선언만 하면 보호받는 기능을 앱에서 시용하는 데 문제가 없었습니다. 왜냐하면 사용자는 앱의 권한 화면에서 이 앱이 어떤 기능을 이용하는지 확인만 할 수 있었기 때문입니다. 결국 개발자가 "이 퍼미션을 이용한다" 라고 선언만 하면 되는 일종의 신고제였습니다.
그런데 API 레벨 23 버전부터 허가제로 바뀌었습니다. 즉, 개발자가 <uses-permission> 으로 선언했더라도 사용자가 권한 화면에서 이를 거부할 수 있게 되었습니다. 만약 사용자가 앱의 권한 설정에서 특정 퍼미션을 거부하면 <uses-permission> 을 선언하지 않은 것과 같으며 앱에서는 해당 기능을 이용할 수 없습니다. 결국 API 레벨 23버전부터는 매니페스트 파일에 <uses-permission> 을 선언하는 것뿐만 아니라 앱을 실행할 때 사용자가 퍼미션을 거부했는지 확인하고 만약 거부했으면 다시 퍼미션을 허용해 달라고 요청해야 합니다.
사용자가 퍼미션을 허용했는지 확인하려면 checkSelfPermission() 함수를 이용합니다.
open static fun checkSelfPermission(
@NonNull context: Context,
@NonNull permission: String
): Int
두 번째 매개변수가 퍼미션을 구분하는 이름이며 결괏값은 다음 중 하나의 상수로 전달됩니다.
- PackageManager.PERMISSION_GRANTED : 권한을 허용한 경우
- PackageManager.PERMISSION_DENIED : 권한을 거부한 경우
val status = ContextCompat.checkSelfPermission(this,
"android.permission.ACCESS_FINE_LOCATION")
if (status == PackageManager.PERMISSION_GRANTED) {
Log.d("kkang", "permission granted")
} else {
Log.d("kkang", "permission denied")
}
만약 퍼미션을 거부한 상태라면 사용자에게 해당 퍼미션을 허용해 달라고 요청해야 합니다. 사용자에게 퍼미션 허용을 요청할 때는 requestPermissions() 함수를 사용합니다. 이 함수가 실행되면 퍼미션을 요청하는 다이얼로그가 화면에 표시됩니다.
open static fun requestPermissions(
@NonNull activity: Acitivity,
@NonNull permissions: Array<String!>,
@IntRange(0) requestCode: Int
): Unit
두 번째 매개변수의 문자열 배열이 허용을 요청하는 퍼미션의 이름입니다. 한꺼번에 여러 개를 요청할 수 있으며 퍼미션 개수만큼 다이얼로그 화면이 갱신됩니다.
ActivityCompat.requestPermissions(this, arrayOf<String>("android.permission.ACCESS_FINE_LOCATION"), 100)
그런데 이렇게 다이얼로그를 띄워서 퍼미션을 요청하더라도 사용자가 거부할 수 있습니다. 따라서 퍼미션 요청 다이얼로그가 닫힌 후에 사용자가 퍼미션을 허용했는지 확인해야 합니다.
사용자가 다이얼로그에서 퍼미션을 허용했는지 onRequestPermissionsResult() 함수로 알 수 있습니다.
abstract fun onRequestPermissionsResult(
requestCode: Int,
@NonNull permissions: Array<String!>,
@NonNull grantResults: IntArray
): Unit
이 함수를 선언해 놓으면 퍼미션 요청 다이얼로그가 닫힐 때 자동으로 호출되며, 다이얼로그에서 요청한 퍼미션의 결괏값이 세 번째 매개변수로 전달됩니다.
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d("kkang", "permission granted")
} else {
Log.d("kkang", "permission denied")
}
}
3. 다양한 다이얼로그
다이얼로그란 사용자와 상호 작용하는 대화상사입니다. 여기서는 가장 많이 사용하는 토스트, 날짜 또는 시간 입력, 알림 창 등을 알아보고 개발자가 직접 구상하는 커스텀 다이얼로그도 살펴보겠습니다.
토스트 메시지 띄우기
토스트(Toast)는 화면 아래쪽에 잠깐 보였다가 사라지는 문자열을 의미하며 사용자에게 간단한 메시지로 특정한 상황을 알릴 때 사용합니다.
토스트는 Toast의 makeText() 함수로 만듭니다.
- open static fun makeText(context: Context!, text: CharSequence!, duration: Int): Toast!
- open static fun makeText(context: Context!, resId: Int, duration: Int): Toast!
makeText() 함수의 두 번째 매개변수가 출력할 문자열이며, 세 번째 매개변수는 토스트가 화면에 출력되는 시간으로, 보통 다음의 상수를 사용합니다. Toast.LENGTH_SHORT는 일반적으로 3초 정도이며 Toast.LENGTH_LONG은 5초 정도의 시간을 의미합니다.
val toast = Toast.makeText(this, "종료하려면 한 번 더 누르세요", Toast.LENGTH_SHORT)
toast.show()
makeText() 함수로 만든 토스트는 show() 함수로 화면에 출력됩니다. 그런데 토스는 makeText() 함수를 이용하지 않고 다음의 세터 함수로 만들 수도 있습니다.
- open fun setDuration(duration: Int): Unit
- open fun setGravity(gravity: Int, xOffset: Int, yOffset: Int): Unit
- open fun setMargin(horizontalMargin: Float, verticalMargin: Float): Unit
- open fun setText(resId: Int): Unit
setDuration(), setText() 함수를 이용하면 문자열이나 화면에 보이는 시간을 설정할 수 있습니다. 그리고 setGravity()나 setMargin() 함수를 이용하면 토스트가 뜨는 위치를 정할 수도 있습니다.
그리고 API 레벨 30 버전에서는 토스트가 화면에 보이거나 사라지는 순간을 콜백으로 감지해 특정 로직을 수행하는 기능이 추가되었습니다.
@RequiresApi(Build.VERSION.CODES.R)
fun showToast() {
val toast = Toast.makeText(this, "종료하려면 한 번 더 누르세요.", Toast.LENGTH_SHORT)
toast.addCallback(
object : Toast.Callback() {
override fun onToastHidden() {
super.onToastHidden()
Log.d("kkang", "toast hidden")
}
override fun onToastShown() {
super.onToastShown()
Log.d("kkang", "toast shown")
}
})
toast.show()
}
토스트의 콜백을 등록하려면 Toast.Callback 타입의 객체를 토스트 객체의 addCallback() 함수로 등록해주면 됩니다. 이렇게 하면 화면에 토스트가 뜨는 순간 자동으로 콜백 객체의 onToastShown() 함수가 호출되며, 화면에서 사라지는 순간 onToastHidden() 함수가 자동으로 호출됩니다.
날짜 또는 시간 입력받기
앱에서 사용자에게 날짜나 시간을 입력받는 데 사용하는 다이얼로그를 피커(Picker) 다이얼로그라고 합니다. 날짜를 입력받을 때는 데이트 피커 다이얼로그(DatePickerDialog)를, 시간을 입력받을 때는 타임 피커 다이얼로그(TimePickerDialog)를 사용합니다.
날짜를 입력받는 데이트 피커 다이얼로그의 생성자는 다음과 같습니다.
DatePickerDialog(context: Context, listener: DatePickerDialog.OnDateSetListener?, year: Int, month: Int, dayOfMonth: Int)
두 번째 매개변수로 DatePickerDialog.OnDateSetListener 구현 객체를 등록하면 다이얼로그에서 사용자가 설정한 날짜를 콜백 함수로 받을 수 있습니다. 나머지 Int 타입의 매개변수는 처음에 보이는 날짜입니다.
DatePickerDialog(this, object: DatePickerDialog.OnDateSetListener {
override fun onDateSet(p0: DatePicker?, p1: Int, p2: Int, p3: Int) {
Log.d("kkang", "year : $p1, month : ${p2+1}, dayOfMonth : $p3")
}
}, 2020, 8, 21).show()
시간을 입력받는 타임 피커 다이얼로그의 생성자는 다음과 같습니다.
TimePickerDialog(context: Context!, listener: TimePickerDialog.OnTimeSetListener!, hourOfDay: Int, minute: Int, is24HourView: Boolean)
마지막 매개변수로 시간을 24시간과 12시간 형태 중에 어떤 것으로 출력할 것인지를 지정합니다. false로 지정해 12시간 형태로 출력하면 오전/오후를 선택하는 부분이 보입니다. 반면에 true로 지정해 24시간 형태로 출력하면 오전/오후를 선택하는 부분이 보이지 않습니다.
TimePickerDialog(this, object: TimePickerDialog.OnTimeSetListener {
override fun onTimeSet(p0: TimePicker?, p1: Int, p2: Int) {
Log.d("kkang", "time : $p1, minute : $p2")
}
}, 15, 0, true).show()
알림 창 띄우기
안드로이드 다이얼로그의 기본은 이 책에서 알림 창으로 부르는 AlertDialog입니다. 알림 창은 단순히 메시지만 출력할 수도 있고 다양한 화면을 출력할 수도 있습니다. 알림 창은 크게 3가지 영역으로 구분됩니다. 위부터 차례대로 제목, 내용, 버튼 영역이 있습니다.
알림 창의 생성자는 접근 제한자가 protected로 선언돼서 객체를 직접 생성할 수 없습니다. 그 대신 AlertDialog.Builder를 제공하므로 이 빌더를 이용해 알림 창을 만듭니다. 먼저 AlertDialog.Builder를 생성하고 빌더의 세터 함수로 알림 창의 정보를 지정합니다.
다이얼로그의 각 버튼에 해당하는 이벤트 핸들러를 따로 만들 수도 있지만, 한 알림 창의 버튼 이벤트를 하나의 이벤트 핸들러에서 모두 처리할 수도 있습니다.
val eventHandler = object : DialogInterface.OnClickListener {
override fun onClick(p0: DialogInterface?, p1: Int) {
if (p1 == DialogInterface.BUTTON_POSITIVE) {
Log.d("kkang", "positive button click")
} else if (p1 == DialogInterface.BUTTON_NEGATIVE) {
Log.d("kkang", "negative button click")
}
}
}
...
setPositiveButton("OK", eventHandeler)
setPositiveButton("Cancel", eventHandeler)
알림 창의 내용 영역에는 간단한 문자열을 출력하는 setMessage() 말고도 다양한 함수가 있습니다. 만약 목록을 제공하고 이 중 하나를 선택받는 알림 창을 만들고자 한다면 setItems()[목록], setMultiChoiceItems()[체크박스], setSignleChoiceItems()[라디오 버튼] 함수를 이용합니다.
알림 창의 제목, 내용, 버튼을 구성하는 함수 이외에 속성을 설정하는 함수를 사용할 수도 있습니다.
- open fun setCancelable(cancelable: Boolean): AlertDialog.Builder!
- open fun setCanceledOnTouchOutside(cancel: Boolean): Unit
두 함수 모두 사용자의 행동에 따라 알림창을 닫을 것인지를 설정합니다. setCancelable() 함수는 사용자가 뒤로가기 버튼을 눌렀을 때, setCanceledOnTouchOutside() 함수는 알림 창의 바깥 영역을 터치했을 때 매개변수가 true 이면 닫고 false 이면 닫지 않습니다. 기본값은 true 입니다.
AlertDialog.Builder(this).run {
setTitle("items test")
setIcon(android.R.drawable.ic_dialog_info)
setItems(items, object: DialogInterface.OnClickListener {
override fun onClick(p0: DialogInterface?, p1: Int) {
Log.d("kkang", "선택한 과일 : ${items[p1]}")
}
})
setCancelable(false)
setPositiveButton("닫기", null)
show()
}.setCanceledOnTouchOutside(false)
커스텀 다이얼로그 만들기
다이얼로그를 만들다 보면 개발자가 원하는 형태로 창을 구성하고 싶을 떄도 있습니다. 이를 커스텀 다이얼로그라고 합니다. 이 커스텀 다이얼로그도 AlertDialog를 이용합니다.
커스텀 다이얼로그를 어떻게 만드는지 알려면 먼저 LayoutInflater 라는 클래스를 이해해야합니다. 이 클래스는 커스텀 다이얼로그뿐만 아니라 다양한 곳에서 이용됩니다. 그만큼 유용한 클래스이므로 잘 정리해 두면 좋습니다.
LayoutInflater 클래스는 레이아웃 XML 파일을 코드에서 초기화(흔히 전개라고도 표현)하는 기능을 제공합니다. 여기서 초기화란 XML 파일에 선언한 뷰를 코드에서 이용하고자 생성하는 작업을 의미합니다. XML 파일은 텍스트 파일일 뿐이며 결국 코드에서 사용하려면 XML에 선언한 대로 객체를 생성해서 메모리에 할당해야 합니다. 이 작업을 LayoutInflater 가 해줍니다.
액티비티의 화면을 구성하는 레이아웃 XML 파일이라면 LayoutInflator가 아니라 setContentView() 함수를 이용하면 됩니다. 그런데 앱을 개발하다 보면 커스텀 다이얼로그를 위한 XML 파일뿐만 아니라 이후에 나오는 리스트 뷰, 리사이클러 뷰의 항목 화면, 프래그먼트를 위한 XML 파일 등 액티비티의 화면을 목적으로 하지 않는 레이아웃 XML 파일을 많이 만듭니다. 이때 LayoutInflater를 이용합니다.
LayoutInflater로 레이아웃 XML 파일을 초기화하는 작업은 어렵지 않습니다.
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val rootView = inflater.inflate(R.layout.activity_one, null)
inflate() 함수의 반환값은 초기화된 XML의 루트 태그에 해당하는 객체입니다. 만약 XML 파일의 루트 태그가 <LinearLayout>이라면 LinearLayout 객체를 반환합니다.
그런데 만약 뷰 바인딩 기법을 이용한다면 XML 초기화 코드를 조금 더 쉽게 작성할 수 있습니다. 위 코드를 다음처럼 뷰 바인딩으로 작성해도 됩니다.
val binding = ActivityOneBinding.inflate(layoutInflater)
val rootView = binding.root
초기화할 XML에 해당하는 바인딩 클래스의 inflate() 함수를 호출하면서 매개변수로 layoutInflater 객체를 전달만 해주면 자동으로 초기화되고 루트 뷰 객체를 얻을 수 있습니다.
이제 커스텀 다이얼로그를 만드는 방법을 살펴보겠습니다. 먼저 다이얼로그를 구성하는 레이아웃 XML 파일을 만들어야 합니다. res/layout 폴더에 dialog_input.xml 파일을 다음처럼 작성했다고 가정하겠습니다.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="male" />
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="female" />
</RadioGroup>
이제 이 XML 파일을 LayoutInflater로 초기화해서 다이얼로그에 적용하면 됩니다. AlertDialog의 setView() 함수에 매개변수로 뷰 객체를 전달하면 창의 내용 영역에 출력됩니다.
val dialogBinding = DialogInputBinding.inflate(layoutInflater)
AlertDialog.Builder(this).run {
setTitle("Input")
setView(dialogBinding.root)
setPositiveButton("닫기", null)
show()
}
4. 소리와 진동 알림
소리 알림
사용자에게 짧은 소리로 특정한 상황을 알릴 떄가 있습니다. 대표적으로 문자나 카카오톡 같은 앱에서 새로운 메시지를 전달받으면 사용자 알림 효과로 짧은 소리를 이용해 알려줍니다. 이런 목적으로 사용하는 짧은 소시를 이 책에서는 알림음이라고 합니다.
알림은은 카카오톡처럼 자체 녹음한 음원을 쓸 수도 있지만 안드로이드 시스템에 등록된 소시를 이용할 수도 있습니다. 먼저 시스템에 등록된 소리를 이용하는 방법을 살펴보겠습니다.
안드로이드 시스템은 알림, 알람, 벨소리 등의 소리를 제공하며 이 소리는 RingtonManager로 얻을 수 있습니다.
val notification: Uri = RingtonManager.getDefaultUri(RingtonManager.TYPE_NOTIFICATION)
val ringtone = RingtoneManager.getRingtone(applicationContext, notification)
ringtone.play()
먼저 RingtonManager.getDefaultUri() 함수를 이용해 소리의 식별값을 얻습니다. 이 식별값은 Uri 객체이며 이 값을 RingtonManager.getRingtone() 함수의 두 번째 매개변수로 전달하면 소리를 재생하는 Ringtone 객체를 얻습니다. 이 Ringtone 객체의 play() 함수를 호출하면 비로소 소리가 재생됩니다.
이번에는 앱에서 자체 음원을 준비해서 재생하는 방법을 보겠습니다. 음원 파일은 리소스로 등록해서 이용해야 하는데 음원 리소스 디렉터리는 res/raw (res/raw/fallbackring.ogg) 입니다.
음원을 재생하는 클래스는 MediaPlayer 입니다. 이 클래스에 리소스 정보를 지정하고 play() 함수를 호출하면 음원이 재생됩니다.
val player: MediaPlayer = MediaPlayer.create(this, R.raw.fallbrackring)
player.start()
진동 알림
진동도 사용자 알림 효과로 많이 이용합니다. 앱에서 진동이 울리게 하려면 먼저 매니페스트 파일에 <uses-permission>으로 퍼미션을 얻어야 합니다.
<uses-permission android:name="android.permission.VIBRATE" />
진동은 Vibrator 클래스를 이용하며 getSystemService() 함수로 객체를 얻습니다. Vibrator 객체의 함수로 진동을 한 번이나 여러 번 울리게 할 수 있습니다.
val vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
그런데 API 레벨 1부터 제공한 진동 알림 함수가 26버전 (Android 8)에서 Deprecated 되었습니다. 26 버전에서 새로운 함수를 제공하지만 이전 버전의 기기 사용자를 위해 여전히 사용해야 합니다. 결국 API 레벨 호환성을 고려해서 사용해야 합니다.
- open fun vibrate(milliseconds: Long): Unit
- open fun vibrate(pattern: LongArray!, repeat: Int): Unit
첫 번쨰 함수의 매개변수는 Long 타입 하나입니다. 이 매개변수는 진동이 울리는 시간을 의미합니다. 만약 500으로 설정하면 0.5초간 진동이 울립니다.
두 번째 함수는 매개변수가 2개인데 진동을 반복해서 울리는 함수입니다. 첫 번째 매개변수에는 진동 패턴을 배열로 지정합니다. 만약 500, 1000, 500, 2000 의 배열값을 전달하면 0.5초 쉬고 1초간 울리고 0.5초 쉬고 2초간 울립니다. 두 번째 매개변수는 이 패턴을 얼마나 반복할지를 지정합니다. 만약 -1로 지정하면 반복하지 않고 패턴대로 한 번만 진동이 울리고, 0으로 지정하면 코드에서 cancel() 함수로 진동 알림을 끄지 않는 한 패턴대로 계속 울립니다.
그런데 API 레벨 26부터는 진동 정보를 VibrationEffect 객체로 지정할 수 있는 함수를 제공합니다. VibrationEffect 객체로는 진동이 울리는 시간 이외에 진동의 세기까지 제어할 수 있습니다. 다음은 API 레벨 26에서 제공하는 진동 알림 함수입니다.
- open fun vibrate(vibe: VibrationEffect!): Unit
vibrate() 함수의 매개변수에 VibrationEffect 객체를 지정합니다. VibrationEffect는 진동 정보를 지정하는 다음 함수를 제공합니다.
- open static fun createOneShot(milliseconds: Long, amplitude: Int): VibrationEffect!
이 함수로 만든 VibrationEffect 객체를 vibrate() 함수에 대입하면 첫 번째 매개변수의 시간 동안 진동이 울립니다. 그리고 두 번째 매개변수를 이용해 진동의 세기를 지정할 수 있습니다. 진동의 세기는 0 ~ 255 사이의 숫자로 표현하며, 0으로 지정하면 진동이 울리지 않고 255는 기기에서 지원하는 가장 센 강도로 올립니다.
이렇게 숫자를 직접 대입해도 되고 VibrationEffect.DEFAULT_AMPLITUDE처럼 상수를 지정해 기기가 정의한 기본 세기로 진동이 울리게 할 수도 있습니다.
val vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
vibrator.vibrate(VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(500)
}
이번에는 반복해서 진동을 울리는 createWaveform() 함수를 살펴보겠습니다.
- open static fun createWaveform(timings: LongArray!, amplitudes: IntArray!, repeat: Int): VibrationEffect!
첫 번째 매개변수는 마찬가지로 진동이 울리는 시간 패턴의 배열이며, 두 번째 매개변수는 진동 세기 패턴의 배열입니다. 그리고 세 번째 매개변수가 이 패턴의 반복 횟수입니다.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
vibrator.vibrate(VibrationEffect.createWaveform(longArrayOf(500, 1000, 500, 2000), intArrayOf(0, 50, 0, 200), -1))
} else {
vibrator.vibrate(longArrayOf(500, 1000, 500, 2000), -1)
}
5. 알림 띄우기
알림 채널
상태 바는 화면 상단의 한 줄을 의미하며 이곳에 배터리, 네트워크, 시간 등 시스템의 상태 정보가 출력됩니다. 이 상태바에 앱의 정보를 출력하는 것을 알림(Notification)이라고 합니다.
원래 상태 바는 시스템에서 관리하는 곳이며 앱이 직접 제어할 수 없습니다. 그런데 앱에서 시스템에 의뢰하면 시스템에서 관리하는 상태 바에 앱의 알림을 출력할 수 있습니다. 따라서 앱의 화면을 구성하거나 사용자 이벤트를 처리하는 프로그래밍과는 구조가 다르며 알림을 위해 제공하는 API 를 사용해야 합니다.
알림은 NotificationManager의 notify() 함수로 발생합니다. notify() 함수에는 NotificationCompat.Builder가 만들어주는 Notification 객체를 대입하며 이 객체에는 알림 정보가 저장됩니다. 그런데 NotificationCompat.Builder를 만들때 NotificationChannel 정보를 대입해줘야 합니다.
정리하자면, NotificationChannel 로 알림 채널을 만들고 이 채널 정보를 대입해 NotificationCompat.Builder를 만든 다음, 이 빌더로 Notification 객체를 만듭니다. 이 Notification 객체를 NotificationManager의 notify() 함수에 대입하는 구조입니다.
Notification을 만들려면 NotificationCompat.Builder가 필요한데 빌더를 만드는 방법이 API 레벨 26(Android 8) 버전부터 변경되었습니다. 26버전 이전까지는 다음 생성자를 이용했습니다.
- Builder(context: Context!)
즉, 26버전 이전까지는 빌더를 만들 때 NotificationChannel 정보가 필요 없었습니다. 하지만 26버전에서 이 생성자는 deprecated 되었으며 빌더를 만들 때 다음 생성자를 사용해야합니다. 즉, NotificationChannel을 만들고 이 채널의 식별값을 빌더의 생성자 매개변수에 지정해줘야 합니다.
- Builder(context: Context!, channelId: String!)
API 레벨 26 버전에서 채널이라는 개념이 추가되었는데 이는 앱의 알림을 채널로 구분하겠다는 의도입니다. 사용자가 환경 설정에서 어떤 앱의 알림을 받을지 말지를 설정할 수 있습니다. 다음은 이러한 알림 채널의 생성자입니다.
- NotificationChannel(id: String!, name: CharSequence!, importance: Int)
매개변수로 채널의 식별값과 설정 화면에 표시할 채널 이름을 문자열로 지정합니다. 세 번째 매개변수는 이 채널에서 발생하는 알림의 중요도이며 다음의 상수로 지정합니다.
중요도 상수 | 설명 |
NotificationManager.IMPORTANCE_HIGH | 긴급 상황으로 알림음이 울리며 헤드업으로 표시 |
NotificationManager.IMPORTANCE_DEFAULT | 높은 중요도이며 알림음이 울림 |
NotificationManager.IMPORTANCE_LOW | 중간 중요도이며 알림음이 울리지 않음 |
NotificationManager.IMPORTANCE_MIN | 낮은 중요도이며 알림음도 없고 상태 바에도 표시되지 않음 |
채널의 각종 정보는 함수나 프로퍼티로 설정할 수 있습니다.
- fun setDescription(description: String!): Unit : 채널의 설정 문자열
- fun setShowBadge(showBadge: Boolean): Unit : 홈 화면의 아이콘에 배지 아이콘 출력 여부
- fun setSound(sound: Uri!, audioAttributes: AudioAttributes!): Unit : 알림음 발생
- fun enableLights(lights: Boolean): Unit : 불빛 표시 여부
- fun setLightColor(argb: Int): Unit :불빛이 표시된다면 불빛의 색상
- fun enableVibration(vibration: Boolean): Unit : 진동을 울릴지 여부
- fun setVibrationPattern(vibrationPattern: LongArray!): Unit : 진동을 울린다면 진동의 패턴
setDescription() 함수에 전달하는 문자열은 설정 화면에서 채널을 설명하는 곳에 보입니다. 그리고 setShowBadge(true)로 설정하면 홈 화면의 앱 아이콘에 확인하지 않은 알림 개수가 표시된 배지 아이콘이 보입니다.
이러한 내용을 반영하여 알림 빌더(Notification.Builder)를 다음처럼 작성할 수 있습니다.
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val builder : NotificationCompat.Builder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
val channelId = "one-channel"
val channelName = "My Channel One"
val channel = NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
)
// 채널에 다양한 정보 설정
channel.description = "My Channel One Description"
channel.setShowBadge(true)
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ALARM)
.build()
channel.setSound(uri, audioAttributes)
channel.enableLights(true)
channel.lightColor = Color.RED
channel.enableVibration(true)
channel.vibrationPattern = longArrayOf(100, 200, 100, 200)
// 채널을 NotificationManager에 등록
manager.createNotificationChannel(channel)
// 채널을 이용해 빌더 생성
builder = NotificationCompat.Builder(this, channelId)
} else {
builder = Notification.Builder(this)
}
알림 객체
알림 빌더를 만들었으면 이 빌더를 이용해 Notification 객체를 만들어야 합니다. 이 객체에 출력할 이미지, 문자열 등의 정보를 담습니다. 앱에서 알림이 발생하면 상태바에 이미지가 출력됩니다. 이를 스몰 아이콘이라 부릅니다. 그리고 상태바를 끌어내리면 알림정보가 보입니다.
알림은 스몰 아이콘과 발생 시각, 제목, 내용 등으로 구성됩니다. 이러한 알림 정보를 Notification 객체에 설정해야 합니다.
builder.setSmallIcon(android.R.drawable.ic_notification_overlay)
builder.setWhen(System.currentTimeMillis())
builder.setContentTitle("Content Title")
builder.setContentText("Content Message")
빌더의 세터 함수를 이용해 알림의 구성 정보를 설정합니다. 여기까지 작성했다면 이제 NotificationManager 클래스의 notify() 함수를 이용해 알림을 띄웁니다.
manager.notify(11, builder.build())
builder.build() 함수가 Notification 객체를 만들고 이로써 알림이 발생합니다. 첫 번째 매개변숫값은 알림을 식별하는 데 사용하는 숫자이며 개발자가 임의로 지정합니다. 이 식별값은 사용자 폰에 발생한 알림을 코드에서 취소할 때 사용합니다. 이때 cancel() 함수를 이용하며 매개변수로 취소할 알림의 식별값을 전달합니다.
manager.cancel(11)
사용자가 알림을 터치하면 이벤트가 발생할 수 있으며 이때 알림은 화면에서 자동으로 사라집니다(취소). 또한 사용자가 알림을 손가락으로 밀어서(스와이프) 취소할 수 있습니다. 그런데 터치나 스와이프를 하더라도 알림이 사라지지 않게 하려면 빌더의 세터 함수로 지정해야 합니다.
builder.setAutoCancel(false)
builder.setOngoing(true)
setAutoCancel(false) 로 지정하면 알림을 터치할 때 이벤트는 발생하지만 알림이 사라지지는 않습니다. 또한 setOngoing(true)로 지정하면 사용자가 알림을 스와이프해도 사라지지 않습니다. 만약 2가지를 모두 설정했다면 사용자가 알림을 취소할 수 없으며 결국 코드에서 특정 순간에 cancel() 함수로 취소해야 합니다.
알림 구성
지금부터 자주 사용하는 알림 구성을 살펴보겠습니다.
1. 알림 터치 이벤트
알림은 사용자에게 앱의 상태를 간단하게 알려 주는 기능을 하는데, 사용자가 더 많은 정보를 요구할 수 있습니다. 그래서 대부분 앱은 사용자가 알림을 터치했을 때 앱의 액티비티 화면을 실행합니다. 이렇게 하려면 알림의 터치 이벤트를 구현해야 합니다.
그런데 알림은 앱이 관할하는 화면이 아니며 시스템에서 관리하는 상태 바에 출력하는 정보입니다. 그러므로 이 알림에서 발생한 터치 이벤트는 앱의 터치 이벤트로 처리할 수 없습니다. 즉, 이전에 살펴봤던 onTouchEvent() 함수로는 처리할 수 없습니다. 결국 앱에서는 사용자가 알림을 터치했을 때 실행해야 하는 정보를 Notification 객체에 담아 두고, 실제 이벤트가 발생하면 Notification 객체에 등록된 이벤트 처리 내용을 시스템이 실행하는 구조로 처리합니다.
사용자가 알림을 터치하면 앱의 액티비티 또는 브로드 캐스트 리시버를 실행해야 하는데 이를 실행하려면 인텐트(intent)를 이용해야 합니다. 인텐트는 뒤에서 다루도록 하고 이곳에서는 간단하게 '앱의 컴포넌트를 실행하는 데 필요한 정보' 정도로 생각하면 됩니다.
사용자가 알림 내용을 터치할 때 앱의 컴포넌트를 실행하려면 먼저 인텐트를 준비해야 합니다. 이 인텐트가 있어야 알림에서 원하는 컴포넌트를 실행할 수 있습니다. 그런데 인텐트는 앱의 코드에서 준비하지만 이 인텐트로 실제 컴포넌트를 실행하는 시점은 앱에서 정할 수 없습니다. 따라서 인텐트를 준비한 후 Notification 객체에 담아서 이벤트가 발생할 때 인텐트를 실행해 달라고 시스템에 의뢰해야 합니다. 이러한 의뢰는 PendingIntent 클래스를 이용합니다. PendingIntent 클래스는 컴포넌트별로 실행을 의뢰하는 함수를 제공합니다. 각 함수의 세 번째 매개변수에 인텐트 정보를 등록합니다.
- static fun getActivity(context: Context!, requestCode: Int, intent: Intent!, flags: Int): PendingIntent!
- static fun getBroadcast(context: Context!, requestCode: Int, intent: Intent!, flags: Int): PendingIntent!
- static fun getService(context: Context!, requestCode: Int, intent: Intent!, flags: Int): PendingIntent!
다음은 알림을 터치했을 때 DetailActivity 라는 액티비티의 실행 정보를 Notification 객체에 등록하는 코드입니다. 그리고 터치 이벤트 등록은 빌더의 setContentIntent() 함수를 이용합니다.
val intent = Intent(this, DetailActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT)
builder.setContentIntent(pendingIntent) // 터치 이벤트 등록
2. 액션
알림에는 터치 이벤트 외에도 액션을 최대 3개까지 추가할 수 있습니다. 알림에서 간단한 이벤트는 액션으로 처리합니다. 알람 앱의 알람 취소, 전화 앱의 전화 수신이나 거부 등이 대표적인 예입니다.
액션도 사용자 이벤트 처리가 목적입니다. 따라서 알림 터치 이벤트와 마찬가지로 사용자가 액션을 터치할 때 실행할 인텐트 정보를 PendingIntent 로 구성해서 등록해야 합니다. 실제 사용자가 액션을 터치하면 등록된 인텐트가 시스템에서 실행되어 이벤트가 처리되는 구조입니다. 액션을 등록할 때는 addAction() 함수를 이용합니다.
open fun addAction(action: Notification.Action!): Notification.Builder
매개변수로 액션의 정보를 담는 Action 객체를 전달합니다. Action 객체는 Action.Builder로 만듭니다.
Builder(icon: Int, title: CharSequence!, intent: PendingIntent!)
액션 빌더의 생성자에 아이콘 정보와 액션 문자열, 그리고 사용자가 액션을 클릭했을 때 이벤트를 위한 PendingIntent 객체를 전달합니다.
val actionIntent = Intent(this, OneReceiver::class.java)
val actionPendingIntent = PendingIntent.getBroadcast(this, 20, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT)
builder.addAction(
NotificationCompat.Action.Builder(
android.R.drawable.stat_notify_more,
"Action",
actionPendingIntent
).build()
)
3. 원격 입력
원격 입력이란 알림에서 사용자가 입력을 직접 받는 기법입니다. 원래 사용자 입력을 받으려면 에디트 텍스트 같은 뷰가 있는 화면을 제공해야 하는데, 간단한 입력은 앱의 화면을 통하지 않고 원격으로 액션에서 직접 받아서 처리할 수 있습니다.
val KEY_TEXT_REPLY = "key_text_reply"
var replyLabel = "답장"
var remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run {
setLabel(replyLabel)
build()
}
RemoteInput 은 API 레벨 20에서 추가되었습니다. 따라서 앱의 minSdkVersion을 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, PendingIntent.FLAG_MUTABLE
)
앞에서 살펴본 PendingIntent와 특별한 차이점은 없습니다. 단지 RemoteIntent 은 알림에서 사용자의 입력을 받는 것이므로 이벤트로 액티비티를 실행해 앱의 화면이 출력되게 하지 않고 브로드캐스트 리시버를 실행해 백그라운드에서 사용자 입력을 처리하는 게 일반적입니다.
이제 알림에 액션을 등록하면서 RemoteInput 정보를 함께 설정합니다.
builder.addAction(
NotificationCompat.Action.Builder(
R.drawable.send,
"답장",
replyPendingIntent
).addRemoteInput(remoteInput).build()
)
그런데 전송할 떄 실행되는 브로드캐스트 리시버에서 사용자가 입력한 글을 받을 때는 다음과 같은 코드를 사용합니다.
val replyTxt = RemoteInput.getResultsFromIntent(intent)
?.getCharSequence("key_text_reply")
이 코드에서 중요한 부분은 getCharSequence() 함수의 매개변수로 지정한 문자열입니다. 이 문자열이 RemoteInput 을 만들 떄 지정한 식별값과 같아야 합니다. 또한 브로드캐스트 리시버에서 사용자의 입력 글을 받은 후 알림을 갱신해줘야 합니다. 이때 RemoteInput의 알림을 띄울 때 사용했던 알림 객체의 식별값을 지정합니다.
manager.notify(11, builder.build())
4. 프로그레스
앱에서 어떤 작업이 이루어지는 데 시간이 걸린다면 보통 알림을 이용해 일의 진행 상황을 프로그레스에 바로 알려줍니다. 대표적인 예가 앱에서 서버로 파일을 올리거나 내려받는 경우입니다.
알림의 프로그레스 바는 화면을 따로 준비하지 않고 빌더에 setProgress() 함수만 추가해주면 자동으로 나옵니다.
open fun setProgress(max: Int, progress: Int, indeterminate: Boolean): Notification.Builder
첫 번쨰 매개변수가 프로그레스 바의 최댓값이며 두 번쨰 매개변수가 진행값입니다. 처음에 현잿값을 지정한 후 스레드 같은 프로그램을 사용해 진행값을 계속 바꾸면서 상황을 알려주면 됩니다. 그리고 세번째 매개변수값이 true이면 프로그레스 바는 왼쪽에서 오른쪽으로 계속 흘러가듯이 표현됩ㄴ디ㅏ.
다음은 스레드를 이용해 10초 동안 프로그레스 바의 진행값을 증가시키는 예입니다.
builder.setProgress(100, 0, false)
manager.notify(11, builder.build())
thread {
for (i in 1..100) {
builder.setProgress(100, i, false)
manager.notify(11, builder.build())
SystemClock.sleep(100)
}
}
알림 스타일
1. 큰 이미지 스타일
알림에 문자열, 액션 외에 큰 그림도 있습니다. 이 이미지를 BigPictureStyle로 만듭니다.
val bigPicture = BitmapFactory.decodeResource(resources, R.drawable.test)
val bigStyle = NotificationCompat.BigPictureStyle()
bigStyle.bigPicture(bigPicture)
builder.setStyle(bigStyle)
BigPictureStyle 객체의 bigPicture 프로퍼티에 출력할 이미지를 비트맵 형식으로 지정하며, 이렇게 만든 BigPicture 객체를 빌더의 setStyle() 함수에 지정합니다.
2. 긴 텍스트 스타일
알림에 긴 문자열을 출력해 사용자가 앱을 실행하지 않아도 많은 정보를 알 수 있게 할 수 있습니다. 대표적으로 이메일 앱은 이메일을 수신했을 때 알림으로 제목과 발신자뿐만 아니라 일부 내용도 보여줍니다. 긴 문자열 알림은 BigTextStyle을 이용합니다.
val bigTextStyle = NotificationCompat.BigTextStyle()
bigTextStyle.bigText(resources.getString(R.string.long_text))
builder.setStyle(bigTextStyle)
3. 상자 스타일
상자 스타일 알림은 문자열을 목록으로 출력하는 InboxSytle을 이용합니다. 하나의 알림에 문자열을 여러 개 나열할 떄 유용합니다.
val style = NotificationCompat.InboxStyle()
style.addLine("1코스 - 수락.불암산코스")
style.addLine("2코스 - 용마.아차산코스")
style.addLine("3코스 - 고덕.일자산코스")
style.addLine("4코스 - 대모.우면산코스")
builder.setStyle(style)
4. 메시지 스타일
메시지 스타일 알림은 여러 사람이 주고받은 메시지를 구분해서 출력할 때 사용합니다. 메시지 스타일에 보일 메시지는 각각 Message 객체로 표현합니다.
Message(text: CharSequence, timestamp: Long, sender: Person?)
하나의 메시지는 3가지 정보로 표현됩니다. 메시지 내용, 매시지 발생 시각, 발신자 정보 입니다. 이때 세 번째 매개변수는 Person 객체로 표현합니다. Person은 출력될 한 사람의 정보를 담는 클래스입니다.
val sender1: Person = Person.Builder()
.setName("kkang")
.setIcon(IconCompat.createWithResource(this, R.drawable.person1))
.build()
val sender2: Person = Person.Builder()
.setName("kim")
.setIcon(IconCompat.createWithResource(this, R.drawable.person2))
.build()
Person은 API 레벨 28 버전에 추가된 클래스이므로 API 레벨 호환성을 위해 androidx.core.app.Person 라이브러리를 임포트해야 합니다. 이렇게 만든 Person 객체를 Message에 대입해서 이용합니다.
val message1 = NotificationCompat.MessagingStyle.Message(
"hello",
System.currentTimeMillis(),
sender1
)
val message2 = NotificationCompat.MessagingStyle.Message(
"world",
System.currentTimeMillis(),
sender2
)
이제 Message 객체를 MessageStyle에 대입합니다.
val messageStyle = NotificationCompat.MessagingStyle(sender1)
.addMessage(message1)
.addMessage(message2)
builder.setStyle(messageStyle)
6. 카카오톡 알림 만들기
새 모듈을 만든 후 뷰 바인딩 기법을 이용해야 하므로 모듈 수준의 그레들 파일에서 다음 코드를 추가합니다.
1. 새 모듈 생성하기
android {
...
viewBinding.isEnabled = true
}
2. 파일 복사하기
drawable 디렉토리의 big.jpg, small.png, send.png 파일을 res/drawable 디렉터리에 복사합니다. 또한 layout/activity_main.xml 파일을 res/layout 디렉터리에 복사해 이전 파일을 대체합니다.
<activity_main.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<Button
android:id="@+id/notificationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="알림 발생"/>
</LinearLayout>
3. 브로드캐스트 리시버 작성하기
MainActivity.kt 파일이 있는 위치에 브로드캐스트 리시버 파일을 만들어줍니다.
<ReplyReceivier.kt>
package com.example.myapplication6
import android.app.NotificationManager
import android.app.RemoteInput
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class ReplyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// 알림의 입력 글 획득
val replyTxt = RemoteInput.getResultsFromIntent(intent)
?.getCharSequence("key_text_reply")
Log.d("kkang", "replyTxt:$replyTxt")
// 알림 취소
val manager = context.getSystemService(
AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
manager.cancel(11)
}
}
4. 메인 액티비티 작성하기
<MainActivity.kt>
package com.example.myapplication6
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.media.AudioAttributes
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import com.example.myapplication6.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) {
if (it.all { permission -> permission.value == true }) {
noti()
} else {
Toast.makeText(this, "permission denied...", Toast.LENGTH_SHORT).show()
}
}
binding.notificationButton.setOnClickListener {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
"android.permission.POST_NOTIFICATIONS"
) == PackageManager.PERMISSION_GRANTED
) {
noti()
} else {
permissionLauncher.launch(
arrayOf(
"android.permission.POST_NOTIFICATIONS"
)
)
}
}else {
noti()
}
}
}
fun noti(){
val manager = getSystemService(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(this, channelId)
}else {
// 26 버전 이하
builder = NotificationCompat.Builder(this)
}
// 알림의 기본 정보
builder.run {
setSmallIcon(R.drawable.small)
setWhen(System.currentTimeMillis())
setContentTitle("홍길동")
setContentText("안녕하세요.")
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())
}
}
5. 앱 실행하기
<Run> 을 클릭해 앱을 실행합니다. 앱이 실행되면 화면 가운데의 <알림 발생>을 클릭합니다. 그러면 상태 바에 스몰아이콘이 표시됩니다. 상태바를 내리면 알림을 확인할 수 있고 <답장>을 클릭하면 글을 입력할 수 있습니다.
'Android' 카테고리의 다른 글
[깡쌤의 안드로이드 프로그래밍 with 코틀린][4. 구글의 라이브러리로 화면 구성하기] 12. 머티리얼 라이브러리 (0) | 2024.12.25 |
---|---|
[깡쌤의 안드로이드 프로그래밍 with 코틀린][4. 구글의 라이브러리로 화면 구성하기] 11. 제트팩 라이브러리 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 9. 리소스 활용하기 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 8. 사용자 이벤트 처리하기 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 7. 뷰를 배치하는 레이아웃 (0) | 2024.12.25 |