본문 바로가기
깡샘 코틀린

19-1 사용자 위치 얻기

by 농농씨 2023. 7. 13.

여섯째 마당에서 앱에 다양한 기능을 추가하는 것에 대해 공부하고 있다.

17장 저장소에 데이터 보관하기, 18장 네트워크 프로그래밍에 이어 19장에서는 위치정보를 활용하는 것을 배울 것이다.

 

앱에서 사용자의 위치를 얻는 데는 1️⃣플랫폼 API를 이용하는 방법과 2️⃣구글 Play 서비스에서 제공하는 라이브러리를 이용하는 방법이 있다.

 

위치 접근 권한

먼저 앱에서 사용자의 위치를 추적하려면 3가지 권한을 얻어야 한다. 또한 각 상황에 맞춰 매니페스트 파일에 이 권한을 등록해야 한다.

  • android.permission.ACCESS_COARSE_LOCATION: 와이파이나 모바일 데이터(또는 둘 다)를 사용해 기기의 위치에 접근하는 권한이다. 도시에서 1블록 정도의 오차 수준이다.(정확도 높아야 할 때. ex) 길안내)
  • android.permission.ACCESS_FINE_LOCATION: 위성, 와이파이, 모바일 데이터 등 이용할 수 있는 위치 제공자를 사용해 최대한 정확한 위치에 접근하는 방법이다.(대략 어느 블록 안에 있는지만 파악할 때)
  • android.permission.ACCESS_BACKGROUND_LOCATION: 안드로이드 10(API 레벨 29) 이상에서 백그라운드 상태에서 위치에 접근하는 권한이다.

ACCESS_COARSE_LOCATION과 ACCESS_FINE_LOCATION은 앱에서 얼마나 정확한 위치가 필요한 지에 따라 선택하면 된다. 길 안내처럼 정확도가 높아야 한다면 ACCESS_FINE_LOCATION을, 대략 어느 블록 안에 있는지만 파악하려면 ACCESS_COARSE_LOCATION을 등록한다. 또한 앱이 백그라운드 상황일 때 위치에 접근하려면 안드로이드 10*부터는 ACCESS_BACKGROUND_LOCATION을 추가로 등록해야 한다.

*안드로이드 10버전 미만에서는 앱이 포그라운드 위치 권한을 얻으면 백그라운드 위치 권한도 자동으로 부여해 준다.

그리고 안드로이드 12(API 레벨 31) 버전부터는 android.permission.ACCESS_FINE_LOCATION을 등록하려면 android.permission.ACCESS_COARSE_LOCATION을 같이 등록해줘야 한다.

 

서비스 컴포넌트에서 위치에 접근하려면 다음처럼 foregroundServiceType 속성에 "location"을 지정해 줘야 한다.

// 서비스에서 위치 접근
<service // 서비스 컴포넌트의 태그의
    android:name="MyNavigationService"
    android:foregroundServiceType="location" // 속성에 "location" 지정
    (... 생략 ...)>
</service>

 

 

플랫폼 API의 위치 매니저

플랫폼 API를 이용해 사용자의 위치를 얻었을 때는 LocationManager라는 시스템 서비스를 이용한다.

// 위치 매니저 사용
val manager = getSystemService(LOCATION_SERVICE) as LocationManager

 

위치 제공자 지정하기

사용자의 위치를 얻으려면 먼저 위치 제공자(location provider)를 구해야 한다. 흔히 GPS 위성만을 위치 제공자라고 생각하기 쉽지만 이동 통신망이나 와이파이도 있으며 국가에 따라 차이가 있다.

  • GPS: GPS 위성을 이용한다.
  • Network: 이동 통신망을 이용한다.
  • Wifi: 와이파이를 이용한다.
  • Passive: 다른 앱에서 이용한 마지막 위치 정보를 이용한다.

만약 현재 기기에 어떤 위치 제공자가 있는지를 알고 싶다면 다음처럼 LocationManager의 allProvider 프로퍼티를 이용한다. allProviders는 MutableList<String> 타입의 결과를 반환한다.

// 모든 위치 제공자 알아보기
val result = "ALL Providers : "
val providers = manager.allProviders // allProviders 프로퍼티 이용
for (provider in providers) {
    result += "$provider, "
}
Log.d("kkang", result) // All Providers : passive, gps, network,

만약 지금 사용할 수 있는 위치 제공자를 알아보려면 getProviders() 함수를 이용한다.

// 지금 사용할 수 있는 위치 제공자 알아보기
result = "Enabled Providers : "
val enabledProviders = manager.getProvider(true)
for (provider in enabledProviders) {
    result += "$provider, "
}
Log.d("kkang", result) // Enabled Providers : passive, gps, network,

 

 

위치 정보 얻기

이제 사용자의 위치 정보를 얻으려면 LocationManager의 getlastKnownLocation() 함수를 이용한다.

// 위치 한 번만 가져오기
if (ContextCompat.checkSelfPermission(
        this, Manifest.permission.ACCESS_FINE_LOCATION
    ) === Packagemanager.PERMISSION_GRANTED
) {
    val location: Location? =
        manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
        // 사용자의 위치 정보 얻기 위한 함수. 반환값은 Location.
    location?.let{
        val latitude = location.latitude // 위도 정보
        val longitude = location.longtitude // 경도 정보
        val accuracy = location.accuracy // 위치 정확도 정보
        val time = location.time // 위치 획득 시간 정보
        
        Log.d("kkang", "$latitidue, $longtitude, $accuracy, $time")
    }
}

getLastKnownLocation() 함수의 반환값은 Location이며 만약 널이면 위치를 가져오는 데에 실패한 것이다. Location은 위치의 정확도, 위도, 경도, 획득 시간 등의 데이터를 포함하는 객체이다.

  • getAccuracy(): 정확도 
  • getLatitude(): 위도
  • getLongtitude(): 경도
  • getTime(): 획득 시간

getLastKnownLocation() 함수는 위치를 한 번만 가져올 때 사용하며, 계속 위치를 가져와야 한다면 LocationListener를 이용한다.

// 위치 계속 가져오기
val listener: LocationListener = object : LocationListener {
// 계속 위치 가져오기 위해 LocationListener 객체 이용
    override fun onLocationChanged(location: Location) {
    // 새로운 위치 가져올 때 호출되는 함수
    // 추상함수이므로 꼭 재정의해야 함
        Log.d("kkang", "${location.latitude}, ${location.longtitude}, ${location.accuracy}")
    }
    override fun onProviderDisabled(provider: String) {
    // 위치 제공자가 이용할 수 있는 상황에 호출, 필요에 따라 재정의
        super.onProviderDisabled(provider)
    }
    override fun onProviderEnabled(provider: String) {
    // 위치 제공자가 이용할 수 없는 상황에 호출, 필요에 따라 재정ㅇ의
        super.onProviderEnabled(provider)
    }
}
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10_000L, 10f, listener)
// LocationListener를 구현한 객체를 등록하여 위치를 계속가져올 수 있도록 함
// 두 번째 매개변수로 위치 전달 주기 밀리초로 설정
// 세번째 매개변수로 거리가 10f 미터만큼 변경되었을 때 위치 전달 받음
(... 생략 ...)
manager.removeUpdates(listener)
// 위치 더이상 가져오지 않을 때 등록 해제함

LocationListener의 함수는 다음과 같은 상황일 때 호출된다.

  • onLocationChanged(): 새로운 위치를 가져오면 호출된다.
  • onProviderEnabled(): 위치 제공자가 이용할 수 있는 상황이면 호출된다.
  • onproviderDisabled(): 위치 제공자가 이용할 수 없는 상황이면 호출된다.

LocationListener 인터페이스에서 onLocationChanged()는 추상함수이므로 꼭 재정의해야 하지만 나머지 onProviderEnabled() 와 onProviderDisabled() 함수는 필요에 따라 재정의하면 된다. LocationListener를 구현한 클래스의 객체를 LocationManager의 requestLocationUpdates() 함수로 등록하면 위치를 계속 가져올 수 있다. 만약 위치를 더 이상 가져올 필요가 없다면 removeUpdates() 함수로 해제한다. requestLocationUpdates() 함수의 두 번째 매개변수는 위치 전달 주기이며 밀리초(ms, 1,000분의 1초) 단위로 설정한다. 세 번째 매개변수는 거리가 얼마만큼 변경되었을 때 위치를 전달받을지 미터(m) 단위로 설정한다.

 

 

구글 Play 서비스의 위치 라이브러리

앞에서 살펴본 것처럼 앱에서 사용자의 위치를 얻을 때는 여러 가지 상황을 고려해 적절한 위치 제공자를 지정하는 일이 무엇보다 중요하다. 이때 고려할 사항을 정리하면 다음과 같다.

  • 전력을 적게 소비하는가?
  • 정확도는 높은가?
  • API가 간단한가?
  • 부가 기능을 제공하는가?
  • 대부분 안드로이드 기기를 지원하는가?

이처럼 고려할 사항이 많다 보니 자연스럽게 개발 과정이 복잡해진다. 그래서 구글에서는 최적의 알고리즘으로 위치 제공자를 지정할 수 있도록 Fused Location Provider라는 라이브러리를 제공한다.

 

먼저 Fused Location Provider를 이용하려면 빌드 그래들의 dependencies 항목에 다음처럼 구글의 play-services 라이브러리를 선언해야 한다.

// play 서비스 사용 선언
implementation 'com.google.android.gms:play-services:12.0.1'

Fused Location Provider에서 핵심 클래스는 다음 2가지이다.

  • FusedLocationProviderClient: 위치 정보를 얻는다.
  • GoogleApiClient: 위치 제공자 준비 등 다양한 콜백을 제공한다.

1️⃣GoogleApiClient에서 위치 정보 제공자를 결정하면 이를 이용해서 2️⃣FusedLocationProviderClient에서 위치를 가져오는 구조이다. 우선 두 클래스를 초기화해야 하는데, GoogleApiClient에서는 GoogleApiClient.ConnectionCallbacks(연결성공시의 콜백) 와 GoogleApiClient.OnConnectionFailedListener(연결실패시의 콜백(?)) 인터페이스를 구현한 객체를 지정해야 한다.

// GoogleApiClient 초기화(위치 정보 제공자 결정할 준비)
val connectionCallback=object: GoogleApiclient.ConnectionCallbacks {
// 위치 제공자 준비 등 다양한 콜백을 준비하는 GoogleApiClient 클래스 중에서도
// 위치제공자 있을 시의 콜백을 담당하는 ConnectionCallbacks 인터페이스를 구현한 객체를 지정함
    override fun onConnected(p0: Bundle?) {
        // 위치 제공자를 사용할 수 있을 때 (자동 호출됨)
        // 위치 획득
    }
    override fun onConnectionSuspended(p0: int) {
        // 위치 제공자를 사용할 수 없을 때 (자동 호출됨)
    }
}
val onConnectionFailedCallback = object : GoogleApiClient.OnConnectionFailedListener {
// GoogleApiClient 클래스의, 위치제공자 없을 시의 콜백을 담당하는 
// OnConnectionailedListener 인터페이스를 초기화함(구현한 객체를 지정함)
    override fun onConnectionFailed(p0: ConnectionResult) {
        // 사용할 수 있는 위치 제공자가 없을 때 (자동 호출됨)
    }
}
val apiClient: GoogleApiClient = GoogleApiClient.Builder(this)
    .addApi(LocationServices.API)
    .addConnectionCallbacks(connectionCallback)
    .addOnConnectionFailedListener(onConnectionFailedCallback)
    .build()

GoogleApiClient.Builder의 addConnectionCallbacks() 함수에 GoogleApiClient.ConnectionCallbacks 인터페이스를 구현한 객체를 지정하면 위치 제공자를 사용할 수 있을 때 onConnected() 함수가 자동으로 호출된다. 그러면 onConnected() 함수에서 위치를 가져오면 된다. 만약 위치 제공자가 어느 순간에 사용할 수 없게 되면 onConnectionSuspended() 함수가 자동으로 호출된다.

 

GoogleApiClient.Builder의 addOnconnectionFailedListener() 함수에 GoogleApiClient.OnConnectionFailedListener 인터페이스를 구현한 객체를 지정하면 사용할 수 있는 위치 제공자가 없을 때 onConnectionFailed() 함수가 자동으로 호출된다.

 

FusedLocationProviderClient는 다음처럼 초기화한다.

// FusedLocationProviderClient 초기화
val providerClient: FusedlocationProviderClient = // 위치 정보 얻는 클래스
    LocationServices.getFusedLocationProviderClient(this)

 

GoogleApiClient와 FusedLocationProviderClient를 초기화했다면 이제 위치를 가져와야 하는데, 그러려면 먼저 GoogleApiClient 객체에 위치 제공자를 요청해야 한다.

// 위치 제공자 요청
apiClient.connect()

 

GoogleApiClient의 connect() 함수를 호출하면 여러 가지 상황을 고려해 가장 알맞은 위치 제공자를 선택한 다음 onConnected() 함수를 호출해 준다. 따라서 onConnected() 함수에서 다음처럼 FusedLocationProviderClient의 getLastLocation() 함수만 호출해주면 된다. 이 함수는 앱에서 마지막으로 알려진 사용자 기기의 위치를 요청한다.

// 사용자 위치 얻기
override fun onConnected(p0: Bundle?) {
// 위치제공자 선택 후 호출
    if (Contextcompat.checkSelfPermission(
            this@FusedActivity,
            manifest.permission.AcCESS_FIEN_LOCATION
        ) === PackageManager.PERMISSION_GRANTED
    ) {
        providerClient.getLastLocation().addOnSuccessListener(
        // 앱에서 마지막으로 알려진 사용자 기기의 위치 요청
            this@FusedActivity,
            object : OnSuccessListener<Location> {
                override fun onSuccess(location: Location?) {
                // onSuccess 함수 호출되면서 위치 전달됨
                    val latitude = locatioin?.latitude
                    val longtitude = location?.longtitude
                }
            })
    }
}

getLastLocation() 함수로 위치를 가져오는데, 이때 결괏값은 addOnSuccessListener() 함수에 등록한 OnSuccessListener를 구현한 객체의 onSuccess() 함수가 호출되면서 전달된다.