[Android] Location이 null이라고요..?? 위치 설정 및 위치 권한 요청하기

hxeyexn·2024년 9월 24일
0

😱 Intro

우아한테크코스 론칭 페스티벌에서 발표 직전, 서비스를 시연하고자 했던 기기에서 앱을 실행하자마자 종료되는 일이 생겼다...

로그를 확인해보니 위치 정보가 null이었다.. 위치 권한을 허용한 사용자에 한해서 현 위치를 추적하도록 구현했는 데 대체 왜 Location이 null이죠..?!?!

그 이후로 위치 설정 및 위치 권한 설정 늪에 빠지게 되었는 데... 🫠

그래서 같은 실수를 반복하지 않고자 스타카토 프로젝트에서 진행했던 위치 설정 및 위치 권한 요청과 관련해 정리해보려고 한다!



🧭 위치 설정(GPS)

왜 Location이 null이었을까?

Location이 null이었던 이유는 위치 설정(GPS)이 off 상태였기 때문이었다.

안드로이드에는 아래와 같이 상태바를 내려보면 위치를 on/off 할 수 있는 버튼이 있다.

위치 설정(GPS)이 꺼져있다면 앱에서 사용자의 위치를 얻을 수 없다.
즉, 위치 권한을 요청하기 전 위치 설정(GPS) 여부를 확인하는 작업이 선행되어야 한다.

필자는 위치 설정(GPS) 여부를 먼저 확인하지 않고 위치 권한만 확인했기에, 사용자가 위치 권한을 허용했음에도 Location 값이 null로 반환되었던 것이다.

위치 설정(GPS) 여부 확인하기

위치 설정(GPS) 여부는 어떻게 확인할 수 있을까?

1. LocationRequest 객체 정의

우선 LocationRequest를 만들어야 한다. LocationRequest란 위치 데이터를 수집할 때 사용하는 클래스이다.

이 클래스는 Google의 위치 서비스 API인 FusedLocationProviderClient 와 함께 사용되며, 사용자가 위치를 업데이트 받는 빈도, 정확성, 우선순위 등을 설정할 수 있다.

가능한 가장 정확한 위치를 요청하기 위해 우선순위는 Priority.PRIORITY_HIGH_ACCURACY로 설정했고, 위치 업데이트 요청 간격은 10초(10000L)로 설정했다.

fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
	// 1. LocationRequest 객체 정의
    val locationRequest: LocationRequest = buildLocationRequest()
    ...
}

private fun buildLocationRequest(): LocationRequest =
    // 우선 순위 : Priority.PRIORITY_HIGH_ACCURACY
    // 위치 업데이트 요청 간격 : 10000L
	LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, INTERVAL_MILLIS)
    	// 권한에 따른 정밀도 설정
        // GRANULARITY_PERMISSION_LEVEL : 앱의 위치 권한 수준에 따라 COARSE 또는 FINE 정밀도를 자동으로 선택
		.setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL)
        // 정확한 위치를 기다리도록 설정
		.setWaitForAccurateLocation(true)
	    .build()
    
companion object {
    private const val INTERVAL_MILLIS = 10000L
}

2. 현재 위치 설정 가져오기

사용자 디바이스의 현재 위치 설정 상태를 가져오려면 LocationSettingsRequest.Builder을 생성하고, LocationRequest를 추가해 줘야 한다.

fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
	// 1. LocationRequest 객체 정의
    ...
    
    // 2. 현재 위치 설정 가져오기
    val builder =
        LocationSettingsRequest
            .Builder()
            .addLocationRequest(locationRequest)
}

3. SettingsClient를 사용하여 어떤 설정이 사용되는지 가져오기

SettingsClinet를 사용하면 어떤 설정이 사용되는 지 확인할 수 있다. settingsClientcheckLocationSettings 메서드를 호출하면 위치 설정이 필요한 조건에 맞는지 확인하는 작업(Task<LocationSettingsResponse>)을 요청한다.

Task<LocationSettingsResponse>가 완료되면 LocationSettingsResponse 객체의 상태 코드를 확인하여 위치 설정을 확인 할 수 있다.

fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
	// 1. LocationRequest 객체 정의
    ...
    
    // 2. 현재 위치 설정 가져오기
    ...
    
    // 3. SettingsClient를 사용하여 어떤 설정이 사용되는지 확인
    val settingsClient: SettingsClient = LocationServices.getSettingsClient(activity)
    // task가 완료되면 LocationSettingsResponse 객체의 상태 코드로 위치 설정을 확인할 수 있음
    val locationSettingsResponse: Task<LocationSettingsResponse> =
    	settingsClient.checkLocationSettings(builder.build())
}

4. 위치 설정 여부에 따라 동작 처리하기

위치 설정 여부를 확인했다 설정 여부에 따라 동작을 처리해야 한다.

위치 설정이 off 상태라면 사용자에게 위치 설정을 변경하라는 메시지를 표시해야 한다.
startResolutionForResult()를 호출하여 사용자에게 위치 설정을 수정할 수 있는 권한을 묻는 다이얼로그를 표시할 수 있다.

위치 설정이 on 상태일 때는 위치 권한 설정 여부를 확인하도록 구현했다.

fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
	// 1. LocationRequest 객체 정의
    ...
    
    // 2. 현재 위치 설정 가져오기
    ...
    
    // 3. SettingsClient를 사용하여 어떤 설정이 사용되는지 확인
    ...
    // task가 완료되면 LocationSettingsResponse 객체의 상태 코드로 위치 설정을 확인할 수 있음
    ...
     
    // 4. 위치 설정에 따른 동작 처리         
    locationSettingsResponse.handleLocationSettings(actionWhenHavePermission, activity)
}
private fun Task<LocationSettingsResponse>.handleLocationSettings(
	actionWhenHavePermission: () -> Unit,
    activity: Activity,
) { 
    // 위치 설정 on 상태일 때 처리할 동작
    addOnSuccessListener { actionWhenHavePermission() }
    // 위치 설정 off 상태일 때 처리할 동작
    addOnFailureListener { exception ->
        exception.actionWhenHaveNoPermission(activity)
    }
}

private fun Exception.actionWhenHaveNoPermission(activity: Activity) {
    if (this is ResolvableApiException) {
        // 위치 설정 dialog를 띄움
        startResolutionForResult(
            activity,
            REQUEST_CODE_LOCATION,
        )
    }
}

구현 결과



위치 권한 요청

앞서 위치 설정에 대해 알아보았으니 이제 위치 권한 요청에 대해 알아보자!

🤔 권한 요청은 자주 하는 것이 좋을까?

아니다. 앱 품질을 개선하고 사용자 개인 정보를 보호하기 위해서는 앱의 권한 요청 및 사용을 최소화하는 것이 좋다.

특히 민감한 권한과 API를 사용하려면 데이터 액세스, 수집, 사용 및 공유에 관한 인앱 정보를 공개해야 한다.

권한 요청 최소화

  • 실행 권한을 반드시 선언해야 하는 경우에는 항상 사용자의 결정을 존중하고 앱 환경을 단계적으로 저하하는 방법을 제공해야 한다.
  • 앱에 위치 정보가 자주 필요하지 않거나 한 번만 필요한 경우 사용자에게 주소나 우편번호를 대신 입력하도록 요청하는 것이 좋다.

권한 거부 처리

🙅‍♂️ 사용자가 권한 요청을 거부했다면?

사용자가 권한 요청을 거부했다면 앱은 권한이 없어서 작동하지 않는 기능을 사용자가 이해하도록 지원해야 한다.

어떻게 지원할 수 있을까?

  1. 앱에 필요한 권한이 없어서 기능이 제한된 앱 UI의 특정 부분을 강조표시해야 한다.
    : 기능의 결과나 데이터가 나타난 메시지를 표시
    : 오류 아이콘과 색상이 포함된 다른 버튼을 표시
  2. 자세히 설명해야 한다.
    : 앱에 필요한 권한이 없어서 어떤 기능을 사용할 수 없는지 교육용 UI 등을 활용해 명확히 설명해야 한다.
  3. 사용자 인터페이스 차단해서는 안된다.
    : 사용자가 앱을 계속 사용하는 것을 막는 전체 화면 경고 메시지를 표시하면 안된다.
  4. 앱은 권한을 거부하겠다는 사용자의 결정을 존중해야 한다.

정확성

앱에 대략적인 위치만 필요한 경우 정확한 위치 정보(ACCESS_FINE_LOCATION) 를 제외하고 대략적인 위치 정보(ACCESS_COARSE_LOCATION)만을 요청하는 것이 좋다.

대략적인 위치(ACCESS_COARSE_LOCATION)란?

  • 기기 위치 추정치를 제공해준다.
  • LocationManagerService 또는 FusedLocationProvider에서 가져온 경우 이 위치 추정치의 오차 범위는 약 3제곱킬로미터 이내이다.
  • ACCESS_COARSE_LOCATION 권한만 선언하면 앱은 이 수준의 정확도로 위치를 수신할 수 있다.

정확한 위치(ACCESS_FINE_LOCATION)란?

  • 최대한 정확한 기기 위치 추정치를 제공한다.
  • LocationManagerService 또는 FusedLocationProvider에서 가져온 경우 이 위치 추정치의 오차 범위는 일반적으로 약 50미터 이내이며 몇 미터 이내 또는 그 이상으로 정확할 때도 있다.
  • ACCESS_FINE_LOCATION 권한을 선언하면 앱이 이 수준의 정확도로 위치를 수신할 수 있다.
  • 기기 위치의 정확한 추정치가 필요한 상황은 ACCESS_FINE_LOCATION 권한을 선언해도 되는 유일한 경우다.

스타카토 프로젝트에서 위치 권한 요청 처리

아래 1️⃣~4️⃣까지의 시나리오를 모두 만족할 수 있도록 처리했다!

1️⃣ 앱 다운로드 후 첫 실행

  • 시스템 권한 창을 이용해 위치 권한을 요청한다.
    • 위치 권한을 허용했을 때
      → 현 위치 기능을 활성화해야 한다.
    • 위치 권한을 거부했을 때
      → 현 위치 기능을 활성화하지 않는다.
      → 특정 권한이 필요한 이유가 담긴 위치 권한 설정 다이얼로그(교육용 UI)를 사용자에게 표시해야 한다.

2️⃣ 앱 재 접속

  • 위치 권한을 허용했을 때
    → 현 위치 기능을 활성화해야 한다.
  • 위치 권한을 거부했을 때
    → 현 위치 기능을 활성화하지 않는다.
    → 특정 권한이 필요한 이유가 담긴 위치 권한 설정 다이얼로그(교육용 UI)를 사용자에게 표시해야 한다.

3️⃣위치 권한 설정 다이얼로그(교육용 UI)에서 취소 버튼을 눌렀을 때

  • 위치 권한을 활성화하지 않는다.
  • 위치 권한 설정을 위해 설정으로 이동 했을 때
    • 위치 권한을 허용한 후 앱으로 돌아왔을 때
      • 현 위치 기능을 활성화해야 한다.
    • 위치 권한을 거부한 후 앱으로 돌아왔을 때
      • 현 위치 기능을 활성화하지 않는다.
      • 위치 권한 설정 다이얼로그(교육용 UI)를 사용자에게 표시해야 한다.
    • 위치 권한이 거부된 채로 유지했을 때
      • 현 위치 기능을 활성화하지 않는다.
      • 위치 권한 설정 다이얼로그(교육용 UI)를 사용자에게 표시하지 않는다.
  • 앱을 종료하지 않은 채로 다른 앱(설정 제외)으로 이동 후 다시 돌아왔을 때
    • 위치 권한 설정 다이얼로그(교육용 UI)는 사용자에게 표시되지 않는다.

4️⃣위치 권한 설정 다이얼로그(교육용 UI)에서 설정 버튼을 눌렀을 때

  • 설정으로 이동한다.
    • 위치 권한을 허용한 후 앱으로 돌아왔을 때
      • 현 위치 기능을 활성화해야 한다.
    • 위치 권한을 거부한 후 앱으로 돌아왔을 때
      • 현 위치 기능을 활성화하지 않는다.
      • 위치 권한 설정 다이얼로그(교육용 UI)를 사용자에게 표시해야 한다.

코드 설명

  private fun enableMyLocation() {
      when {
          // 앱에 이미 권한이 부여 되었는 지 확인
          checkSelfLocationPermission() -> {
              // 권한이 부여 되었을 때 수행되어야하는 동작
          }
          // ContextCompat.checkSelfPermission() 메서드가 PERMISSION_DENIED 을 반환하면
          // shouldShowRequestPermissionRationale()를 호출
          // true를 반환하면 교육용 UI를 사용자에게 표시
          // 1. UI에서 사용자가 사용 설정하려는 기능에 특정 권한이 필요한 경우를 설명
          // 2. 앱이 위치, 마이크, 카메라와 관련된 권한을 요청하는 경우 이 정보에 앱이 액세스 해야 하는 이유를 설명
          shouldShowRequestLocationPermissionsRationale() -> {
              // 권한을 명시적으로 거부한 경우 true
              // 처음보거나, 다시묻지 않음을 선택한 경우 false
              observeIsLocationDenial { showLocationRequestRationaleDialog() }
          }
          // 사용자에게 교육용 UI가 표시되거나 shouldShowRequestPermissionRationale()의 반환 값에서
          // 교육용 UI를 표시하지 않아도 된다고 나타내면 권한을 요청
          else -> {
              observeIsLocationDenial { requestPermissionLauncher.launch(locationPermissions) }
          }
      }
  }
    
  // RequestPermission 계약 -> 시스템이 권한 요청 코드를 관리하도록 허용
  // RequestPermission() -> 단일 권한 요청
  // RequestMultiplePermissions() -> 여러 권한을 동시에 요청
  private fun initRequestPermissionsLauncher() =
      registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
          ...
      }

적용된 교육용 UI



📚 참고자료

Android Docs: 위치 정보 액세스 권한 요청
Android Docs: 런타임 권한 요청
안드로이드 권한 관련 이야기
위치 서비스(GPS) 기능 설정 팝업
Android Request To Turn On GPS Pragmatically

profile
Android Developer

0개의 댓글