사용자 위치를 알려주는 api
기기에 있는 다양한 센서를 활용해 기기의 위치를 나타내는 기능이 있음
ex) GPS, 와이파이 이용한 위치 식별
https://developers.google.com/location-context/fused-location-provider?hl=ko
implementation("com.google.android.gms:play-services-location:17.0.0")
or
implementation(libs.play.services.location)
//구글에서 제공하는 사용자 위치 찾기용 변수
private lateinit var mFusedLocationClient:FusedLocationProviderClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
//위치 얻기 초기화
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
binding!!.tvSelectCurrentLocation.setOnClickListener(this)
}
LocationManager 객체를 가져와서 위치 관련 서비스를 제어하고 사용할 수 있도록 설정
getSystemService()
안드로이드 시스템의 다양한 서비스(예: 위치 서비스, 네트워크 서비스, 알림 서비스 등)를 가져오는 메서드
- 여기서는 Context.LOCATION_SERVICE를 전달하여 위치 서비스에 접근
- 반환된 객체는 Object 타입이므로, 이를 LocationManager로 캐스팅해야함
LocationManager
위치 데이터를 관리하고 애플리케이션에서 GPS, 네트워크, 기타 센서를 통해 위치 정보를 제공하는 서비스주요 메서드
1. 현재 위치를 가져오기
특정 제공자로부터 마지막으로 알려진 위치 정보를 가져옴
getLastKnownLocation(String provider)
2. 위치 제공자(GPS, 네트워크)가 활성 상태인지 확인
isProviderEnabled(String provider)
3. 위치 변경사항을 요청 & 위치 업데이트 요청 등록
위치 변경사항을 요청하여 LocationListener를 통해 콜백을 받음
requestLocationUpdates()
4. 위치 업데이트 요청을 제거
removeUpdates(LocationListener listener)
//사용자 위치 권한이 활성화 되어 있는지 확인
private fun isLocationEnabled() : Boolean{
//사용자의 위치를 알고 있는지 또는 사용자의 위치를 알 수 있는지 여부를 알려줌
val locationManager : LocationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|| locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
위치권한 활성화 체크로 분기처리 하여 활성화 되어 있을 시 Dexter 활용해서 위치정보 관련한 퍼미션 체크 후 내 위치 정보 정보 얻어오기
안 되어 있을 시 셋팅 창으로 이동
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onClick(v: View?) {
when(v!!.id) {
R.id.tv_select_current_location -> {
if (!isLocationEnabled()) {
//비활성화
Toast.makeText(this, "위치정보가 비활성화 되어 있습니다", Toast.LENGTH_SHORT).show()
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivity(intent)
} else {
Dexter.withActivity(this).withPermissions(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
).withListener(object : MultiplePermissionsListener{
override fun onPermissionsChecked(p0: MultiplePermissionsReport?) {
//report가 모든 퍼미션이 허용이라고 하는 곳
requestNewUserLocation()
}
//사용자가 허가 안해줌
override fun onPermissionRationaleShouldBeShown(
p0: MutableList<PermissionRequest>?,
p1: PermissionToken?
) {
//허락받기 위한 대화 상자 노출
showRationalDialogForPermission()
}
}).onSameThread() //Dexter 권한 요청 라이브러리에서 사용하는 메서드로,
// 권한 요청 결과 콜백(MultiplePermissionsListener)이
// **현재 스레드(UI 스레드)**에서 실행되도록 보장
.check() // 이 호출이 있어야 권한 요청이 실행됨
}
}
}
}
private fun showRationalDialogForPermission() {
AlertDialog.Builder(this).setMessage(
"이 기능에 필요한 권한이 거절되었습니다. 앱 세팅에서 변경할 수 있습니다"
).setPositiveButton("Go to settings")
{
_,_->
try {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName,null) //앱설정 화면으로 넘어간 뒤 사용자 권한을 바꿀수 있게함
intent.data = uri
startActivity(intent)
} catch (e:ActivityNotFoundException) {
e.printStackTrace()
}
}.setNegativeButton("Cancel"){
dialog, which->
dialog.dismiss()
}.show()
}
FusedLocationProviderClient의 requestLocationUpdates()를 사용해서 사용자 위치 정보 찾아오기
FusedLocationProviderClient.requestLocationUpdates(위치 요청 객체, 위치 데이터 처리하는 콜백, Looper)
1. LocationRequest 객체:
위치 업데이트 요청에 필요한 매개변수를 정의하는 객체입니다.
설정 가능한 주요 옵션:
priority: 위치 정확도 설정.
interval: 위치 업데이트 간격.
numUpdates: 위치 업데이트 요청 횟수(여기선 1회만 요청).
- mFusedLocationClient.requestLocationUpdates():
위치 업데이트를 요청합니다.
인자로 LocationRequest, LocationCallback, 그리고 Looper를 전달합니다.
LocationCallback: 위치 업데이트 데이터를 받을 콜백 객체.
Looper: 위치 업데이트 콜백이 실행될 스레드(여기선 현재 스레드)
- @SuppressLint("MissingPermission"):
위치 권한 관련 경고를 억제합니다.
하지만 실제로는 ACCESS_FINE_LOCATION 또는 ACCESS_COARSE_LOCATION 권한이 필요하므로 요청 전에 확인해야 합니다.
LocationCallBack 콜백 객체
1. LocationCallback 인터페이스:
requestLocationUpdates()로 요청한 위치 업데이트 데이터를 비동기적으로 받는 콜백 클래스
2. onLocationResult(locationResult: LocationResult):
위치 업데이트 결과 수신
LocationResult에서 lastLocation을 호출하여 마지막 위치 정보를 가져옴
//requestLocationUpdates를 사용하면 퍼미션 받으라고 오류표시 남
// -> 이미 requestNewUserLocation() 하기전에 퍼미션을 받았기때문에 어노테이션을 붙여 권한손실 오류 방지
@SuppressLint("MissingPerMission")
private fun requestNewUserLocation() {
//Priority : 우선순위
//PRIORITY_HIGH_ACCURACY - 이 설정을 사용하여 가장 정확한 위치를 요청합니다. 이 설정을 사용하면 위치 서비스가 GPS를 사용하여 위치를 확인할 가능성이 높습니다.
//PRIORITY_LOW_POWER - 이 설정을 사용하여 도시 수준의 정밀도를 요청합니다. 대략 10킬로미터의 정확성입니다. 이는 대략적인 수준의 정확성으로 간주되며 전력을 더 적게 소비할 수 있습니다.
//PRIORITY_PASSIVE - 전력 소비에 별다른 영향을 미치지 않으면서 사용 가능한 경우 위치 업데이트를 수신. 앱에서 위치 업데이트를 트리거하지 않고 다른 앱에서 트리거한 위치를 수신
var mLocationRequest = LocationRequest()
mLocationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
mLocationRequest.interval = 1000 //얼마나 자주 작동할지
mLocationRequest.numUpdates = 1 //한번만 사용
mFusedLocationClient.requestLocationUpdates(mLocationRequest, mLocationCallBack, Looper.myLooper())
}
//사용자 위치 정보 받아오는 콜백함수
private val mLocationCallBack = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
// 마지막 위치 정보를 가져옴
val mLastLocation : Location = locationResult.lastLocation!!
// 위도와 경도를 저장
mLatitude = mLastLocation.latitude
mLongitude = mLastLocation.longitude
Log.d("TAG", "콜백 : $mLatitude & $mLongitude")
}
}
package com.airapssinsj.happyplaceapp.utils
import android.content.Context //앱의 현재 상태 정보를 제공하는 클래스, Geocoder 초기화 시 필요
import android.location.Address //주소 정보를 나타내는 객체
import android.location.Geocoder //위도와 경도를 주소로 변환하거나, 반대로 주소를 위도/경도로 변환할 수 있는 유틸리티 클래스
import android.os.AsyncTask //비동기적으로 작업을 처리하기 위한 클래스
import java.util.Locale //기기의 언어 및 지역 설정 정보를 나타내는 클래스
// 같은 스레드에서 동시에 실행되지않게 하기 위해 AsyncTask 상속받음
class GetAddressFromLatLog(context: Context,
private val latitude:Double,
private val longitude:Double)
: AsyncTask<Void,String,String>(){
//위도와 경도로 주소 가져오는 부분
//1. GeoCoder 설정 : 경도와 위도를 사용해 주소로 생성해 주는 클래스
// Geocoder(Context, 언어설정) / Locale.getDefault() : 기기의 디폴트 언어 사용
private val geocoder : Geocoder = Geocoder(context, Locale.getDefault())
private lateinit var mAddressListener: AddressListener // 주소를 가져온 후 결과를 호출하는 데 사용
//백그라운드에서 실행
override fun doInBackground(vararg params: Void?): String {
//Geocoder는 네트워크 연결 문제나 API 호출 실패 시 예외를 던질 수 있기 때문에 예외처리
try {
//getFromLocation(위도 , 경도, 주소 결과의 최대 개수)
val addressList:List<Address>? = geocoder.getFromLocation(latitude,longitude, 1)
//-> 세번째 매개변수를 1로 둠 : 결과값의 첫 번째 주소만 가져오겠단 얘기
//그 중 첫번째 값을 Address 변수에 저장
if (addressList != null && addressList.isNotEmpty()) {
val address : Address = addressList[0]
//리턴 값에 맞춰 String으로 처리
val sb = StringBuilder()
for (i in 0..address.maxAddressLineIndex) {
//현재 식별된 주소 정보를 사용하기 위해 가장 큰 인덱스 값을 생성해
// 주소가 여러줄이거나 복잡한 정보여도 값을 얻어내기 위함
// address객체가 가진 변수 append하기
sb.append(address.getAddressLine(i)).append(" ")
}
sb.deleteCharAt(sb.length-1) //마지막 공백 지우기
return sb.toString()
}
} catch (e:Exception) {
e.printStackTrace()
}
return ""
}
//onPostExecute : 비동기 작업이 끝난 후 메인(UI) 스레드에서 호출
//비동기 작업 결과(resultString)를 인터페이스를 통해 전달
override fun onPostExecute(resultString: String?) {
super.onPostExecute(resultString)
if (resultString == null) {
//주소 가져오지 못한 경우 처리
mAddressListener.inError()
} else {
// 정상적으로 주소를 가져온 경우 인터페이스를 통해 전달
mAddressListener.onAddressFound(resultString)
}
}
//결과를 전달받을 수 있도록 인터페이스 준비 시키는 초기화 담당
fun setAddressListener(addressListener: AddressListener) {
mAddressListener = addressListener
}
//주소를 가져오는 기능을 가진 함수
//AsyncTask를 실행하기 위한 함수 : 이 함수가 호출되면 비동기 작업이 시작
fun getAddress() {
execute()
}
//실제 기능들을 적용하기 전에 인터페이스를 생성하고 AddressListener라는 인터페이스 사용
//주소 조회 결과를 전달하기 위한 인터페이스
interface AddressListener{
fun onAddressFound(address:String?) //주소가 성공적으로 조회되었을 때 호출
fun inError() //조회 실패 시 호출
}
}
//사용자 위치 정보 받아오는 콜백함수
//받아온 위치정보를 유필 폴더에 만든 GetAddressFromLatLog에 넘겨주어 통해 주소로 받아오기
private val mLocationCallBack = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
val mLastLocation : Location = locationResult.lastLocation!!
mLatitude = mLastLocation.latitude
mLongitude = mLastLocation.longitude
//GetAddressFromLatLog 유틸 실행
val addressTask = GetAddressFromLatLog(this@HappyPlaceActivity, mLatitude, mLongitude)
addressTask.setAddressListener(object : GetAddressFromLatLog.AddressListener{
override fun onAddressFound(address: String?) {
binding!!.etLocation.setText(address)
}
override fun inError() {
Log.e("TAG","주소 얻어오기 실패")
}
})
addressTask.getAddress() //실행
}
}
package com.airapssinsj.happyplaceapp.utils
import android.content.Context
import android.location.Address
import android.location.Geocoder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale
class GetAddressFromLatLng(
context: Context,
private val latitude: Double,
private val longitude: Double
) {
private val geocoder: Geocoder = Geocoder(context, Locale.getDefault())
private lateinit var addressListener: AddressListener
suspend fun fetchAddress() {
// 백그라운드에서 주소 가져오기
val address = withContext(Dispatchers.IO) {
try {
val addressList: List<Address>? = geocoder.getFromLocation(latitude, longitude, 1)
if (!addressList.isNullOrEmpty()) {
val sb = StringBuilder()
val address = addressList[0]
for (i in 0..address.maxAddressLineIndex) {
sb.append(address.getAddressLine(i)).append(" ")
}
sb.deleteCharAt(sb.length - 1) // 마지막 공백 제거
sb.toString()
} else {
null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
// 결과 처리
if (address == null) {
addressListener.onError()
} else {
addressListener.onAddressFound(address)
}
}
fun setAddressListener(listener: AddressListener) {
this.addressListener = listener
}
interface AddressListener {
fun onAddressFound(address: String?)
fun onError()
}
}
val getAddress = GetAddressFromLatLng(context, latitude, longitude)
getAddress.setAddressListener(object : GetAddressFromLatLng.AddressListener {
override fun onAddressFound(address: String?) {
println("Address found: $address")
}
override fun onError() {
println("Failed to fetch address")
}
})
// CoroutineScope를 사용하여 실행
lifecycleScope.launch {
getAddress.fetchAddress()
}
package com.airapssinsj.happyplaceapp.utils
import android.content.Context
import android.location.Address
import android.location.Geocoder
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.util.Locale
class GetAddressWorker(
context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
override fun doWork(): Result {
val latitude = inputData.getDouble("latitude", 0.0)
val longitude = inputData.getDouble("longitude", 0.0)
val geocoder = Geocoder(applicationContext, Locale.getDefault())
return try {
val addressList: List<Address>? = geocoder.getFromLocation(latitude, longitude, 1)
if (!addressList.isNullOrEmpty()) {
val sb = StringBuilder()
val address = addressList[0]
for (i in 0..address.maxAddressLineIndex) {
sb.append(address.getAddressLine(i)).append(" ")
}
sb.deleteCharAt(sb.length - 1)
val addressResult = sb.toString()
// 작업이 성공했음을 알리고 데이터를 반환
val output = workDataOf("address" to addressResult)
Result.success(output)
} else {
Result.failure()
}
} catch (e: Exception) {
e.printStackTrace()
Result.failure()
}
}
}
import androidx.work.*
val workManager = WorkManager.getInstance(context)
// Work 요청 생성
val workRequest = OneTimeWorkRequestBuilder<GetAddressWorker>()
.setInputData(
workDataOf(
"latitude" to latitude,
"longitude" to longitude
)
)
.build()
// Work 실행
workManager.enqueue(workRequest)
// 결과 확인
workManager.getWorkInfoByIdLiveData(workRequest.id).observe(this) { workInfo ->
if (workInfo != null && workInfo.state.isFinished) {
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
val address = workInfo.outputData.getString("address")
println("Address found: $address")
} else {
println("Failed to fetch address")
}
}
}