안드로이드 공식문서에서 제공하는 위치 라이브러리입니다. 기존에 사용하던 LocationManager는 생각보다 정확하지 않아서 새로운 위치기반 API로 바꾸면서 정리해보려 합니다.
총 4단계로
- 의존성 주입
- 권한 설정 및 권한 검증
- 위치 기능 구현
- 사용
입니다.
의존성 주입
구글 위치 설정 라이브러리 의존성을 app 단 build.gradle에 넣어줍니다.
//구글 위치 설정 라이브러리
implementation "com.google.android.gms:play-services-location:21.0.1"
권한 설정 및 권한 검증
ManiFest에서 정확 위치 권한과 대략적인 위치 권한을 설정해 줍니다. Anrdoid12(API 30, 31) 이상부턴 ACCESS_FINE_LOCATION을 요청하면 ACCESS_COARSE_LOCATION을 요청해야 하므로 둘 다 요청해 줍니다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
위치권한을 사용자에게 요청합니다.
fun requestMyPermission(){
if(checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION,
), 1
)
}
}
백그라운드 위치 권한
Android10(API 29) 이상에선 개발자는 런타임 시 백그라운드 위치 정보 액세스 권한을 요청하기 위해 맵 매니 페스트에서 권한을 선언해줘야 합니다.
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
권한 요청을 마쳤다면 사용자에게 백그라운드 위치 권한을 허용하도록 Setting 창으로 이동하는 Dialog를 띄워줍니다.
백그라운드 위치 권한은 앱 내에서 설정할 수 없고 앱 권한 설정에서 사용자가 직접 할당할 수 있습니다. 또한 포그라운드 위치가 할당돼 있어야 백그라운드 위치 권한이 제대로 작동합니다. 따라서 포그라운드 위치 권한이 허용된 후 Dialog로 요청해 줍니다.
- 기존 방법
AlertDialog.Builder(this)
.setTitle("백그라운드 위치권한")
.setMessage("백그라운드 위치권한을 허용해주세요")
.setNegativeButton("취소"){p0, p1 ->
p0.dismiss()
}
.setPositiveButton("허용") { p0, p1 ->
requestPermissions(
this, arrayOf(
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
), 0
)
p0.dismiss()
}.show()
- 컴포즈
@RequiresApi(Build.VERSION_CODES.Q)
@Composable
fun showBackgroundPermissionDialog(){
val activity = LocalContext.current as MainActivity
var openAlertDialog by remember {
mutableStateOf(true)
}
if(openAlertDialog)
AlertDialog(onDismissRequest = { /*TODO*/ },
title = { Text(text = "백그라운드 위치권한")},
text = { Text(text = "백그라운드 위치권한을 허용해주세요")},
dismissButton = {
Button(onClick = {
openAlertDialog = false
}) {
Text(text = "취소")
}
},
confirmButton = {
Button(onClick = {
openAlertDialog = false
requestPermissions(activity, arrayOf(
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
), 0 )
}){
Text(text = "허용")
}
}
)
}
여기서 주의할 점은 사용자가 2회 백그라운드 위치 권한을 설정하지 않고 뒤로 가기를 했다면, 요청해도 권한 설정으로 넘어가지 않습니다.
위 사항에 대해선 별도의 안내 문구를 띄워주는 걸 추천드립니다.
위치 기능 구현
위치 요청하는 함수는 3가지 입니다.
- getLastLocation : 캐싱돼 있는 위치를 가져옵니다.
- 장점 : 빠르다. 배터리 소모량이 적다
- 단점 : 캐싱돼 있는 위치 정보가 최신이 아닐 수도 있다.
- requestLocationUpdates : 일정한 간격으로 사용자의 위치를 파악할 수 있습니다.
- 장점 : 별도의 반복 작성 없이 사용자의 위치를 추적할 수 있다.
- 단점 : 제 때 중지하지 않으면 리소스가 낭비될 수 있다. 요청시간이 상대적으로 길다(평균 1.7124초)
- getCurrentLocation
- 장점 : 현재 위치를 가져올 수 있다
- 단점 : 콜백 형식이라 별도의 데이터를 받을 객체나 리스너가 필요하다
위치 요청
클래스 생성
클래스에 위도와 경도 변수를 가진 companion object와 FusedLocationClient를 생성해 줍니다.
class MyLocationManager {
companion object{
var currentLatitude = 0.0
var currentLongitude = 0.0
}
val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(applicationContext())
...
}
위치 결과를 받을 콜백 만들어 줍니다.
private val locationCallBack = object : LocationCallback() {
override fun onLocationAvailability(p0: LocationAvailability){
val a = p0.isLocationAvailable
Log.d(TAG, "Location Availability : $a")
}
override fun onLocationResult(locationResult: LocationResult) {
currentLatitude = locationResult.lastLocation?.latitude ?: 0.00
currentLongitude = locationResult.lastLocation?.longitude ?: 0.00
}
}
마지막 위치를 받을 수 있도록 빌더패턴으로 업데이트를 요청해 줍니다
fun getLastLocationClient(){
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000).build()
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallBack, Looper.getMainLooper())
}
우선순위는
- PRIORITY_HIGH_ACCURACY : 아주 높은 정확성 + 배터리 많이 닳음
- PRIORITY_BALANCED_POWER_ACCURACY : 전력사용 최적화 + 정확한 위치
- PRIORITY_LOW_POWER : 배터리를 저전력으로 사용하지만 도시 수준의 정확도를 제공
- PRIORITY_PASSIVE : 다른 앱에서 이미 연산되어 있는 위치정보를 수동적으로 수신
PRIORITY_HIGH_ACCURACY와 PRIORITY_BALANCED_POWER_ACCURACY 중 선택해서 설정해 주면 됩니다.
두 번째 패러미터인 간격은 실시간 위치가 필요한 것이 아니라면 5 ~ 10초 정도로 설정해 주면 됩니다.
지연시간은 기본 값에서 따로 설정하지 않았습니다.
지연 시간
setMaxWaitTime() 메서드를 사용하여 지연 시간을 지정할 수 있습니다. 일반적으로는 setInterval() 메서드에 지정된 간격보다 몇 배 큰 값을 전달합니다. 이 설정은 위치 정보 전달을 지연하고 여러 위치 업데이트를 일괄 전달할 수 있습니다.
이 두 가지 변경사항은 배터리 소모를 최소화하는 데 도움이 됩니다.
앱에서 즉시 위치 정보가 필요하지 않은 경우 setMaxWaitTime() 메서드에 최대한 큰 값을 전달하여 지연 시간을 늘리는 대신 더 많은 데이터를 제공하고 배터리 효율을 높여야 합니다.
지오펜스를 사용할 경우 앱은 setNotificationResponsiveness() 메서드에 큰 값을 전달하여 전력을 절약해야 합니다. 5분 이상의 값을 사용하는 것이 좋습니다.
위 getLastLocation은 이미 반복하고 있어서 별도의 반복 호출이 필요 없습니다.
패러미터로 메인 스레드에서 도는 루프를 넘겨 메인스레드에서 반복 실행하고 있습니다.
MyLocationManager().getLastLocationClient()
val a = Timer()
a.scheduleAtFixedRate(object : TimerTask(){
override fun run() {
Log.d("위도 경도", "${MyLocationManager.currentLatitude} ${MyLocationManager.currentLongitude}")
}
override fun cancel(): Boolean {
return super.cancel()
}
override fun scheduledExecutionTime(): Long {
return super.scheduledExecutionTime()
}
},0, 3000)
Timer로 일정 시간 동안 반복호출 하면 잘 찍히는 걸 확인할 수 있습니다.
requestLocationUpdates는 업데이트를 중단시키기 않으면 계속 반복해서 메모리 누수의 원인이 되므로 사용했다면 중단시켜야 합니다.
fun stopLocationupdate() = fusedLocationClient.removeLocationUpdates(locationCallBack)
현재 위치받기
현재 위치를 받는 함수는 콜백 형식을 취하고 있기 때문에 뷰모델을 받아서 LiveData에 넣어주거나 Intent의 action으로 알리는 방법 등을 선택할 수 있습니다.
fun setCurrentLocationRequest() = CurrentLocationRequest.Builder()
.setDurationMillis(30000)
.setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY)
.build()
fun getCurrentLocation(){
val token = object: CancellationToken(){
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = CancellationTokenSource().token
override fun isCancellationRequested(): Boolean = false
}
fusedLocationClient.getCurrentLocation(setCurrentLocationRequest(), token)
.addOnSuccessListener { location ->
if(location != null) {
Log.d(TAG, "Location is Success")
currentLatitude = location.latitude
currentLongitude = location.longitude
Log.d("위도 경도", "${currentLatitude} ${currentLongitude}")
} else{
Log.d(TAG, "location is null")
}
}
.addOnFailureListener {e ->
Log.d(TAG, "Location is Failed by $e")
}
.addOnCanceledListener {
Log.d(TAG, "Location is Canceled")
}
.addOnCompleteListener {
Log.d(TAG, "Location is Completed")
}
}
코드 전문
class MyLocationManager {
companion object{
var currentLatitude = 0.0
var currentLongitude = 0.0
}
val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(MyApplication.applicationContext())
private val TAG = javaClass.simpleName
private val locationCallBack = object : LocationCallback() {
override fun onLocationAvailability(p0: LocationAvailability){
val a = p0.isLocationAvailable
Log.d(TAG, "Location Availability : $a")
}
override fun onLocationResult(locationResult: LocationResult) {
currentLatitude = locationResult.lastLocation?.latitude ?: 0.00
currentLongitude = locationResult.lastLocation?.longitude ?: 0.00
}
}
fun getLastLocationClient(){
val lastLocationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000).build()
fusedLocationClient.requestLocationUpdates(lastLocationRequest, locationCallBack, Looper.getMainLooper())
}
fun setCurrentLocationRequest() = CurrentLocationRequest.Builder()
.setDurationMillis(30000)
.setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY)
.build()
fun getCurrentLocation(){
val token = object: CancellationToken(){
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = CancellationTokenSource().token
override fun isCancellationRequested(): Boolean = false
}
fusedLocationClient.getCurrentLocation(setCurrentLocationRequest(), token)
.addOnSuccessListener { location ->
if(location != null) {
Log.d(TAG, "Location is Success")
currentLatitude = location.latitude
currentLongitude = location.longitude
Log.d("위도 경도", "${currentLatitude} ${currentLongitude}")
} else{
Log.d(TAG, "location is null")
}
}
.addOnFailureListener {e ->
Log.d(TAG, "Location is Failed by $e")
}
.addOnCanceledListener {
Log.d(TAG, "Location is Canceled")
}
.addOnCompleteListener {
Log.d(TAG, "Location is Completed")
}
}
fun stopLocationupdate() = fusedLocationClient.removeLocationUpdates(locationCallBack)
}
class MyApplication: Application() {
init {
instance = this
}
companion object {
lateinit var instance: MyApplication
fun applicationContext(): Context {
return instance.applicationContext
}
}
}
'안드로이드 > 응용' 카테고리의 다른 글
다국어 지원 (Wear OS) (0) | 2023.11.17 |
---|---|
SingleTon Holder로 편하게 SingleTon 쓰기 (0) | 2023.09.22 |
ApplicationContext 일반 클래스에서 쓰기 (0) | 2023.09.22 |