[Android] 위치 정보(GeoLocation) 정리

Minjun Kim·2023년 10월 13일
0

Android

목록 보기
47/47
post-thumbnail

📝 SeSAC의 'JetPack과 Kotlin을 활용한 Android App 개발' 강좌를 정리한 글 입니다.


💬 GeoLocation 개요

❕ 퍼미션

  • 유저의 위치를 이용한 서비스
1. android.permission.ACCESS_COARSE_LOCATION
: WiFi 또는 모바일 데이터(또는 둘 다) 를 사용하여 기기의 위치 획득. 정확도는 도시 블록 1개 정도의 오차 수준


2. android.permission.ACCESS_FINE_LOCATION 
: 위성, WiFi 및 모바일 데이터 등 이용 가능한 위치 제공자를 사용하여 최대한 정확하게 위치를 결정


3. android.permission.ACCESS_BACKGROUND_LOCATION 
: Android 10(API 수준 29) 이상에서 백그라운드 상태에서 위치 정보 엑세스 시 필요

📚 Location Provider

  • 위치 제공자 (Location Provider)
1. GPS : GPS 위성을 이용하여 위치 정보 획득

2. Network : 이동통신사 망 정보를 이용하여 위치 정보 획득

3. Wifi : 와이파이의 AP 정보를 이용하여 위치 정보 획득

4. Passive : 다른 앱에서 이용한 마지막 위치 정보 획득

보통은 정확도가 높은 GPS 를 많이 사용하는 편이다. 하지만 음영지역이 있어 건물 내부, 지하 등에서는 사용이 불가능한 경우가 있기 때문에 적절한 위치 제공자를 고민하는 것이 개발자의 몫이다.

그런데 이 고민을 대신 해주는 기술이 있다. -> Fused Location API

📓 모든 위치 제공자 확인

  • 폰이 어떤 위치정보 제공자가 있는지 파악
val manager = getSystemService(LOCATION_SERVICE) as LocationManager
val providers = manager.allProviders
for (provider in providers) {
	result += "$provider,"
}

📓 사용 가능한 위치 제공자 확인

  • 현 시점에 가용한 프로퍼티를 획득
val enabledProviders = manager.getProviders(true)	// getProviders() 매개변수로  true 전달
for (provider in enabledProviders) {
	result += "$provider,"
}

➕ 위도 / 경도

  • 기본적으로 위/경도는 도/분/초로 표시되는데 일반적인 시스템에서는 실수로 사용

위도 : 90도 ~ 0도 ~ -90도 (0도 : 적도)

경도 : 180도 ~ 0도 ~ -180도 (0도 : 그리니치 천문대)

37도 30분 30초 -> 37.5

데이터가 자동으로 실수로 전달되기 때문에 그냥 갖다 쓰면된다. 보통 소수점 6자리까지만 이용한다.

📚 위치 정보 추적 방법

1. LocationManager -> 플랫폼 제공 (표준 API)

2. Fused API -> Google 제공

💬 LocationManager

📚 시스템 서비스 획득

  • 플랫폼 API 에서 제공되는 시스템 서비스
val manager = getSystemService(LOCATION_SERVICE) as LocationManager

API Level 1 부터 제공이 되서 지금까지 변경된 적이 없다고 한다.

📚 위치 정보 획득

  • 위치 정보 획득은 LocationManager 의 getLastknownLocation() 함수를 이용
val location: Location? = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)

getLastKnownLocation() 함수의 매개변수가 Location Provider 이다. GPS 를 지정했기 때문에 유저가 음영지역에 들어가면 위치 추적이 불가능해진다.

획득 데이터는 Location 객체 타입으로 전달되고, 정보가 들어있는 일종의 VO 객체 라고 보면 된다.

📚 위치값 획득

  • 결과값은 Location 객체로 전달
getAccuracy() : 정확도

getLatitude() : 위도

getLongitude() : 경도

getTime() : 획득 시간

Location 객체getter 함수 로 위치값을 뽑아내면 된다.

📚 지속적인 위치 추척

  • 지속적으로 위치를 획득해야 한다면 LocationListener 를 이용
val listener: LocationListener = object : LocationListener {
	override fun onLocationChanged(location: Location) {	// 위치 변경 시 자동 호출
    	TODO("Not yet implemented")
	}
    override fun onProviderDisabled(provider: String) {		// Provider 이용 불가 시 자동 호출
    	super.onProviderDisabled(provider)
	}
	override fun onProviderEnabled(provider: String) {		// Provider 다시 이용 가능 시 자동 호출
    	super.onProviderEnabled(provider)
	}
}

manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10_000L, 10f, listener)
														    // 시간, 오차 범위

//................

manager.removeUpdates(listener)

🧩 실습 예제

❕ 퍼미션 추가

  • AndroidManifest.kt
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

🎨 액티비티 레이아웃

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/resultView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20dp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

📂 메인 소스 코드

  • MainActivity.kt
package com.kotdev99.android.c81

class MainActivity : AppCompatActivity() {

	private lateinit var resultView: TextView
	private lateinit var manager: LocationManager

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

		// 멤버변수 초기화
		resultView = findViewById(R.id.resultView)
		manager = getSystemService(LOCATION_SERVICE) as LocationManager

		// 퍼미션 획득
		val launcher = registerForActivityResult(
			ActivityResultContracts.RequestPermission()
		) { isGranted ->
			// 퍼미션 다이얼로그가 닫힌 순간 콜 되는 부분
			if (isGranted) {
				getLocation()
			} else {
				Toast.makeText(this, "denied...", Toast.LENGTH_SHORT).show()
			}
		}

		// 퍼미션 확인
		val status = ContextCompat.checkSelfPermission(
			this,
			"android.permission.ACCESS_FINE_LOCATION"
		)
		if (status == PackageManager.PERMISSION_GRANTED) {
			getLocation()
		} else {
			launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
		}
	}

	// 위치 추적
	private fun getLocation() {
		val location = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
		location?.let {
			val latitude = location.latitude
			val longitude = location.longitude
			val accuracy = location.accuracy
			val time = location.time

			resultView.text = "$latitude \n $longitude \n $accuracy \n $time"
		}
	}
}

📲 결과


💬 Fused API

❓ 사용 이유

  • 위치정보를 획득할 때 여러가지 상황을 고려
  1. 낮은 전력 소모

  2. 정확도 향상

  3. 간단한 APIs

  • 정보 획득과 관련된 코드의 복잡함을 줄이기 위해 구글에서 제공되는 API 가 Fused Location Provider
implementation("com.google.android.gms:play-services-location:21.0.1")

의존성을 추가해야 한다.

📚 핵심 클래스

  • Fused Location Provider 의 핵심 클래스
FusedLocationProviderClient : 위치 정보 획득

GoogleApiClient : 위치 정보 제공자 이용 준비, 다양한 콜백 제공
  • GoogleApiClient 에는 GoogleApiClient.ConnectionCallbacksGoogleApiClient.OnConnectionFailedListener 인터페이스를 구현한 객체를 지정

GoogleApiClient 로 프로바이더 결정 및 사용가능 여부 등을 판단하고, FusedLocationProviderClient 로 실제 위치값을 얻는다.

📝 코드 작성

val connectionCallback = object : GoogleApiClient.ConnectionCallbacks {
	override fun onConnected(p0: Bundle?) {
    	// 위치 정보 제공자가 사용 가능 상황이 된 순간
        	// 위치 정보 획득
	}
    
    override fun onConnectionSuspended(p0: Int) {
    	// 위치 정보 제공자가 어느순간 사용 불가능 상황이 될 때
	}
}
val onConnectionFailedCallback = object : GoogleApiClient.OnConnectionFailedListener {
	override fun onConnectionFailed(p0: ConnectionResult) {
    	// 가용할 위치 제공자가 없는 경우
	}
}
val apiClient: GoogleApiClient = GoogleApiClient.Builder(this)
	.addApi(LocationServices.API)	// 사용할 API 지정
    .addConnectionCallbacks(connectionCallback)
    .addOnConnectionFailedListener(onConnectionFailedCallback)
    .build()

참고로 GoogleApiClient 는 구글의 다양한 서비스를 이용할 수 있는 클라이언트이기 때문에 위치 정보 이외의 서비스도 포함한다.

📓 위치 제공자 지정

  • GoogleApiClient 객체에 위치 제공자를 판단
apiClient.connect()

📚 위치 획득

  • FusedLocationProviderClient 의 getLastLocation() 함수를 이용해 위치 획득

  • 결과 값은 addOnSuccessListener() 함수에 등록한 OnSuccessListener 구현 객체의 onSuccess() 함수가 호출되며 전달

providerClient.getLastLocation().addOnSuccessListener(
	this@FusedActivity,
    object : OnSuccessListener<Location> {
    	override fun onSuccess(location: Location?) {
        	val latitude = location?.latitude
            val longitude = location?.longitude
		}
    })

🧩 실습 예제

🧷 종속성 추가

  • AndroidManifest.kt
implementation("com.google.android.gms:play-services-location:21.0.1")

❕ 퍼미션 추가

  • AndroidManifest.kt
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

🎨 액티비티 레이아웃

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/resultView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20dp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

📂 메인 소스 코드

  • MainActivity.kt
package com.kotdev99.android.c82

class MainActivity : AppCompatActivity() {

	private lateinit var resultView: TextView
	private lateinit var providerClient: FusedLocationProviderClient

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

		resultView = findViewById(R.id.resultView)

		val apiClient = GoogleApiClient.Builder(this)
			.addApi(LocationServices.API)
			.addConnectionCallbacks(connectionCallback)
			.addOnConnectionFailedListener(connectionFailedCallback)
			.build()

		providerClient = LocationServices.getFusedLocationProviderClient(this)

		val launcher = registerForActivityResult(
			ActivityResultContracts.RequestPermission()
		) {
			if (it) {
				apiClient.connect()
			} else {
				Toast.makeText(this, "denied", Toast.LENGTH_SHORT).show()
			}
		}

		val status = ContextCompat.checkSelfPermission(
			this,
			"android.permission.ACCESS_FINE_LOCATION"
		)
		if (status == PackageManager.PERMISSION_GRANTED) {
			apiClient.connect()
		} else {
			launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
		}
	}

	val connectionCallback = object : GoogleApiClient.ConnectionCallbacks {
		override fun onConnected(p0: Bundle?) {
			providerClient.lastLocation.addOnSuccessListener {
				val latitude = it?.latitude
				val longitude = it?.longitude
				resultView.text = "$latitude, $longitude"
			}
		}

		override fun onConnectionSuspended(p0: Int) {

		}
	}

	val connectionFailedCallback = GoogleApiClient.OnConnectionFailedListener { }
}

📲 결과


💬 GoogleMap

❕ AndroidManifest.xml 설정

implementation("com.google.android.gms:play-services-maps:18.1.0")
implementation("com.google.android.gms:play-services-location:21.0.1")
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
<meta-data android:name="com.google.android.maps.v2.API_KEY"
	android:value="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"/>
<meta-data android:name="com.google.android.gms.version"
	android:value="@integer/google_play_services_version"/>

🎨 UI 구성

  • play-service 라이브러리에서 지도는 프래그먼트로 제공
<fragment
	xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.google.android.gms.maps.SupportMapFragment"
	/>

📝 지도 출력

  • GoogleMap 이라는 클래스가 지도를 출력
override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    (supportFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment?)!!.getMapAsync(this)
}

override fun onMapReady(p0: GoogleMap?) {
	googleMap=p0
}

지도를 출력할 뷰에 getMapAsync() 를 등록하면 지도가 출력 가능한 상태가 되었을 때 콜백을 호출한다. onMapReady() 콜백이 자동 호출되면서 매개변수로 GoogleMap 객체를 받는다.

🧩 실습 예제

🧷 종속성 추가

  • AndroidManifest.kt
implementation("com.google.android.gms:play-services-maps:18.1.0")
implementation("com.google.android.gms:play-services-location:21.0.1")

❕ 퍼미션 추가

  • AndroidManifest.kt
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-library
	android:name="org.apache.http.legacy"
	android:required="false" />
    
	<meta-data
		android:name="com.google.android.maps.v2.API_KEY"
		android:value="$GOOGLE_MAP_API_KEY" />
	<meta-data
		android:name="com.google.android.gms.version"
		android:value="@integer/google_play_services_version" />

🎨 액티비티 레이아웃

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mapView"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

</fragment>

📂 메인 소스 코드

  • MainActivity.kt
package com.kotdev99.android.c83

class MainActivity : AppCompatActivity(), OnMapReadyCallback {

	private var googleMap: GoogleMap? = null

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

		(supportFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment).getMapAsync(
			this
		)
	}

	override fun onMapReady(p0: GoogleMap) {
		googleMap = p0
		val latLng = LatLng(37.566610, 126.978403)
		val position = CameraPosition.Builder()
			.target(latLng)
			.zoom(16f)
			.build()
		googleMap?.moveCamera(CameraUpdateFactory.newCameraPosition(position))
	}
}

📲 결과

profile
응애 나 아기 뉴비

0개의 댓글