이것이 안드로이드다 with 코틀린(고돈호 지음) 으로 공부한 내용을 정리한 글입니다.
Service는 안드로이드의 4가지 컴포넌트 중 하나로 백그라운드 작업을 위한 컴포넌트입니다. 다만 서비스는 워커 쓰레드가 아닌 메인 쓰레드에서 실행되며 따라서 워커 쓰레드를 통해 백그라운드 작업을 처리하는 것과는 다른 동작 방식을 가집니다.
서비스는 Started Service와 Bound Service 두 가지 형태로 실행됩니다.
Started Service는 서비스를 호출한 액티비티와는 관계 없이 독립적으로 동작하는 서비스로 startService()
메서드로 호출합니다. 독립적으로 동작하기 때문에 액티비티의 종료에 영향을 받지 않으며 Singleton
으로 동작합니다.
Singleton
클래스의 인스턴스를 오직 하나만 생성해서 유지하는 디자인 패턴
Bound Service는 서비스가 액티비티에 바인드되며 액티비티와 값을 주고받을 필요가 있을 때 사용합니다. 또 Bound Service는 bindService()
메서드로 호출하며 하나의 Bound Service를 여러 액티비티가 사용할 수 있습니다. 다만 바인드된 액티비티가 모두 종료되면 서비스도 종료된다는 제약이 존재합니다.
Service
클래스를 상속받아 서비스 클래스를 생성합니다.
// MyService.kt
class MyService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Started Service에서 서비스 시작시 호출
}
override fun onBind(intent: Intent): IBinder {
// Bound Service에서 서비스 연결시 호출
}
}
서비스는 AndroidManifest.xml
에 등록되야 사용할 수 있습니다.
<!-- AndroidManifest.xml-->
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"/>
fun serviceStart(view: View) {
val intent = Intent(this, MyService::class.java)
intent.action = MyServcie.ACTION_START
startServcie(intent)
}
fun serviceStop(view: View) {
val intent = Intent(this, MyService::class.java)
stopService(intent)
}
리플렉션(
::
)
원하는 코드의 런타임 때의 위치(런타임 참조)를 찾기위해 사용하는 기능. 예를 들어MyService::class.java
라고 작성하면MyServcie
클래스의 런타임 참조를 얻는 것..java
가 붙는 이유는 코틀린 클래스(KClass)를 자바 클래스(Class)로 변환해주기 위함
Bound Service를 액티비티와 연결하기 위해선 Binder
와 ServiceConnection
을 생성해야 합니다.
// MyService.kt
inner class MyBinder : Binder() {
fun getService(): MyService {
// 액티비티와 서비스가 연결되면 이 메서드를 통해 서비스에 접근
return this@MyService
}
}
val binder = MyBinder()
override fun onBind(intent: Intent): IBinder {
return binder
}
this@label
내부 범위에서 외부 범위에 접근하기 위한 한정자
onServiceConnected()
메서드는 서비스가 연결되면 호출되고 onServiceDisconnected()
메서드는 서비스가 비정상적으로 종료되었을 때 호출됩니다. 따라서 isService
변수를 두고 현재 서비스가 연결되어 있는지를 확인하는 로직이 필요합니다.
var myService: MyService? = null
var isService = false
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as MyService.MyBinder
myService = binder.getService()
isService = true
}
override fun onServiceDisconnected(name: ComponentName) {
isService = false
}
}
fun serviceBind(view: View) {
val intent = Intent(this, MyService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
fun serviceUnbind(view: View) {
if (isService) {
unbindService(connection)
isService = false
}
}
Bound Service는 Started Service와 달리 액티비티에서 서비스의 메서드를 직접 호출하여 사용할 수 있습니다.
// MyService.kt
fun serviceMessage() : String {
return "Hello"
}
// MainActivity.kt
myService?.serviceMessage()
기본적으로 서비스는 모두 백그라운드 서비스되며 포어그라운드로 사용하기 위해선 시스템에 알려줘야 합니다.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
startForegrond()
메서드를 통해 포어그라운드 서비스임을 시스템에 알림서비스를 포어그라운드로 사용하기 위해선 알림바에 알림을 함께 띄워야합니다.
class Foreground : Service() {
val CANNEL_ID = "ForegroundChannel" // 알림에 사용될 채널
// ...
fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 알림은 채널 단위로 동작
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
}
val manager = getSystemService(
NotificationManager::class.java
)
manager.createNotificationChannel(serviceChannel)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notification:Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTile("Foreground Service)
.setSmallIcon(R.mipmap.ic_launcher_round)
.build()
startForeground(1, notification)
return super.onStartCommand(intent, flags, startId)
}
}
포어그라운드 서비스는 ContextCompat.startForegroundService()
를 사용해서 실행해야 합니다.
val intent = Intent(this, Foreground::class.java)
ContextCompat.startForegroundService(this, intent)
Content Provider는 안드로이드의 4가지 컴포넌트 중 하나로 앱의 데이터를 다른 앱에 제공하는 컴포넌트입니다. 하지만 실제로 콘텐트 프로바이더를 구현하는 일은 거의 없고, 주로 다른 앱이나 안드로이드 OS에 이미 구현되어 있는 콘텐트 프로바이드로부터 데이터를 받습니다. 이때 사용되는 도구가 Content Resolver입니다.
Content Resolver로 MediaStore
에서 미디어 정보를 읽어오는 과정은 다음과 같습니다.
MediaStore
상의 주소 정의val listUrl = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val proj = arrayOf(MediaStore.Audio.Meida._ID, MediaStore.Audio.Media.TITLE)
data class Music (val id: String, val title: String)
query()
메서드로 쿼리를 실행val cursor = contentResolver.query(listUrl, proj, 검색 조건, 조건의 값, 정렬 순서)