본문 바로가기

android

Location 기능 사용 시 워크플로우

(1) 위치 액세스 권한 흐름

① Manifest 파일에 ACCESS_COARSE_LOCATION(대략적 위치) 과 ACCESS_FINE_LOCATION(정확한 위치) 권한을 선언합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.antique_boss.location_workflow">

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

 

② 사용자가 위치 정보에 액세스해야 하는 기능(예: 배달앱의 초기 주소 설정 등)을 실행할 때 권한이 존재하는지 검사 후 없다면 사용자에게 런타임 권한 요청을 합니다. 만약 앱에서 정밀한 위치 정보가 필요한 경우에는 ACCESS_COARSE_LOCATION 과 ACCESS_FINE_LOCATION 을 함께 요청해야 합니다. 만약 ACCESS_FINE_LOCATION 만 요청하는 경우 Android 12 이상을 타겟팅하는 경우 "ACCESS_FINE_LOCATION must be requested with ACCESS_COARSE_LOCATION." 오류 메시지가 기록됩니다. 반면에 앱에서 대략적 위치 정보만으로 충분할 경우 ACCESS_COARSE_LOCATION 만 요청합니다.

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.appcompat.widget.AppCompatButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_margin="10dp"
            android:text="현재위치"
            
            //사용자가 버튼을 누름으로써 위치 정보 획득을 위한 일련의 작업이 시작됩니다.
            android:onClick="@{() -> activity.checkLocationPermissions()}" 
            android:backgroundTint="@color/white"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

 

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var requestLocationPermissionsLauncher: ActivityResultLauncher<Array<String>>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.activity = this
        binding.lifecycleOwner = this
        initPermissionLauncher()
    }

    private fun initPermissionLauncher() {
        requestLocationPermissionsLauncher = registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions()
        ) { permissions ->
            when {
                permissions.getValue(Manifest.permission.ACCESS_FINE_LOCATION) -> {
                    //사용자가 정밀한 위치 권한을 앱에 부여함
                }
                
                permissions.getValue(Manifest.permission.ACCESS_COARSE_LOCATION) -> {
                    //사용자가 대략적 위치 권한을 앱에 부여함
                }

                else -> {
                    //사용자가 위치 권한을 거부함
                }
            }
        }
    }

    fun checkLocationPermissions() {
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
                ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            //TODO 이미 권한이 존재하므로 위치 정보 업데이트를 진행합니다.
        } else {
            requestLocationPermissionsLauncher.launch(arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ))
        }
    }
}

 

(2) 위치 설정 확인 흐름

① 앱에서 위치를 업데이트하기 위해서는 위치 설정이 켜져있어야 합니다. 위치 업데이트를 하기전에 우선 위치 설정이 켜져있는지 여부를 먼저 확인해야 합니다. 

 

② 사용자가 위치 정보에 액세스하는 기능을 실행한 경우, 첫번째로 위치 액세스 권한(예: ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)이 있는지 확인합니다. 만약 권한이 없는 경우 런타임 권한을 요청합니다. 그리고 두번째로 사용자 기기의 위치 설정이 켜져있는지 확인합니다. 만약 위치 설정이 꺼져 있는 경우 정상적으로 위치를 업데이트하지 못하기 때문에 사용자에게 위치 설정을 켜달라고 요청해야 합니다.

 

③ 아래의 코드와 스크린샷은 사용자에게 위치 설정을 켜달라고 요청할 때 어떤 순서로 작업해야 하는지에 대한 흐름을 보여줍니다.

 

 

⑴ 기기의 위치 설정이 꺼져있는 경우 사용자에게 위치 설정을 켜달라고 요청할 때 사용할 Launcher 를 설정합니다.

    private fun initResolutionLauncher() {
        resolutionLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
            when(it.resultCode) {
                RESULT_OK -> {
                    Snackbar.make(binding.root, "LOCATION SETTINGS ON", Snackbar.LENGTH_SHORT).show()
                }
                else -> {
                    Snackbar.make(binding.root, "LOCATION SETTINGS OFF", Snackbar.LENGTH_SHORT).show()
                }
            }
        }
    }

 

 

⑵ 앱에 위치 액세스 권한이 있는지 확인합니다. 만약 권한이 없다면 권한을 요청하며 권한이 있다면 위치 설정을 확인합니다.

fun checkLocationPermissions() {
    if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
            ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
        //TODO 위치 액세스 권한이 있으므로 권한 요청 없이 위치 설정이 켜져있는지 확인합니다.
        checkLocationSettings()
    } else {
        requestLocationPermissionsLauncher.launch(arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        ))
    }
}

 

 

⑶ Google Play Service 를 이용하여 위치 설정이 켜져있는지 확인합니다. 만약 위치 설정이 켜져있지 않다면 ResolvableApiException 이 발생합니다. (위치 설정 확인 시 Callback 을 사용하지 않고 Coroutine 을 사용하기 위해 아래의 의존성을 먼저 추가해야 합니다.)

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'

 

private fun checkLocationSettings() {
    CoroutineScope(Dispatchers.Main).launch {
        //TODO 현재 기기의 위치 설정을 받습니다.
        val locationRequest = LocationRequest.create().apply {
            interval = 5000
            priority = Priority.PRIORITY_HIGH_ACCURACY
        }
        val locationSettingsRequest = LocationSettingsRequest.Builder()
            .addLocationRequest(locationRequest)
            .build()

        try {
            LocationServices.getSettingsClient(this@MainActivity)
                .checkLocationSettings(locationSettingsRequest)
                .await()
            //TODO 위치 설정이 켜져있으므로 위치 업데이트를 요청합니다

        } catch (e : ResolvableApiException) {
            //TODO 위치 설정이 꺼져있으므로 사용자에게 위치 설정을 켜달라고 요청합니다.
            resolutionLauncher.launch(IntentSenderRequest.Builder(e.resolution).build())
        } catch (sendEx: IntentSender.SendIntentException) { }
    }
}

 

 

⑷ 기존에 위치 액세스 권한이 없었지만 사용자에 의해 권한이 부여된 경우에도 위치 설정이 켜져있는지 확인하도록 코드를 수정합니다.

private fun initPermissionLauncher() {
    requestLocationPermissionsLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        when {
            permissions.getValue(Manifest.permission.ACCESS_FINE_LOCATION) -> {
                //사용자가 정밀한 위치 권한을 앱에 부여함
                Snackbar.make(binding.root, "ACCESS_FINE_LOCATION GRANTED", Snackbar.LENGTH_SHORT).show()
                checkLocationSettings()
            }
            permissions.getValue(Manifest.permission.ACCESS_COARSE_LOCATION) -> {
                //사용자가 대략적 위치 권한을 앱에 부여함
                Snackbar.make(binding.root, "ACCESS_COARSE_LOCATION GRANTED", Snackbar.LENGTH_SHORT).show()
                checkLocationSettings()
            }
            else -> {
                //사용자가 위치 권한을 거부함
                Snackbar.make(binding.root, "PERMISSIONS DENIED", Snackbar.LENGTH_SHORT).show()
            }
        }
    }
}

 

 

(3) 기기 위치 업데이트 흐름

① 앱에 위치 액세스 권한이 존재하고, 위치 설정이 켜져있는 경우 FusedLocationProvider 를 이용하여 위치 정보를 업데이트할 수 있습니다.

 

② 아래의 코드는 기기의 위치 정보를 한번 가져온 후 더 이상 업데이트를 하지 않도록 하는 코드입니다.

 

⑴ 위치 설정이 이미 켜져있는 경우, 위치 정보를 업데이트하는 메서드를 호출하도록 코드를 수정합니다.

private fun checkLocationSettings() {
    CoroutineScope(Dispatchers.Main).launch {
        //TODO 현재 기기의 위치 설정을 받습니다.
        locationRequest = LocationRequest.create().apply {
            interval = 5000
            priority = Priority.PRIORITY_HIGH_ACCURACY
        }
        val locationSettingsRequest = LocationSettingsRequest.Builder()
            .addLocationRequest(locationRequest)
            .build()

        try {
            LocationServices.getSettingsClient(this@MainActivity)
                .checkLocationSettings(locationSettingsRequest)
                .await()
            //TODO 위치 설정이 켜져있으므로 위치 업데이트를 요청합니다
            updateLocation()
        } catch (e : ResolvableApiException) {
            //TODO 위치 설정이 꺼져있으므로 사용자에게 위치 설정을 켜달라고 요청합니다.
            resolutionLauncher.launch(IntentSenderRequest.Builder(e.resolution).build())
        } catch (sendEx: IntentSender.SendIntentException) { }
    }
}

 

 

⑵ 기기의 위치 설정이 켜져있지 않아 사용자에게 위치 설정을 켜달라고 요청한 후 정상적으로 위치 설정이 켜진 경우, 위치 정보를 업데이트하도록 코드를 수정합니다.

private fun initResolutionLauncher() {
    resolutionLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
        when(it.resultCode) {
            RESULT_OK -> {
                Snackbar.make(binding.root, "LOCATION SETTINGS ON", Snackbar.LENGTH_SHORT).show()
                updateLocation()
            }
            else -> {
                Snackbar.make(binding.root, "LOCATION SETTINGS OFF", Snackbar.LENGTH_SHORT).show()
            }
        }
    }
}

 

 

⑶ 위치 정보를 업데이트하기 위해 첫번째로 FusedLocationProvider 에 요청할 Client 객체를 가져옵니다.

그리고 두번째로 FusedLocationProvider 에서 정상적으로 위치 정보를 전달해줬을 때 호출할 LocationCallback 객체를 생성합니다. 마지막으로 requestLocationUpdates 메서드를 호출하여 FusedLocationProvider 에게 위치 정보 업데이트를 요청합니다.

@SuppressLint("MissingPermission")
private fun updateLocation() {
    fun createLocationCallback(): LocationCallback {
        return object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                for(location in locationResult.locations) {
                    Snackbar.make(binding.root, "latitude: ${location.latitude}, longitude: ${location.longitude}", Snackbar.LENGTH_SHORT).show()
                    break
                }
                fusedLocationProviderClient.removeLocationUpdates(this)
            }
        }
    }

    fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
    fusedLocationProviderClient.requestLocationUpdates(
        locationRequest,
        createLocationCallback(),
        Looper.getMainLooper()
    )
}

 

(4) 요약

지금까지 developer.android.com 에 있는 사용자 위치 가이드라인 문서를 읽고 구현했습니다. 앱에 사용자 위치 정보가 필요한 경우 위의 워크플로우에 따라 사용자 위치 정보를 얻을 수 있습니다. 다음에는 위 코드를 리팩토링하여 Activity 또는 Fragment 를 가볍게 만들 예정입니다.

'android' 카테고리의 다른 글

Custom View onMeasure  (0) 2022.10.29
Android 권한  (0) 2022.10.27
Firebase Ream-Time Database Paging 처리  (1) 2022.10.25
DialogFragment width 조정  (0) 2022.10.22
Frgament 통신  (0) 2022.10.12