이 게시물은 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
}
}
//...