(Andorid | Kotlin) 걷기 앱 개발일지 1: 포그라운드 서비스 활용하기

이 게시물은 2020년 5월에서 8월 사이에 생성된 Android 네이티브 앱을 요약한 것입니다.


포그라운드 서비스를 사용하는 이유는 무엇입니까?

  • 서비스는 보이는 화면 없이 백그라운드에서 장기 실행 작업을 수행합니다.
  • 서비스의 Foreground 유형은 알림에 중요하기 때문에 백그라운드에서 사용자의 위치를 ​​추적하고 있음을 사용자에게 알릴 수 있습니다.

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()로 서비스 시작을 요청합니다.
  • Intent를 startService()에 전달하여 서비스에 저장할 데이터를 전달할 수 있습니다.
  • 저장된 데이터(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)
    }

포그라운드 서비스 실행(알림 설정)

  • 포그라운드 서비스는 상태 표시줄에 알림을 제공해야 합니다.
  • 서비스를 중지하거나 포그라운드에서 제거하지 않는 한 알림을 해제할 수 없습니다.
  • 알림의 우선순위는 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 개체로 정의된 콘텐츠 의도를 지정하고 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
    )

nav_graph.xml

<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() 함수로 알림을 생성하는 동안 포그라운드 서비스를 실행합니다.

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())

    }

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

  • 시작된 서비스는 수명 주기를 직접 관리해야 합니다.
  • stopForeground(true) 및 stopSelf()를 사용하여 서비스를 중지할 수 있습니다.

이 앱에서는 사용자가 걷기 종료 버튼을 누르면 서비스를 중지할 것이므로 UI ​​업데이트 데이터를 모니터링하여 서비스 수명 주기를 관리할 것입니다.

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

TrackingService.kt

    //...

    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)
        
        //...
    }
    
    //...
}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
        }
    }
    
    //...