[Andorid] 산책 앱 개발 일지 1 : 포그라운드 서비스 사용하기

클로이·2021년 12월 15일
1

왜 포그라운드 서비스를 쓰나요?

  • Service는 보이는 화면 없이 백그라운드에서 오래 실행되는 작업을 수행해요.
  • ServiceForeground 유형은 알림이 필수라 백그라운드에서 사용자의 위치를 추적하고 있다는 것을 사용자에게 알릴 수 있어요.

AndroidManifest.xml

  • android:foregroundServiceType="location"은 포그라운드 서비스임을 지정하고 앱이 기기의 현재 위치를 가져올 수 있다는 것을 의미해요.
<manifest>
	//...
	<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    
	<application
    		//...
		<service android:name=".services.TrackingService"
        		 android:foregroundServiceType="location">
		</service>
        //...
	</<application>
</manifest>

서비스 시작

  • startService()로 서비스를 시작하도록 요청해요.
  • IntentstartService()에 전달하여 Service에 저장할 데이터를 전달할 수 있어요.
  • 저장된 데이터(Intent)는 onStartCommand()에 수신돼요.

    이 앱에서는 서비스의 동작들을 수신했어요.

TrackingFragment.kt

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
	super.onViewCreated(view, savedInstanceState)
    
	//...
    
	binding.btnStart.setOnClickListener {
            sendCommandToService(ACTION_START_OR_RESUME_SERVICE)
        }
        
        binding.btnStop.setOnClickListener {
            sendCommandToService(ACTION_STOP_SERVICE)
        }
        
	//...	
    
    }
        
private fun sendCommandToService(action: String) {
        Intent(requireContext(), TrackingService::class.java).also { Intent ->
            Intent.action = action
            requireContext().startService(Intent)
        }
    }

TrackingService.kt

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        intent?.let {
            when(it.action){
                ACTION_START_OR_RESUME_SERVICE -> {
                    Log.d("test","서비스 시작")
                    Log.d("test","서비스중..")
                }
               
                ACTION_STOP_SERVICE -> {
                    Log.d("test","서비스 중지")
                }
                else -> Log.d("","else")
            }
        }
        return super.onStartCommand(intent, flags, startId)
    }

포그라운드 서비스 실행

  • Foreground Service는 상태 표시줄에 대한 알림을 필수로 제공해야 해요.
  • 서비스를 중단하거나 Foreground에서 제거하지 않은 이상 알림을 해제할 수 없어요.
  • 알림의 우선순위는 PRIORITY_LOW 이상이어야 해요.

    이 앱에서는 사용자가 산책을 하고 있다는 것을 알림으로 보여줬어요.

알림 설정

알림 콘텐츠 설정

TrackingService.kt

private fun startForegroundService() {

	//...
    
	val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setAutoCancel(false)
            .setOngoing(true)
            .setSmallIcon(R.drawable.ic_paw_theme)
            .setContentTitle("어야가자")
            .setContentText("어야가자 앱에서 산책이 진행 중입니다..")
            .setContentIntent(getMainActivityPendingIntent())
            
	//...

    }

채널 만들기 및 중요도 설정

  • Android 8.0(API 수준 26)부터 알림을 채널로 분류하여 사용자가 앱의 알림을 각각 설정 할 수 있게 해요.
  • 알림의 중요도를 사용하여 알림이 사용자를 방해하는 수준을 결정해요. 중요도가 높을수록 알림이 사용자를 방해하는 수준도 높아져요.

TrackingService.kt

@RequiresApi(Build.VERSION_CODES.O)
    private fun createNotificationChannel(notificationManager : NotificationManager ) {
        val channel = NotificationChannel(
            NOTIFICATION_CHANNEL_ID,
            NOTIFICATION_CHANNEL_NAME,
            IMPORTANCE_LOW
        )
        notificationManager.createNotificationChannel(channel)
    }

알림의 탭 작업 설정

  • 알림의 탭을 클릭했을 때 일어날 작업을 설정하려면 PendingIntent 객체로 정의된 콘텐츠 Intent를 지정하여 setContentIntent()에 전달해야해요.

    이 앱에서는 탭을 클릭했을 때 TrackingFragment가 열리도록 설정했어요.

TrackingService.kt

private fun getMainActivityPendingIntent() = PendingIntent.getActivity(
        this,
        0,
        Intent(this, MainActivity::class.java).also { Intent ->
            Intent.action = ACTION_SHOW_TRACKING_FRAGMENT
        },
        FLAG_UPDATE_CURRENT
    )
<navigation ...>

    <action android:id="@+id/action_global_trackingFragment"
        app:destination="@+id/trackingFragment"
        app:launchSingleTop="true"/>

	<!--...-->
    
</navigation>
    

MainActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        
        navigateToTrackingFragmentIfNeeded(intent)
        
        //...
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        navigateToTrackingFragmentIfNeeded(intent)
    }

    private fun navigateToTrackingFragmentIfNeeded(intent: Intent?){
        val navHostFragment = supportFragmentManager.findFragmentById(R.id.navHostFragment) as NavHostFragment
        if(intent?.action == ACTION_SHOW_TRACKING_FRAGMENT) {
            navHostFragment.findNavController().navigate(R.id.action_global_trackingFragment)
        }
    }

알림 표시

  • startForeground() 함수로 알림을 생성하면서 Foreground Service를 실행해요.

TrackingService.kt

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        intent?.let {
            when(it.action){
                ACTION_START_OR_RESUME_SERVICE -> {
                    startForegroundService()  
                    Log.d("test","서비스 시작")
                    Log.d("test","서비스중..")
                }
                //...
            }
        }
        return super.onStartCommand(intent, flags, startId)
    }
    
    private fun startForegroundService() {

        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
           createNotificationChannel(notificationManager)
        }

        //...

        startForeground(NOTIFICATION_ID, notificationBuilder.build())

    }

서비스 생명주기 관리 (서비스 중단)

  • 시작된 Service는 생명 주기를 직접 관리해야 해요.
  • stopForeground(true)stopSelf()로 서비스를 중단 할 수 있어요.

이 앱에서는 사용자가 산책 끝내기 버튼을 눌렀을 때 서비스를 중단할 것이기 때문에 UI 업데이트를 위한 데이터를 관찰하여 서비스의 생명주기를 관리할거에요.

UI 업데이트를 위한 변수(isTracking) 생성

TrackingService.kt

class TrackingService: LifecycleService() {

    //...

    companion object {
        val isTracking = MutableLiveData<Boolean>()
    }
    
    private fun postInitialValues() {
        isTracking.postValue(false)
    }

    override fun onCreate() {
        super.onCreate()
        postInitialValues()
    }
    
    //...

    private fun stopService() {
        isTracking.postValue(false)
        postInitialValues()
        stopForeground(true)
        stopSelf()
    }
    
    private fun startForegroundService() {
        isTracking.postValue(true)
        
        //...
    }
    
    //...
}

TrackingFragment.kt


     private var isTracking = false
	
    	//...

     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //...

        binding.btnStart.setOnClickListener {
            sendCommandToService(ACTION_START_OR_RESUME_SERVICE)
        }

        binding.btnStop.setOnClickListener {
            sendCommandToService(ACTION_STOP_SERVICE)
        }

        subscribeToObservers()

    }
    

    private fun subscribeToObservers() {
        TrackingService.isTracking.observe(viewLifecycleOwner, { isTracking ->
            TrackingService.isTracking.observe(viewLifecycleOwner,{
            updateUI(it)
        })
    }
    

    private fun updateUI(isTracking: Boolean) {
        this.isTracking = isTracking
        if(isTracking) {
            binding.btnStart.visibility = View.GONE
            binding.clWalkLayout.visibility = View.VISIBLE

        } else {
            binding.btnStart.visibility = View.VISIBLE
            binding.clWalkLayout.visibility = View.GONE
        }
    }
    
    //...

출처 & 참고

서비스 개요
서비스 매니페스트 파일
알림 만들기
Tracking Service Basic Setup - MVVM Running Tracker App - Part 11
Making Our Service a Foreground Service - MVVM Running Tracker App - Part 12

0개의 댓글