1. Android Mock Location 앱으로 SynRTK 지정

설정 > 개발자 옵션 > 가상 위치 앱 선택 > 가상 위치 앱으로 SynRTK 선택

Screenshot_20260616_155942_Settings.jpg

Screenshot_20260616_155916_Settings.jpg

2. Location 데이터 구조 (표준)

필드 타입 설명
latitude double 위도
longitude double 경도
altitude double 고도 (m) 해수면 고도 (MSL)
accuracy float 수평 정확도 (m) HRMS
(Horizontal Root Mean Square):
수신기에서 측정한 위치가 수평 방향(위도 및 경도, 즉 2D 평면)으로 실제 위치(True Position)에서 평균적으로 얼마나 벗어나 있는지를 나타내는 통계적 오차 지표
speed float Gnss Status (Fix 상태)
0.0f → No Fix
1.0f → 3DFix
2.0f → DGNSS
3.0f → Unknown
4.0f → RTK Fixed
5.0f → RTK Float 4.0f 즉 RTK Fixed 상태이면,
위/경도가 2 ~ 3 cm 오차의
RTK 정확도 위치임
bearing float 0.0 으로 고정
time long 밀리초 (시스템 시간)
elapsedRealtimeNanos long 나노초 (시스템 시간)

3. 샘플 코드

package com.synerex.samplebroadcastreceiver

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat

// Mock Location 으로 데이터를 수신하는 액티비티.
// Mock Location 으로 수신되는 Location 의 extras 에는
// Broadcast(position) 로 수신하던 것과 동일한 key/value 가 들어 있어
// MainActivity 에서 보여주던 Position 정보를 그대로 표시한다.
class MockLocationActivity : AppCompatActivity() {

    private var text_data: TextView? = null
    private var locationManager: LocationManager? = null

    private val locationListener = object : LocationListener {
        override fun onLocationChanged(location: Location) {
            text_data?.text =
                "time: ${location.time}\n" +
                "latitude: ${location.latitude}\n" +
                "longitude: ${location.longitude}\n" +
                "altitude: ${location.altitude}\n" +
                "accuracy: ${location.accuracy}\n" +
                "gnss_status: ${location.speed} (${fixTypeText(location.speed)})\n" +
                "bearing: ${location.bearing}\n" +
                "elapsedRealtimeNanos: ${location.elapsedRealtimeNanos}\n"
        }
        override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
        override fun onProviderEnabled(provider: String) {}
        override fun onProviderDisabled(provider: String) {}
    }

    // location.speed 값에 대응하는 Fix 상태 텍스트
    private fun fixTypeText(speed: Float): String {
        return when (speed) {
            0.0f -> "No Fix"
            1.0f -> "3DFix"
            2.0f -> "DGNSS"
            3.0f -> "Unknown"
            4.0f -> "RTK Fixed"
            5.0f -> "RTK Float"
            else -> "Unknown" // -1.0f 포함, 정의되지 않은 값
        }
    }

    private val locationPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (granted) {
            startMockLocationUpdates()
        } else {
            Toast.makeText(this, "위치 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
            finish()
        }
    }

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

        text_data = findViewById(R.id.text_data)
        locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager

        // 뒤로 가기(닫기) 버튼 → 액티비티 종료 후 MainActivity 로 복귀 (좌/우 상단)
        val closeClickListener = android.view.View.OnClickListener { finish() }
        findViewById<Button>(R.id.button_close).setOnClickListener(closeClickListener)
        findViewById<Button>(R.id.button_close_left).setOnClickListener(closeClickListener)
    }

    override fun onResume() {
        super.onResume()
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
            == PackageManager.PERMISSION_GRANTED) {
            startMockLocationUpdates()
        } else {
            locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
        }
    }

    override fun onPause() {
        super.onPause()
        stopMockLocationUpdates()
    }

    private fun startMockLocationUpdates() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED) {
            return
        }
        try {
            locationManager?.requestLocationUpdates(
                LocationManager.GPS_PROVIDER, 0L, 0f, locationListener
            )
        } catch (e: SecurityException) {
            e.printStackTrace()
        } catch (e: IllegalArgumentException) {
            Toast.makeText(this, "GPS provider 를 사용할 수 없습니다.", Toast.LENGTH_SHORT).show()
        }
    }

    private fun stopMockLocationUpdates() {
        locationManager?.removeUpdates(locationListener)
    }
}