본문 바로가기
Android/Kotlin App

[Kotlin App] 서비스와 브로드캐스트 리시버

by 태옹 2021. 6. 7.

1. 서비스

2. 브로드캐스트 리시버

3. 콘텐트 프로바이더

 

 

서비스

  • 일반적으로 화면없이 동작하는 프로그램을 뜻함
  • 데몬(daemon), 백그라운드 프로세스라고도 함
  • 서비스는 백그라운드에서 실행되므로 화면과 상관없이 계속 동작함

 

화면이 종료되어도 계속되는 음악 서비스 만들기

  • 버튼을 클릭하면 음악이 시작되고, 화면이 종료되어도 음악이 계속 흐르는 서비스
  • 또한 로그캣을 활용하여 메소드가 실행되는 순서를 확인해봄

 

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnStart"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="음악서비스 시작" />

    <Button
        android:id="@+id/btnStop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="음악서비스 중지" />
</LinearLayout>

 

Service클래스를 상속받아 구현한 MusicService클래스 생성 (Kotlin클래스)

package com.cookandroid.kotlinapp0607

import android.content.Intent
import android.media.MediaPlayer
import android.os.IBinder


class MusicService : android.app.Service(){ //Service클래스를 상속받음
    lateinit var mp:MediaPlayer //mp3객체를 생성

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        /*로그를 기록하는 코드*/
        android.util.Log.i("서비스 테스트", "onStartCommand()")
        mp = MediaPlayer.create(this, R.raw.song1)  
        mp.isLooping = true
        mp.start()  //음악 시작
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onCreate() {
        /*로그를 기록하는 코드*/
        android.util.Log.i("서비스 테스트", "onCreate()")
        super.onCreate()
    }

    override fun onDestroy() {
        /*로그를 기록하는 코드*/
        android.util.Log.i("서비스 테스트", "onDestory()")
        mp.stop()   //음악 중지시키기
        super.onDestroy()
    }
}

 

MainActivity.kt

package com.cookandroid.kotlinapp0607

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    lateinit var soundIntent:Intent

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //인텐트 변수를 생성하면서 MusicService클래스를 생성자에 넘김
        soundIntent = Intent(this, MusicService::class.java)

        btnStart.setOnClickListener {
            startService(soundIntent)
            android.util.Log.i("서비스 테스트","startService()")
        }

        btnStart.setOnClickListener {
            stopService(soundIntent)
            android.util.Log.i("서비스 테스트","stopService()")
        }
    }
}

 

서비스를 사용할 수 있도록 Manifest.xml에서 </activity> 아래에 추가해준다.

 <service android:name=".MusicService" />

 

실행결과 확인하기

logcat을 통해 로그 기록을 확인할 수 있다. [음악서비스 시작]버튼을 누른 경우, startService() -> onCreate() -> onStartCommand() 순서로 메소드가 실행됨을 확인할 수 있다.

 

[음악서비스 중지]버튼을 누른 경우, stopService() -> onDestroy() 순서로 실행이 된다.

 

실행하는 동안 앱을 완전히 종료하지 않고 홈버튼을 눌러 화면만 나가게 되면 음악은 계속 들린다.

=> '서비스'의 기능!

(그러나 다른 응용프로그램을 종료하지 않고 덮어쓰기식으로 재생이 된다. 😐)

 

 


 

브로드캐스트 리시버

  • 안드로이드는 문자 메시지 도착, 배터리 방전, SD카드 탈부착, 네트워크 환경 변화 등이 발생하면 방송(브로드캐스트) 신호를 보내는데, 이런 신호를 받아서 처리하는 것이 브로드캐스트 리시버임
  • 브로드캐스트 리시버의 대표적인 응용은 배터리 상태 확인
  • 배터리 상태와 관련된 액션 ▼
액션 설명
ACTION_BATTERY_CHANGED 배터리 상태가 변경될 때
ACTION_BATTERY_LOW 배터리가 거의 방전되었을 때
ACTION_BATTERY_OKAY 배터리가 방전 상태에서 정상 수준으로 돌아왔을 때

 

 

배터리 상태를 표시하는 앱 만들기

  • 배터리 상태가 변할 때마다 충전 상태 이미지와 배터리 상태를 출력
  • 명령 프롬프트에서 AVD의 배터리 상태를 강제로 변경

 

xml파일

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity2">

    <ImageView
        android:id="@+id/imgBattery"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/battery_0" />

    <EditText
        android:id="@+id/edtBattery"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:enabled="false"
        android:inputType="textPersonName"
        android:text="Name" />
</LinearLayout>

 

MainActivity.kt

package com.cookandroid.kotlinapp0607

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main2.*

class MainActivity2 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)

        title = "브로드캐스트 리시버"
    }

	//브로드캐스트 리시버의 정보는 intent에 포함되어있음
    var br:BroadcastReceiver = object : BroadcastReceiver(){
        override fun onReceive(context: Context?, intent: Intent?) {
            var action= intent?.action
            
            if(action==Intent.ACTION_BATTERY_CHANGED){
                /*인텐트 엑스트라에서 배터리의 잔량을 추출*/
                var remain = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)

                /*배터리 잔량 표시*/
                edtBattery.setText("현재 충전량 : $remain\n")

                /*잔량에 따라 배터리 이미지 변경*/
                if (remain != null) {
                    if(remain >= 90)
                        imgBattery.setImageResource(R.drawable.battery_100)
                    else if(remain >= 70)
                        imgBattery.setImageResource(R.drawable.battery_80)
                    else if(remain >= 50)
                        imgBattery.setImageResource(R.drawable.battery_60)
                    else if(remain >= 30)
                        imgBattery.setImageResource(R.drawable.battery_20)
                    else
                        imgBattery.setImageResource(R.drawable.battery_0)
                }

                /*인텐트의 엑스트라에서 배터리의 전원 연결 상태를 추출*/
                var plug = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED,0)

                /*배터리 전원 연결 상태 표시*/
                when(plug){
                    0->edtBattery.append("전원 연결 : 안됨")
                    BatteryManager.BATTERY_PLUGGED_AC -> edtBattery.append("전원 연결 : 어댑터 연결됨")
                    BatteryManager.BATTERY_PLUGGED_USB -> edtBattery.append("전원 연결 : USB 연결됨")
                }
            }
        }

    }

    override fun onResume() {
        super.onResume()
        /*인텐트 필터를 생성*/
        var iFilter = IntentFilter()
        
        /*ACTION_BATTERY_CHANGED액션을 추가*/
        iFilter.addAction(Intent.ACTION_BATTERY_CHANGED)
        
        /*br에 등록*/
        registerReceiver(br, iFilter)
    }
    override fun onPause() {
        super.onPause()
        
        /*등록된 br을 해제*/
        unregisterReceiver(br)
    }

}

 

실행결과 확인하기

 

잔여 배터리에 따라 상태가 변하는 것을 확인하기 위해 AVD의 설정에서 배터리와 전원 연결 상태를 변경할 수 있다.

 

 


 

 

콘텐트 프로바이더

  • 안드로이드는 보안상 앱에서 사용하는 데이터를 외부에서 접근할 수 없음 (그냥은 안됨)
  • 파일이나 데이터베이스를 외부 앱에서 사용하도록 하려면 콘텐트 프로바이더를 만들어서 외부로 제공

 

URI

  • 콘텐트 프로바이더에서 제공하는 데이터에 접근하기 위한 주소
  • "content://패키지명/경로/아이디" 형식으로 지정

 

안드로이드에서 통화 기록을 가져오는 예제

  • AVD에서 통화 버튼을 눌러서 통화 기록을 몇 건 남겨놓음
  • 통화 기록에 접근하기 위해 AndroidManifest.xml의 <application 위에 다음 코드를 추가하여 접근 권한을 줌
<uses-permission android:name="android.permission.READ_CALL_LOG"/>

 

xml파일

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity3">


    <Button
        android:id="@+id/btnCall"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="통화 기록 가져오기" />

    <EditText
        android:id="@+id/editCall"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="textMultiLine"
        android:text="Name" />
</LinearLayout>

 

 

 

MainActivity.kt

package com.cookandroid.kotlinapp0607

import android.app.Activity
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.CallLog
import android.telecom.Call
import androidx.core.app.ActivityCompat
import kotlinx.android.synthetic.main.activity_main3.*
import java.text.SimpleDateFormat
import java.util.*
import java.util.jar.Manifest

class MainActivity3 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main3)

        /*접근 권한 허용*/
        ActivityCompat.requestPermissions(
            this,
            arrayOf(android.Manifest.permission.READ_CALL_LOG),
            Context.MODE_PRIVATE
        )

        /*통화기록을 띄울 editText 세팅*/
        btnCall.setOnClickListener {
            editCall.setText(findCallHistory()) //findCallHistory()함수 호출
        }
    }

    fun findCallHistory(): String {
        var callSet = arrayOf(  //날짜, 착/발신 정보, 전화번호, 전화시간(초) 정보를 가져옴
            CallLog.Calls.DATE, CallLog.Calls.TYPE,
            CallLog.Calls.NUMBER, CallLog.Calls.DURATION
        )

        /*.query()는 sql문 쿼리와 유사하게 작동함*/
        /*SELECT callSet
        * FROM CallLog.Calls.CONTENT_URI
        * WHERE null,null
        * ORDER BY null*/
        var c = contentResolver.query(CallLog.Calls.CONTENT_URI, callSet, null, null, null)

        if (c != null) {
            if (c.count == 0)
                return "통화기록 없음"
        }

        var callBuff = StringBuffer()
        callBuff.append("\n날짜  :  구분  :  전화번호  :  통화시간\n\n")
        c!!.moveToFirst()   //커서가 현재 맨 뒤에 있음 -> 처음으로 옮겨옴
        do {
            /*날짜(0) - 포멧처리해서 버퍼에 넣기*/
            var callDate = c!!.getLong(0)
            var datePattern = SimpleDateFormat("yyyy-MM-dd")
            var date_str = datePattern.format(Date(callDate))
            callBuff.append("$date_str:")

            /*타입(1) - 착신인지 발신인지 구분해서 버퍼에 넣기*/
            if (c.getInt(1) == CallLog.Calls.INCOMING_TYPE)
                callBuff.append("착신 : ")
            else
                callBuff.append("발신 : ")

            /*번호(2)*/
            callBuff.append(c!!.getString(2) + ":")

            /*통화시간(3)*/
            callBuff.append(c!!.getString(3) + "초\n")
        } while (c!!.moveToNext())  //커서를 한 칸씩 옮겨주는 것을 반복함
        c.close()
        return callBuff.toString()
    }

}

댓글