📝 SeSAC의 'JetPack과 Kotlin을 활용한 Android App 개발' 강좌를 정리한 글 입니다.
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) 이상에서 백그라운드 상태에서 위치 정보 엑세스 시 필요
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 제공
val manager = getSystemService(LOCATION_SERVICE) as LocationManager
API Level 1 부터 제공이 되서 지금까지 변경된 적이 없다고 한다.
val location: Location? = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
getLastKnownLocation()
함수의 매개변수가 Location Provider
이다. GPS 를 지정했기 때문에 유저가 음영지역에 들어가면 위치 추적이 불가능해진다.
획득 데이터는 Location
객체 타입으로 전달되고, 정보가 들어있는 일종의 VO 객체
라고 보면 된다.
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)
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<?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>
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"
}
}
}
낮은 전력 소모
정확도 향상
간단한 APIs
implementation("com.google.android.gms:play-services-location:21.0.1")
의존성을 추가해야 한다.
FusedLocationProviderClient : 위치 정보 획득
GoogleApiClient : 위치 정보 제공자 이용 준비, 다양한 콜백 제공
GoogleApiClient
에는 GoogleApiClient.ConnectionCallbacks
와 GoogleApiClient.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
는 구글의 다양한 서비스를 이용할 수 있는 클라이언트이기 때문에 위치 정보 이외의 서비스도 포함한다.
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
}
})
implementation("com.google.android.gms:play-services-location:21.0.1")
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<?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>
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 { }
}
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"/>
<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"
/>
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
객체를 받는다.
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="$GOOGLE_MAP_API_KEY" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<?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>
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))
}
}