『Do it! 깡샘의 안드로이드 앱 프로그래밍 with 코틀린』 교재를 바탕으로 정리한 내용입니다.
minSdkVersion 16
targetSdkVersion 31
// @TargetApi(Build.VERSION_CODES.R)
@RequiresApi(Build.VERSION_CODES.R)
fun showToast() {
(...생략...)
toast.Callback(
(...생략...)
)
toat.show()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
fun showToast() {
(...생략...)
toast.Callback(
(...생략...)
)
toat.show()
}
}
앱의 특정 기능에 부여하는 접근 권한으로, 다른 앱이나 안드로이드 시스템에서 보호하는 특정 기능을 이용하거나 다른앱에서 사용할 수 없도록 기능을 보호하고 싶을 때 퍼미션을 설정한다.
: 기능을 보호하려는 앱의 매니페스트 파일에 설정
: 퍼미션으로 보호된 기능을 사용하려는 앱의 매니페스트 파일에 설정
👉 A앱의 컴포넌트를 B앱에서 사용하려고 하는 경우
B앱의 입장에서 생각해보면,
퍼미션 적용 안한 상태 → A앱의 컴포넌트 사용 가능
A앱 컴포넌트에 설정한 상태 → A앱의 컴포넌트 사용 불가능
B앱에서 해당 퍼미션에 대해 태그 설정 → A앱 컴포넌트 사용 가능
<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 : 안드로이드 시스템 앱이거나 같은 키로 인증한 앱만 실행
👉 protectionLevel 예시
ACCESS_NETWORK_STATE : normal
ACCESS_FINE_LOCATION : dangerous
→ protectionLevel이 다른 두 퍼미션을 아래처럼 사용 설정하면, 앱 설치한 후 앱의 권한 화면에는 보호 수준이 dangerous인 퍼미션만 나타남
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<activity
android:name=".MainActivity"
android:permission="com.example.TEST_PERMISSION">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<uses-permission android:name="com.example.permission.TEST_PERMISSION" />
// 퍼미션 허용 확인 함수
open static fun checkSelfPermission(
@NonNULL context: Context,
@NonNull permission: String
) : Int
// 권한 허용한 경우 PackageManager.PERMISSION_GRANTED 상수 반환
// 권한 거부한 경우 PackageManager.PERMISSION_DENIED 상수 반환
// 퍼미션 허용 요청 함수
// 함수가 실행 시 퍼미션을 요청하는 다이얼로그가 화면에 나타남
open static fun requestPermissions(
@NonNULL activity: Activity,
@NonNULL permissions: Array<String!>,
@IntRange(0) requestCode: Int
): Unit
// 퍼미션 요청 결과 확인 함수
abstract fun onRequestPermissionResult(
requestCode: Int,
@NonNULL permissions: Array<String!>,
@NonNULL grantResults: IntArray
): Unit
Toast.makeText(this, "종료하려면 한 번 더 누르세요", Toast.LENGTH_SHORT).show()
open fun setDuration(duration: Int): Unit
open fun setText(resId: Int): Unit
토스트가 뜨는 위치를 정할 수 있는 세터 함수
open fun setGravity(gravity: Int, sOffset: Int, yOffset: Int): Unit
open fun setMargin(horizontalMargin: Float, verticalMargin: Float): Unit
@RequiresApi(Build.VERSION_CODES.R)
fun showToast() {
val toast = Toast.makeText(this, "종료하려면 한 번 더 누르세요", Toast.LENGTH_SHORT)
toast.addCallback(
object : Toast.Callback() {
// 토스트가 뜨는 순간 호출
override fun onToastShown() {
super.onToastShown()
}
// 토스트가 사라지는 순간 호출
override fun onToastHidden() {
super.onToastHidden()
}
})
toast.show()
}
date.setOnClickListener {
val cal = Calendar.getInstance(
val dateSetListener =
DatePickerDialog.OnDateSetListener { view, year, month, dayOfMonth ->
Log.d("kkang", "${year}년 ${month + 1}월 ${dayOfMonth}일") }
DatePickerDialog(
this,
dateSetListener,
cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)
).show()
}
time.setOnClickListener {
val cal = Calendar.getInstance()
val timeSetListener =
TimePickerDialog.OnTimeSetListener { view, hourOfDay, minute ->
Log.d("kkang", "${hourOfDay}시 ${minute}분") }
TimePickerDialog(
this,
timeSetListener,
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), true
).show()
}
AlertDialog 사용
알림창은 위부터 차례대로 제목, 내용, 버튼 영역으로 구분 → 원하는 영역만 지정해서 사용 가능
알림창은 생성자가 protected로 선언되어 객체 직접 생성이 불가능하므로 AlertDialog.Builder 사용
알림 창의 아이콘, 제목, 내용을 지정하는 함수
알림 창의 버튼을 지정하는 함수
open fun setPositiveButton(text: CharSequence!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
open fun setNegativeButton(text: CharSequence!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
open fun setNeutralButton(text: CharSequence!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
→ 첫 번째 매개변수는 버튼의 문자열, 두 번째 매개변수는 버튼 클릭 시 이벤트 핸들러 전달
❗버튼 클릭 시 처리할 내용 없으면 두 번째 매개변수에 null 대입
❗버튼은 최대 3개까지 추가할 수 있으므로, 같은 함수 여러번 사용 시 하나만 나타나게
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", eventHandler)
setNegativeButton("Cancel", eventHandler)
dialog.setOnClickListener {
AlertDialog.Builder(this).run {
setTitle("test dialog")
setIcon(android.R.drawable.ic_dialog_info)
setMessage("정말 종료하시겠습니까?")
setPositiveButton("OK", null)
setNegativeButton("Cancel", null)
setNeutralButton("More", null)
setPositiveButton("Yes", null)
setNegativeButton("No", null)
show()
}
}
open fun setItems(items: Array<Char Sequence!>!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
open fun setMultiChoiceItems(items: Array<Char Sequence!>!, checkedItems: BooleanArray!, listener: DialogInterface.OnMultiChoiceClickListener!): AlertDialog.Builder!
open fun setSingleChoiceItems(items: Array<Char Sequence!>!, checkedItem: Int, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
// 목록 출력
val items = arrayOf<String>("사과", "복숭아", "수박", "딸기")
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]}")
}
})
setPositiveButton("닫기", null)
show()
}
// 체크박스
setMultiChoiceItems(items, booleanArrayOf(true, false, true, false),
object : DialogInterface.OnMultiChoiceClickListener {
override fun onClick(p0: DialogInterface?, p1: Int, p2: Boolean) {
Log.d("kkang", "${items[p1]}이 ${if (p2) "선택되었습니다." else "선택 해제되었습니다."}")
}
})
// 라디오 버튼
setSingleChoiceItems(items, 1, object : DialogInterface.OnClickListener {
override fun onClick(p0: DialogInterface?, p1: Int) {
Log.d("kkang", "${items[p1]}이 선택되었습니다.")
}
})
open fun setCancelable(cancelable: Boolean): AlertDialog.Builder!
매개변수 true로 설정 시 알림 창의 바깥 영역을 터치했을 때 닫힘
open fun setCancelabledOnTouchOutside(cancel: Boolean): Unit
AlertDialog.Builder(this).run {
(...생략...)
})
setCancelable(false)
setPositiveButton("닫기", null)
show()
}.setCanceledOnTouchOutside(false)
LayoutInflater 클래스란?
→ 레이아웃 XML 파일을 코드에서 초기화하는 기능 제공
즉, XML에 선언한 뷰를 코드에서 이용하기 위해 생성해주는 역할
// XML파일 초기화
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val rootView = inflater.inflate(R.layout.activity_one, null)
// 뷰 바인딩 기법 적용한 XML파일 초기화
val binding = ActivityOneBinding.inflate(layoutInflater)
val rootView = binding.root
1. 다이얼로그를 구성하는 레이아웃 XML파일 작성
2. 작성한 XML파일을 LayoutInflater로 초기화해 다이얼로그에 적용
val dialogBinding = DialogInputBinding.inflate(layoutInflater)
AfterDialog.Builder(this).run {
setTitle("Input")
setView(dialogBinding.root)
setPositiveButton("닫기", null)
show()
}
val notification: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val ringtone = RingtoneManager.getRingtone(applicationContext, notification)
ringtone.play()
val player: MediaPlayer = MediaPlayer.create(this, R.raw.whoosh)
player.start()
<uses-permission android:name="android.permission.VIBRATE" />
val vibrator = getSystemService(VIBRATE_SERVICE) as Vibrator
진동 시간과 패턴 지정 함수 → API 레벨 1부터 제공하는 함수
26버전(Android 8)에서 deprecated 되었지만, 이전 버전 기기 사용자를 위해 계속 사용해야 함
진동의 세기까지 지정 가능한 함수 → API 레벨 26부터 제공하는 함수
API 레벨 26부터는 진동 정보를 VibrationEffect 객체로 지정할 수 있는 함수 제공
👉 기기가 정의한 기본 세기로 진동 사용하려면?
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)
}
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)
}
//알림 빌더
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val builder: NotificationCompat.Builder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
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 = NotificationCompat.Builder(this)
}
builder.setSmallIcon(android.R.drawable.ic_notification_overlay) //스몰 아이콘
builder.setWhen(System.currentTimeMillis()) //알림 시각
builder.setContentTitle("Content Title") //제목
builder.setContentText("Content Message") //내용
manager.notify(11, builder.build())
manager.cancel(11)
builder.setAutoCancel(false) // 알림 터치 시 이벤트는 발생하지만 알림이 사라지지는 않음
builder.setOngoing(true) // 스와이프해도 알림이 사라지지 않음
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!
val intent = Intent(this, SubActivity::class.java)
val pendingIntent =
PendingIntent.getActivity(this, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT)
builder.setContentIntent(pendingIntent)
val actionIntent = Intent(this, SubActivity::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()
)
// 원격 입력
val KEY_TEXT_REPLY = "key_text_reply"
var replyLabel: String = "답장"
var remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run {
setLabel(replyLabel)
build()
}
// 인텐트 준비
val replyIntent = Intent(this, SubActivity::class.java)
val replyPendingIntent = PendingIntent.getBroadcast(
this, 30, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
// 원격 입력 액션 등록
builder.addAction(
NotificationCompat.Action.Builder(
android.R.drawable.ic_menu_send,
"답장",
replyPendingIntent
).addRemoteInput(remoteInput).build()
)
// 브로드캐스트 리시버로부터 사용자가 입력한 글 받아오기
val replyText = RemoteInput.getResultsFromIntent(intent)?.getCharSequence("key_text_reply").toString()
// 알림 갱신
manager.notify(11, builder.build())
builder.setProgress(100, 0, false)
manager.notify(11, builder.build())
// 10초 동안 프로그래스 바 진행값 증가
thread {
for (i in 1..100) {
builder.setProgress(100, i, false)
manager.notify(11, builder.build())
SystemClock.sleep(100)
}
}
val bigPicture = BitmapFactory.decodeResource(resources, R.drawable.test)
val bigStyle = NotificationCompat.BigPictureStyle()
bigStyle.bigPicture(bigPicture)
builder.setStyle(bigStyle)
val bigTextStyle = NotificationCompat.BigTextStyle()
bigTextStyle.bigText(resources.getString(R.string.long_text))
builder.setStyle(bigTextStyle)
val style = NotificationCompat.InboxStyle()
style.addLine("first")
style.addLine("second")
style.addLine("third")
style.addLine("fourth")
builder.setStyle(style)
// Person 객체 생성
val sender1: Person = Person.Builder()
.setName("kkang")
.setIcon(IconCompat.createWithResource(this, R.drawable.person1))
.build()
val sender2: Person = Person.Builder()
.setName("choi")
.setIcon(IconCompat.createWithResource(this, R.drawable.person2))
.build()
// 메시지 객체 생성
val message1 = NotificationCompat.MessagingStyle.Message(
"hello",
System.currentTimeMillis(),
sender1
)
val message2 = NotificationCompat.MessagingStyle.Message(
"world",
System.currentTimeMillis(),
sender2
)
// MessageStyle에 메시지 객체 대입
val messageStyle = NotificationCompat.MessagingStyle(sender1)
.addMessage(message1)
.addMessage(message2)
builder.setStyle(messageStyle)