Kotlin

3. Android - Location post service (feat. Alarm)

kakaroo 2022. 3. 1. 20:37
반응형

article logo

 

설정한 시간을 주기로 현재 위치를 서버로 post하는 어플리케이션을 생성하겠습니다.

 

아래 순서대로 해 보겠습니다.

- 저장 주기 설정

- 알람 등록

- 현재 위치와 현재 시간 가져오기

- 저장 주기마다 값을 서버에 POST 하기

 

기본적인 UI는 안드로이드의 preference UI를 이용해서

서비스의 시작 시간과 제어 시간, 중심 위치등을 설정할 수 있게 합니다.

메인 화면

 

 

저장 주기 설정

 

TimePickerPreference를 설정합니다.

        <com.kakaroo.footprinterservice.TimepickerPreference
            app:key="timePref_Key"
            app:title="@string/service_start_time"
            app:summary="서비스가 시작되는 시간을 설정해 주세요"
            app:iconSpaceReserved="false"
            app:defaultValue="90"/>

        <ListPreference
            app:key="timeIntervalPref_key"
            app:defaultValue="30"
            app:entries="@array/time_entries"
            app:entryValues="@array/time_values"
            app:title="@string/service_time_gap"
            app:iconSpaceReserved="false"
            app:useSimpleSummaryProvider="true" />

 

TimePickerPreferenceDialog를 생성합니다.

class TimePickerPreferenceDialog : PreferenceDialogFragmentCompat() {

    lateinit var mTimePicker: TimePicker

    override fun onCreateDialogView(context: Context?): View {
        mTimePicker = TimePicker(context)
        return mTimePicker
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onBindDialogView(view: View?) {
        super.onBindDialogView(view)

        val minutesAfterMidnight = (preference as TimepickerPreference)
            .getPersistedMinutesFromMidnight()
        mTimePicker.setIs24HourView(false)
        mTimePicker.hour = minutesAfterMidnight / 60
        mTimePicker.minute = minutesAfterMidnight % 60
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDialogClosed(positiveResult: Boolean) {
        // Save settings
        if (positiveResult) {
            val hours: Int
            val minutes: Int
            // Get the current values from the TimePicker
            if (Build.VERSION.SDK_INT >= 23) {
                hours = mTimePicker.hour
                minutes = mTimePicker.minute
            } else {
                @Suppress("DEPRECATION")
                hours = mTimePicker.currentHour
                @Suppress("DEPRECATION")
                minutes = mTimePicker.currentMinute
            }

            // Generate value to save
            val minutesAfterMidnight = hours * 60 + minutes

            // Save the value
            preference.apply {
                // This allows the client to ignore the user value.
                if (callChangeListener(minutesAfterMidnight)) {
                    // Save the value
                    (preference as TimepickerPreference).persistMinutesFromMidnight(minutesAfterMidnight)
                    preference.summary = (preference as TimepickerPreference).minutesFromMidnightToHourlyTime(minutesAfterMidnight)
                }
            }
        }
        /*if(positiveResult) {
            val minutesAfterMidnight = (timepicker.hour * 60) + timepicker.minute
            (preference as TimepickerPreference).persistMinutesFromMidnight(minutesAfterMidnight)
            preference.summary = "123"//minutesFromMidnightToHourlyTime(minutesAfterMidnight)
        }*/
    }

    companion object {
        fun newInstance(key: String): TimePickerPreferenceDialog {
            val fragment = TimePickerPreferenceDialog()
            val bundle = Bundle(1)
            bundle.putString(ARG_KEY, key)
            fragment.arguments = bundle

            return fragment
        }
    }
}

 

 

알람 서비스 등록

 

alarm 을 시작/종료 하는 함수를 구현합니다.

해제시간 설정시 서비스 시간을 재설정하기 위해 intent action은 repeat mode와 restart mode로 구분하였습니다.

 

alarm repetition으로 테스트 한 결과, 정확한 시간에 알람이 울리지 않아,

알람이 울리고 나면 다시 알람을 재설정 하는 로직으로 변경했습니다.

@RequiresApi(Build.VERSION_CODES.M)
    fun startAlarmService(action: Int) {

        Log.i(Common.MY_TAG, "startAlarmService:: action:$action" )

        val bStartedService = mPref.getBoolean("service_start_key", false)
        Log.i(Common.MY_TAG, "startAlarmService:: bStartedService:${bStartedService}")

        //기존 알람은 삭제
        cancelAlarm()

        val alarmManager = this.mContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val alarmIntent = Intent(this.mContext, MyReceiver::class.java)

        alarmIntent.action = Common.MY_ACTION_FOOT_PRINTER
        /*
        if(action == Common.ALARM_REPEAT_MODE || action == Common.ALARM_RESTART_REPEAT_MODE || action == Common.ALARM_DAY_CHANGE_MODE) {
            alarmIntent.action = Common.MY_ACTION_FOOT_PRINTER
        } else if(action == Common.ALARM_RESTART_MODE) {
            alarmIntent.action = Common.MY_ACTION_FOOT_PRINTER_RESTART
        }*/

        var flags = PendingIntent.FLAG_UPDATE_CURRENT
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT//FLAG_UPDATE_CURRENT
        }
        val pendingIntent = PendingIntent.getBroadcast(this.mContext, 0, alarmIntent, flags)//PendingIntent.FLAG_CANCEL_CURRENT)
        var triggerTimeMillis: Long = 0

        if(action == Common.ALARM_REPEAT_MODE) {    //start service
            val calendar = MyUtility().getCalendar(mPref.getInt("timePref_Key", 0), true)

            val strInterval : String? = mPref.getString("timeIntervalPref_key", "0")
            val minuteInterval : Int = strInterval?.toInt() ?: 0

            try {
                val selections = mPref.getStringSet("timeExDayPref_key", null) as HashSet<String>
                if(selections != null) {
                    val selected: List<String> = selections.toList()

                    //위에서 설정된 시간이 제외 요일에 포함되어 있는 경우 제외되지 않은 다음 날짜로 설정한다.
                    if(selected.isNotEmpty()) {
                        var dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)   //일:1, 월:2 .. 토:7
                        selected.forEach { item ->
                            if(item.toInt() == dayOfWeek) {
                                Log.w(Common.MY_TAG, "제외 요일이므로 다음날짜에 시작됩니다.")
                                calendar.add(Calendar.DATE, 1)
                                dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
                            }
                        }
                        dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
                        if(dayOfWeek == 1) { //위에서 계산된 요일이 일요일이면 한 번 더 체크한다.
                            selected.forEach { item ->
                                Log.w(Common.MY_TAG, "2nd item: ${item.toInt()}, dayOfWeek: ${dayOfWeek}")
                                if(item.toInt() == dayOfWeek) {
                                    Log.w(Common.MY_TAG, "제외 요일이므로 다음날짜에 시작됩니다.")
                                    calendar.add(Calendar.DATE, 1)
                                    dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
                                }
                            }
                        }
                    }
                }
            } catch (e: NullPointerException) {
                Log.w(Common.MY_TAG, "$e :: intentional NullPointerException when getting timeExDayPref_key")
            }

            val dateText = MyUtility().getDateTimeFromCalendar(calendar)
            Toast.makeText(this.mContext, dateText + "부터, "+minuteInterval+"분 간격으로 " +"알림이 실행됩니다!", Toast.LENGTH_LONG).show()
            Log.d(Common.MY_TAG, dateText + "부터, "+minuteInterval+"분 간격으로 " +"알림이 실행됩니다!")

            //ELAPSED_REALTIME_WAKEUP
            triggerTimeMillis = if(Common.ALARM_WAKEUP_TYPE_ELAPSED == 1) {
                val nowCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
                val diffTimeMillis = calendar.timeInMillis - nowCalendar.timeInMillis
                diffTimeMillis
            } else {
                calendar.timeInMillis
            }
        } else if(action == Common.ALARM_RESTART_REPEAT_MODE) {
            val calendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
            calendar[Calendar.SECOND] = 0
            val strInterval : String? = mPref.getString("timeIntervalPref_key", "0")
            val minuteInterval : Int = strInterval?.toInt() ?: 0
            val millisInterval = MyUtility().getMilliTime(minuteInterval)

            val dateText = MyUtility().getDateTimeFromCalendar(calendar)
            //Toast.makeText(this.mContext, dateText + "부터, "+minuteInterval+"분 간격으로 " +"알림이 실행됩니다!", Toast.LENGTH_LONG).show()
            Log.d(Common.MY_TAG, dateText + "부터, "+minuteInterval+"분 간격으로 알람이 울립니다!")

            //ELAPSED_REALTIME_WAKEUP
            if(Common.ALARM_WAKEUP_TYPE_ELAPSED == 1) {
                val nowCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
                nowCalendar[Calendar.SECOND] = 0
                val diffMillis = abs(System.currentTimeMillis() - nowCalendar.timeInMillis)
                Log.d(Common.MY_TAG, "diffMillis: $diffMillis")
                //0초 부터 알람을 울리기 위해
                triggerTimeMillis = millisInterval - diffMillis
            } else {
                triggerTimeMillis = calendar.timeInMillis + millisInterval
            }

        } else if(action == Common.RESTART_SERVICE_MODE) {
            Log.w(Common.MY_TAG, "RESTART_SERVICE_MODE!!!")

            var bTimeIsSet = false
            var calendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
            calendar[Calendar.SECOND] = 0

            val strInterval : String? = mPref.getString("timeIntervalPref_key", "0")
            val minuteInterval : Int = strInterval?.toInt() ?: 0

            val prevTriggerCalendar = getTriggerPrefTime()

            Log.d(Common.MY_TAG, "현재 시간은 ${MyUtility().getDateTimeFromCalendar(calendar)} 입니다")
            Log.d(Common.MY_TAG, "트리거시간은 ${MyUtility().getDateTimeFromCalendar(prevTriggerCalendar)} 입니다")

            if(prevTriggerCalendar.before(calendar) || prevTriggerCalendar == calendar) {   //prevTriggerCalendar < calendar :: Trigger할 시간이 지났으면 바로 알람을 띄운다.
                bTimeIsSet = true
                Log.w(Common.MY_TAG, "RESTART_SERVICE_MODE:Case-1")
            } else if(mPref.getBoolean(Common.HAS_BEEN_POST_THIS_SERVICE_KEY, false)) { // 이번 서비스에 post 한 적이 있다면
                val postYear = mPref.getInt(Common.POST_CALENDAR_YEAR_KEY, 2000)
                val postDayOfYear = mPref.getInt(Common.POST_CALENDAR_DAY_OF_YEAR_KEY, 1)
                val postHour = mPref.getInt(Common.POST_CALENDAR_HOUR_KEY, 9)
                val postMinute = mPref.getInt(Common.POST_CALENDAR_MINUTE_KEY, 10)

                val postCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))

                postCalendar[Calendar.YEAR] = postYear
                postCalendar[Calendar.DAY_OF_YEAR] = postDayOfYear
                postCalendar[Calendar.HOUR_OF_DAY] = postHour
                postCalendar[Calendar.MINUTE] = postMinute + minuteInterval
                postCalendar[Calendar.SECOND] = 0

                Log.d(Common.MY_TAG, "포스트예상시간은 ${MyUtility().getDateTimeFromCalendar(postCalendar)} 입니다")

                if(postCalendar.before(calendar) || postCalendar == calendar) {   //Post할 시간이 현재 시간을 지났으면 바로 알람을 띄운다.
                    bTimeIsSet = true
                    Log.w(Common.MY_TAG, "RESTART_SERVICE_MODE:Case-2")
                }
            }

            if(bTimeIsSet) {
                //전송할 시간이 exception시간에 속하면 exception 종료시간으로 알람을 설정한다.
                if(MyReceiver().timeIsIncludedExceptionTime(calendar, mPref)) {
                    val minute = mPref.getInt("timeExPref_key", 0)
                    val strInterval : String? = mPref.getString("timeExGapPref_key", "0")
                    val minuteInterval : Int = strInterval?.toInt() ?: 0
                    calendar = MyUtility().getCalendar(minute + minuteInterval, true)
                    Log.w(Common.MY_TAG, "RESTART_SERVICE_MODE: Exception 시간으로 전송시간을 재설정합니다.")
                }
            }
            
            if(bTimeIsSet) {
                //최종적으로 설정된 시간이 맞는지 체크
                val nowCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
                nowCalendar[Calendar.SECOND] = 0

                if(calendar.before(nowCalendar) || calendar == nowCalendar) {
                    Log.e(Common.MY_TAG, "Error:: Time is already passed, will be changed to current time")
                    calendar = nowCalendar
                    //calendar.add(Calendar.DATE, 1);
                }

                val dateText = MyUtility().getDateTimeFromCalendar(calendar)
                //Toast.makeText(this.mContext, dateText + "부터, "+minuteInterval+"분 간격으로 " +"알림이 실행됩니다!", Toast.LENGTH_LONG).show()
                Log.d(Common.MY_TAG, dateText + "부터, "+minuteInterval+"분 간격으로 알람이 울립니다!")

                //ELAPSED_REALTIME_WAKEUP
                if(Common.ALARM_WAKEUP_TYPE_ELAPSED == 1) {
                    val millisInterval = MyUtility().getMilliTime(minuteInterval)
                    val nowCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
                    nowCalendar[Calendar.SECOND] = 0
                    val diffMillis = abs(System.currentTimeMillis()-nowCalendar.timeInMillis)
                    Log.d(Common.MY_TAG, "diffMillis: $diffMillis")
                    //0초 부터 알람을 울리기 위해
                    triggerTimeMillis = millisInterval - diffMillis
                } else {
                    triggerTimeMillis = calendar.timeInMillis
                }
            }

        } else if(action == Common.ALARM_RESTART_MODE) {
            val minute = mPref.getInt("timeExPref_key", 0)
            val strInterval : String? = mPref.getString("timeExGapPref_key", "0")
            val minuteInterval : Int = strInterval?.toInt() ?: 0
            val calendar = MyUtility().getCalendar(minute + minuteInterval, true)

            val dateText = MyUtility().getDateTimeFromCalendar(calendar)
            //Toast.makeText(this.mContext, dateText + "에 알림이 다시 실행될 예정입니다!", Toast.LENGTH_LONG).show()
            Log.w(Common.MY_TAG, "현재시간이 제외시간이기 때문에 ${dateText}에 알림이 다시 실행될 예정입니다!")

            //ELAPSED_REALTIME_WAKEUP
            triggerTimeMillis = if(Common.ALARM_WAKEUP_TYPE_ELAPSED == 1) {
                val nowCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
                val diffTimeMillis = calendar.timeInMillis - nowCalendar.timeInMillis
                diffTimeMillis//-30000 //30000 : 10초전에 알람을 울려 restart를 한다.*/
            } else {
                calendar.timeInMillis
            }
        } else if(action == Common.ALARM_DAY_CHANGE_MODE) {
            val calendar = MyUtility().getCalendar(0, true)   //00시 00분
            val dateText = MyUtility().getDateTimeFromCalendar(calendar)
            //Toast.makeText(this.mContext, dateText + "에 알림이 다시 실행될 예정입니다!", Toast.LENGTH_LONG).show()
            Log.w(Common.MY_TAG, dateText + "에 알림이 다시 실행될 예정입니다!")

            //ELAPSED_REALTIME_WAKEUP
            triggerTimeMillis = if(Common.ALARM_WAKEUP_TYPE_ELAPSED == 1) {
                val nowCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
                val diffTimeMillis = calendar.timeInMillis - nowCalendar.timeInMillis
                diffTimeMillis
            } else {
                calendar.timeInMillis
            }
        }

        //***알람 설정 ; 파라미터에 알람타입, trigger 시간정보 calendar, 시간 간격(단위, millis이므로 하루: 24 * 60 * 60 * 1000), sender
        /*alarmManager.setRepeating(
            AlarmManager.RTC_WAKEUP, calendar.timeInMillis,
            millisInterval, pendingIntent
        )*/

        if(triggerTimeMillis > 0) {
            //ELAPSED_REALTIME_WAKEUP
            if(Common.ALARM_WAKEUP_TYPE_ELAPSED == 1) {
                Log.d(Common.MY_TAG, "triggerTimeMillis: ${triggerTimeMillis}, ${triggerTimeMillis/60000.0}분 뒤에 알람 설정")
                when {
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->
                        alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTimeMillis, pendingIntent)
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ->
                        alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTimeMillis, pendingIntent)
                    else ->
                        alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTimeMillis, pendingIntent)
                }
            } else {
                //휴대폰 설정한 시간 기준
                val tDate = Date(triggerTimeMillis)
                val tDateFormat = SimpleDateFormat("yyyy-MM-dd kk:mm:ss E", Locale("ko", "KR"))

                // 현재 시간을 dateFormat 에 선언한 형태의 String 으로 변환
                val strDate = tDateFormat.format(tDate)

                Log.i(Common.MY_TAG, "${strDate}요일로 알람이 설정되었습니다.")
                when {
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->
                        alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pendingIntent)
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ->
                        alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pendingIntent)
                    else ->
                        alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pendingIntent)
                }
            }

            setTriggerPrefTime(triggerTimeMillis)
        }
    }

    private fun cancelAlarm() {
        val alarmManager = this.mContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val alarmIntent = Intent(this.mContext, MyReceiver::class.java)

        alarmIntent.action = Common.MY_ACTION_FOOT_PRINTER

        var flags = PendingIntent.FLAG_UPDATE_CURRENT//FLAG_NO_CREATE
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT//FLAG_NO_CREATE
        }
        val pendingIntent = PendingIntent.getBroadcast(this.mContext, 0, alarmIntent, flags)
        if(pendingIntent != null) {
            Log.i(Common.MY_TAG, "알람 ${alarmIntent.action}를 해제합니다.")
            alarmManager.cancel(pendingIntent)
        } else {
            Log.i(Common.MY_TAG, "등록된 알람 ${alarmIntent.action}가 없습니다.")
        }
    }

    fun stopAlarmService(){
        Log.i(Common.MY_TAG, "stopAlarmService called")
        cancelAlarm()

        resetTriggerPrefTime()
        resetPostTrialPref()

        //Toast.makeText(this.mContext, "service가 중지됩니다.!!", Toast.LENGTH_SHORT).show()
    }

 

알람 설정을 위해 receiver를 등록합니다.

<receiver android:name=".receiver.MyReceiver"
    android:exported="false">
    <intent-filter >
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="com.kakaroo.footprinterservice.FOOT_PRINTER"/>
    </intent-filter>
</receiver>

 

Receiver 에서 설정한 알람이 울릴 경우, 현재 위치를 서버에 post 합니다.

아래 조건에 해당되는 경우는 post 하지 않습니다.

- 현재시간이 설정한 제외시간/요일에 해당 되는 경우

- 위치 설정이 안 되어 있는 경우

- 위치 값이 이전 위치 값에서 설정한 반경 이내인 경우 (즉, 이전 경로 대비 이동거리가 짧은 경우)

- 서버 오류로 인해 동일시간(분단위)에 전송이 또 되는 경우

@RequiresApi(Build.VERSION_CODES.O)
    override fun onReceive(context: Context?, intent: Intent?) {
        var recvMode: Int = Common.ALARM_REPEAT_MODE
        Log.e(Common.MY_TAG, "onReceive : ${intent?.action}")
        when (intent?.action) {
            Common.MY_ACTION_FOOT_PRINTER -> {
                recvMode = Common.ALARM_REPEAT_MODE
            }
            Intent.ACTION_SCREEN_ON -> {
                recvMode = Common.BOOT_COMPLETE_MODE
            }
            Common.MY_ACTION_RESTART_SERVICE -> {
                recvMode = Common.RESTART_SERVICE_MODE
            }
        }

        if (context != null) {
            mContext = context

            val mPref = PreferenceManager.getDefaultSharedPreferences(mContext)
            val myAlarmService = MyAlarmService(mContext!!, mPref)

            if (recvMode == Common.ALARM_REPEAT_MODE) {
                if(executeLocationService(mPref, myAlarmService)) {
                    //알람 반복을 위해 다시 알람을 등록한다.
                    myAlarmService.startAlarmService(Common.ALARM_RESTART_REPEAT_MODE)
                }
            }
            else if (recvMode == Common.BOOT_COMPLETE_MODE) {
                context.startService(Intent(context, RestartService::class.java))
                /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    context.startForegroundService(Intent(context, RestartService::class.java))
                } else {
                    context.startService(Intent(context, RestartService::class.java))
                }*/

                val bStartedService = mPref.getBoolean("service_start_key", false)
                if(bStartedService) {
                    val nowCalendar = Calendar.getInstance(TimeZone.getTimeZone(Common.ASIA_TIME_ZONE))
                    if(timeIsIncludedExceptionTime(nowCalendar, mPref)) {  //현재시간이 해제시간에 포함되면
                        Log.i(Common.MY_TAG, "OnReceive - 부팅후, 현재 해제시간이므로 알람을 재설정합니다.!!")
                        myAlarmService.startAlarmService(Common.ALARM_RESTART_MODE)
                    } else {
                        Log.i(Common.MY_TAG, "OnReceive - 부팅후, 서비스 알람을 재설정합니다.!!")
                        executeLocationService(mPref, myAlarmService)   //바로 HTTP 시작

                        myAlarmService.startAlarmService(Common.ALARM_RESTART_REPEAT_MODE)
                    }
                } else {
                    Log.i(Common.MY_TAG, "OnReceive - 서비스가 켜져 있지 않았습니다.")
                }
            } else if(recvMode == Common.RESTART_SERVICE_MODE) {
                Log.i(Common.MY_TAG, "OnReceive - RestartService will be started")
                val bStartedService = mPref.getBoolean("service_start_key", false)
                //Log.i(Common.MY_TAG, "RESTART_SERVICE_MODE:: bStartedService:${bStartedService}")
                //알람 반복을 위해 다시 알람을 등록한다.
                if(bStartedService) {
                    myAlarmService.startAlarmService(Common.RESTART_SERVICE_MODE)
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    context.startForegroundService(Intent(context, RestartService::class.java))
                } else {
                    context.startService(Intent(context, RestartService::class.java))
                }
            }
        } else {
            Log.e(Common.MY_TAG, "OnReceive - context is not initialized yet!!")
        }
    }

서버에 data post는 Retrofit lib를 활용해 보겠습니다.

dependencies {
	//...
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.google.code.gson:gson:2.9.0'

}

 

<application
        android:usesCleartextTraffic="true"

 

 

Retrofit Service Interface 정의

 

post 방식만 필요하여 post만 정의했습니다.

package com.kakaroo.footprinterservice.service

import com.kakaroo.footprinterservice.entity.FootPrinter
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Query

//API를 관리해 주는 Interface
interface IRetrofitNetworkService {
    @POST("/post")
    @Headers("accept: application/json",
        "content-type: application/json")
    fun postMethod(
        @Body footPrinter: FootPrinter//,
        //@Query("id") id: Long
    ): Call<Int>	//Int: Server로부터의 response type
}

 

Retrofit 객체 생성

val retrofit = Retrofit.Builder()
    .baseUrl(Common.DEFAULT_URL + Common.URL_SLASH)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

 

인터페이스 타입의 서비스 객체 얻기

var networkService: IRetrofitNetworkService = retrofit.create(IRetrofitNetworkService::class.java)

 

 

모든 준비가 끝났습니다. 네트워크 통신이 필요한 순간에 Retrofit 객체로 얻은 서비스 객체의 함수를 호출만 해주면 됩니다. 서비스 클래스와 객체는 Retrofit 이 만들어 주지만, 우리가 만든 인터페이스를 구현한 클래스이므로, 인터페이스의 함수를 호출하면 네트워크 통신을 시도합니다.

 

//ex) 최종 output 형태는 2022-03-01 07:01:52 이어야 한다.
val current = LocalDateTime.now()
val formatted = current.format(DateTimeFormatter.ISO_DATE_TIME) //출력형식: 2019-03-22T23:56:36.408
val pTime = formatted.replace("T", " ")
val lastIndex = pTime.lastIndexOf(".")
val time = if(lastIndex == -1) pTime else pTime.substring(0, lastIndex)

Log.i(Common.MY_TAG, "postMethod:: $time, 위도:${mCurrentLocation.latitude}, 경도:${mCurrentLocation.longitude}")

val data = FootPrinter(id = 1, time = time, latitude = mCurrentLocation.latitude, longitude = mCurrentLocation.longitude)
networkService.postMethod(data).enqueue(object : Callback<Int> {
    override fun onResponse(call: Call<Int>, response: Response<Int>) {
        Log.d(Common.MY_TAG, response.toString())
        Log.d(Common.MY_TAG, "Post response: "+ response.body().toString())
    }

    override fun onFailure(call: Call<Int>, t: Throwable) {
        // 실패
        Log.d(Common.MY_TAG, t.message.toString())
        Log.d(Common.MY_TAG, "fail")
    }
})

 

 

Task가 kill 되고 나면 알람이 정상적으로 동작하지 않기 때문에 앱을 죽지 않게 하기 위해 서비스를 하나 등록해 줍니다.

<service
    android:name=".RestartService"
    android:enabled="true"
    android:exported="true"/>

 

해당 서비스는 destory가 불릴때  service를 다시 start 해주기 위해 아래 intent-filter를 alarm에 등록합니다.

<action android:name="com.kakaroo.footprinterservice.RESTART.PERSISTENTSERVICE" />

 

 

create가 호출되면 alarm을 해제하고, destroy가 호출되면 alarm을 등록합니다.

private fun unregisterRestartAlarm() {
        val alarmManager = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val alarmIntent = Intent(this, MyReceiver::class.java)
        alarmIntent.action = Common.MY_ACTION_RESTART_SERVICE

        var flags = PendingIntent.FLAG_UPDATE_CURRENT
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT//FLAG_UPDATE_CURRENT
        }

        val pendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, flags)
        if(pendingIntent != null) {
            alarmManager.cancel(pendingIntent)
        }
    }

    private fun registerRestartAlarm() {
        val alarmIntent = Intent(this, MyReceiver::class.java)
        alarmIntent.action = Common.MY_ACTION_RESTART_SERVICE
        var flags = PendingIntent.FLAG_UPDATE_CURRENT
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT//FLAG_UPDATE_CURRENT
        }
        val pendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, flags)

        val alarmManager = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager
/*
        val firstTime = SystemClock.elapsedRealtime() + Common.RESTART_SERVICE_ALARM_PERIOD
        alarmManager.setRepeating(
            AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime, Common.RESTART_SERVICE_ALARM_PERIOD, pendingIntent)
*/
        val triggerTimeMillis: Long = Common.RESTART_SERVICE_ALARM_PERIOD
        when {
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->
                alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTimeMillis, pendingIntent)
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ->
                alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTimeMillis, pendingIntent)
            else ->
                alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTimeMillis, pendingIntent)
        }

    }

 

 

 

source code : https://github.com/kakarooJ/Android-Kotiln-Location-Monitor

 

GitHub - kakarooJ/Android-Kotiln-Location-Monitor

Contribute to kakarooJ/Android-Kotiln-Location-Monitor development by creating an account on GitHub.

github.com

 

반응형