참고링크 : https://developer.android.com/training/location/geofencing#kotlin

개요

Android 에서는 Geofencing 기능을 통해 현재 위치가 특정 위치 반경에 진입/머물기/이탈 등의 이벤트를 처리 가능합니다.

단말 유저 당, 앱 당 최대 100개의 geofence 를 사용할 수 있습니다.

You can have multiple active geofences, with a limit of 100 per app, per device user.

Location Services에 geofence 를 위한 이벤트들을 요청해놓을 수 있습니다.

요청한 변경들이 감지되면 BroadcastReciever 등을 통해 처리가 가능합니다.

  • 반경 진입
  • 반경 이탈
  • 반경 진입 후 지정한 시간 동안 이탈 안함
  • geofence 만료 시간 (만료 시간 후 Location Service는 geofence를 알아서 지웁니다.)
항목 설명
최대 등록 수 100 per app, per device user
만료시간 Geofence 마다 millisecond 로 지정.
NEVER_EXPIRE 옵션 지원.
시간 지정 후 만료시 자동으로 Geofence 제거됨.
영역 지정 Circle 형태로 등록 되며, 위경도와 반경(m) 지정.
최상의 결과는 반경 100~150m 사이로 설정해야 함.
Wi-FI 사용 가능시 20~40m 까지 가능.
실내 위치 사용시는 5m 정도로 작을 수도 있음.
Geofence 내부 위치를 알수 없다면 Wi-Fi 정확도는 약 50m라고 가정함.
Wi-Fi 위치 사용할 수 없다면 수백m ~ 수Km 편차 생길수도 있음.
머물기 시간 지정 Geofence 진입 후 머물기로 체크할 시간을 millisecond로 지정.
Geofence 감지할 변경 타입 진입 (GEOFENCE_TRANSITION_ENTER)
이탈 (GEOFENCE_TRANSITION_EXIT)
머뭄 (GEOFENCE_TRANSITION_DWELL)
응답시간 지정 응답 시간이 큰 경우 전력을 크게 절약하고
응답 시간을 낮춘다고 바로 알림을 받지는 않음.
Geofence 추가시 초기 알림 설정 진입 (INITIAL_TRIGGER_ENTER)
이탈 (INITIAL_TRIGGER_EXIT)
머뭄 (INITIAL_TRIGGER_DWELL)
Geofence 사용 이유 설명 앱의 백그라운드 위치에 액세스하므로 사용자에게 권한 설명 필요.
Geofence 재등록이 필요없는 경우 Google Play 서비스 업그레이드나 리소스 제한으로 종료 후 재시작시
Location 프로세스 충돌시
Geofence 재등록이 필요한 경우 단말 재부팅시 부팅 완료 시점을 받아 다시 등록해야 함.
앱이 제거 후 재 설치시
앱 데이터 삭제시
Google Play 서비스 데이터 삭제시
앱에 GEOFENCE_NOT_AVAILABLE 알림 수신시
(일반적으로 Android's Network Location Provider 비활성화시 발생함)
Geofence 진입이 잘 동작하지 않는 경우 (GEOFENCE_TRANSITION_ENTER) Geofence 정확한 위치를 사용할 수 없거나 반경이 너무 작은 경우
Wi-Fi가 꺼져 있는 경우 (SettingsClient를 사용해 장치 설정 체크 필요)
- 참고로 Android 10에서는 WifiManager.setEnabled() 호출 불가하므로 설정 패널 사용.
- 시스템 앱이나 Device policy controller 에서는 가능.
Geofence 내 안정적인 네트워크 연결이 없을 경우
단순히 알림이 늦는 경우.
- 지속적으로 위치 쿼리하는 형식이 아니라 대기 시간이 필요함.
- 일반적인 대기 시간은 2 분 미만이며 장치가 움직일 때는 훨씬 적음.
- 백그라운드 위치 제한 적용시 대기 시간은 평균 약 2~3분 정도.
- 단말이 장시간 정지된 경우 대기 시간이 증가할 수 있음. (최대 6분)

 

Android App 샘플 만들기

1. 권한 추가

먼저 권한을 아래와 같이 추가 후 앱에서 permission 체크 및 요청 로직을 추가합니다.

ACCESS_BACKGROUND_LOCATION 권한은 Android 10 부터 필요합니다.

App 권한 요청 처리

Background 위치 접근 권한 처리

AndroidManifest.xml

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

    <!-- Required if your app targets Android 10 (API level 29) or higher -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    ...
</manifest>

MainActivity.kt

    private val MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION = 100
    private val MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION = 101

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        checkPermission()
    }

    override fun onRequestPermissionsResult(requestCode: Int,
                                            permissions: Array<String>, grantResults: IntArray) {
        when (requestCode) {
            MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION,
            MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION -> {
                grantResults.apply {
                    if (this.isNotEmpty()) {
                        this.forEach {
                            if (it != PackageManager.PERMISSION_GRANTED) {
                                checkPermission()
                                return
                            }
                        }
                    } else {
                        checkPermission()
                    }
                }
            }
        }
    }

    private fun checkPermission() {
        val permissionAccessFineLocationApproved = ActivityCompat
            .checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
                PackageManager.PERMISSION_GRANTED

        if (permissionAccessFineLocationApproved) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                val backgroundLocationPermissionApproved = ActivityCompat
                    .checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
                        PackageManager.PERMISSION_GRANTED

                if (!backgroundLocationPermissionApproved) {
                    ActivityCompat.requestPermissions(
                        this,
                        arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                        MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION
                    )
                }
            }
        } else {
            ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION
            )
        }
    }

2. dependency 추가

Location Service 및 Geofence 사용을 위해서 location 라이브러리를 추가합니다.

app/build.gradle

dependencies {
    ...
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    ...
}

3. Geofence List 추가

테스트 용으로 추가할 Geofence 데이터를 생성합니다.

Geofence 객체 생성시 여러가지 설정을 할 수 있습니다.

ex) 위치 및 반경, 어떤 이벤트를 걸건지, 만료나 머무는 체크 시간을 얼마나 할지 등등

MainActivity.kt

    val geofenceList: MutableList<Geofence> by lazy {
        mutableListOf( 
            getGeofence("현대백화점", Pair(37.5085864,127.0601149)),
            getGeofence("삼성역", Pair(37.5094518,127.063603))
        )
    }

    private fun getGeofence(reqId: String, geo: Pair<Double, Double>, radius: Float = 100f): Geofence {
        return Geofence.Builder()
            .setRequestId(reqId)    // 이벤트 발생시 BroadcastReceiver에서 구분할 id
            .setCircularRegion(geo.first, geo.second, radius)    // 위치 및 반경(m)
            .setExpirationDuration(Geofence.NEVER_EXPIRE)        // Geofence 만료 시간
            .setLoiteringDelay(10000)                            // 머물기 체크 시간
            .setTransitionTypes(
                Geofence.GEOFENCE_TRANSITION_ENTER                // 진입 감지시
                        or Geofence.GEOFENCE_TRANSITION_EXIT    // 이탈 감지시
                        or Geofence.GEOFENCE_TRANSITION_DWELL)    // 머물기 감지시
            .build()
    }

4. Geofence Client 생성

Location API 사용을 위하여 Geofencing Client 인스턴스를 생성해야 합니다.

MainActivity.kt

    private val geofencingClient: GeofencingClient by lazy {
        LocationServices.getGeofencingClient(this)
    }

5. Geofencing Request 빌드

Geofence 지정 및 관련 이벤트 트리거 방식을 설정하기 위해 GeofencingRequest 를 빌드합니다.

MainActivity.kt

    private fun getGeofencingRequest(list: List<Geofence>): GeofencingRequest {
        return GeofencingRequest.Builder().apply {
            // Geofence 이벤트는 진입시 부터 처리할 때
            setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)                
            addGeofences(list)    // Geofence 리스트 추가
        }.build()
    }

6. Broadcast Receiver 추가

Geofencing 변경 이벤트를 받을 BroadcastReceiver 를 추가 후 등록해줍니다.

GeofenceBroadcastReceiver.kt

class GeofenceBroadcastReceiver: BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        val geofencingEvent = GeofencingEvent.fromIntent(intent)
        if (geofencingEvent.hasError()) {
            val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
            Log.e("GeofenceBR", errorMessage)
            return
        }

        // Get the transition type.
        val geofenceTransition = geofencingEvent.geofenceTransition    // 발생 이벤트 타입

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
        geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {

            // Get the geofences that were triggered. A single event can trigger
            // multiple geofences.
            val triggeringGeofences = geofencingEvent.triggeringGeofences

            val transitionMsg = when(geofenceTransition) {
                Geofence.GEOFENCE_TRANSITION_ENTER -> "Enter"
                Geofence.GEOFENCE_TRANSITION_EXIT -> "Exit"
                else -> "-"
            }
            triggeringGeofences.forEach {
                Toast.makeText(context, "${it.requestId} - $transitionMsg", Toast.LENGTH_LONG).show()
            }

        } else {
            Toast.makeText(context, "Unknown", Toast.LENGTH_LONG).show()
        }
    }
}

AndroidManifest.xml

<application... >
    ...
    <receiver android:name=".GeofenceBroadcastReceiver"/>
</application>

MainActivity.kt

    private val geofencePendingIntent: PendingIntent by lazy {
        val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
        PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
    }

7. Geofence Request 및 Callback 추가

Geofencing Client에 Geofence 정보 및 트리거 방식을 가지고 있는 Geofencing Request과 이벤트 발생시 처리할 Broadcast Receiver를 추가해줍니다.

MainActivity.kt

private fun addGeofences() {
    geofencingClient.addGeofences(getGeofencingRequest(geofenceList), geofencePendingIntent).run {
        addOnSuccessListener {
            Toast.makeText(this@MainActivity, "add Success", Toast.LENGTH_LONG).show()
        }
        addOnFailureListener {
            Toast.makeText(this@MainActivity, "add Fail", Toast.LENGTH_LONG).show()
        }
    }
}

준비는 끝났습니다. 앱을 실행해보아 동작을 확인합니다.

+ Recent posts