* 이번에는 Location Manager를 이용해서 현재 사용자의 위치를 측정하는 방법을 공부해보자.
* Location 측정 방법
: 현재 안드로이드 상에서는 다양한 방법으로 사용자의 위치를 측정하고 있다. GPS는 가장 정확하지만 실외에서만 제대로 작동하고, 배터리 소모가 심각하고, 사용자가 원하는만큼 빠르게 위치를 계산하지 못한다. 또 다른 방법으로는 안드로이드의 네트워크 위치 프로바이더를 이용하는 것인데, 통신사의 cell tower와 와이파이 신호의 위치를 통해서 실내와 실외 모두에서 측정가능한 방법인데다 응답고 빠르고 베터리 소모가 심하지 않지만, 정확성이 조금 떨어지는 단점이 있다. 따라서 사용자의 위치를 구할 때에는 GPS를 사용하거나 네트워크 위치 플바이더를 사용하거나 둘다 사용할수도 있다.
* 사용자 위치 측정의 어려움
: 모바일 단말에서 사용자의 위치를 측정하는 것은 다소 어려운 문제가 될수도 있다. 워낙에 다양한 상황으로 인해 위치의 측정에 오류가 발생하는 것이 빈번하기 때문이다. 다음의 사항들이 위치 측정에 있어서 어려운 점들이다.
- 다양한 위치 정보 소스: GPS와 cell-ID 그리고 Wi-Fi를 이용해서 사용자의 위치를 측정한다고 치면 어느것을 사용해야할지 정확성, 속도, 배터리 소모의 관점에서 선택을 하는 것이 중요하다.
- 사용자의 움직임: 사용자의 위치가 계속 변화되기 때문에, 사용자의 움직임을 이용해서 사용자의 위치를 추정해야하는 경우가 빈번하게 발생한다.
- 정확성: 각 위치 정보 소스로부터 오는 정보를 기반으로 하는 위치 측정은 항상 일정한 정확성을 유지하고 있지 않다. 10초전에 측정한 위치가 최근에 측정한 위치보다 더 정확한 위치를 표시하고 있을지도 모른다.
: 이러한 어려움들이 신뢰성있는 사용자의 위치 측정을 어렵게 만들고 있고, 앱에 따라서 다양한 방법들을 통해 정확성과 응답성 등을 보장해야하는 방법을 잘 선택해야 할 것이다.
* 위치 정보 갱신 요청
: 위의 어려움들을 해결하기 전에 일단 안드로이드에서 위치를 가져오는 방법을 살펴보자. 사용자의 위치 정보를 가져오는 것은 콜백 형식으로 이루어진다. 위치 정보를 얻고자 LocationManager에게 requestLocationUpdates() 함수를 호출함으로써 요청을 하게 되고, LocationListener를 인자로 넘겨줘서 콜백 함수로서 사용하게 된다. LocationListener 함수는 몇개의 콜백 함수들을 구현해야할 것이고, LocationManager에서는 사용자의 위치 정보가 변경되었을 때 이 콜백 함수들을 호출하게 된다.
: 예를 들면 다음의 코드에서 LocationListener를 어떻게 정의하고 위치 정보를 요청하는지 알수 있을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| LocationManager locationManager = (LocationManager) this .getSystemService(Context.LOCATION_SERVICE); LocationListener locationListener = new LocationListener() { @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } @Override public void onLocationChanged(Location location) { Log.d( "Location" , location.toString()); } }; locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0 , 0 , locationListener); |
: 여기서 requestLocationUpdates의 첫번째 인자는 위치 프로바이더의 종류로 LocationManager.NETWORK_PROVIDER인 경우는 cell tower와 WiFi를 기반으로 사용자의 위치를 측정하게 된다. 두번째 인자는 위치 정보를 다음에 다시 가져오기 위한 최소의 시간을 나타내고, 세번째는 위치 정보를 다시 가져오기 위한 최소의 거리차를 입력하면 된다. 둘다 0으로 입력한다면 정보 업데이트가 가능할때마다 위치 정보를 업데이트 하겠다는 것이다. 마지막 인자는 위에서 작성한 LocationListener를 인자로 넘겨주면 된다.
: GPS를 이용해서 정보를 받고자 한다면 첫번째 인자의 NETWORK_PROVIDER를 GPS_PROVIDER로 바꿔주면 된다. 둘다 사용하고자 한다면 requestLocationUpdates를 한번은 NETWORK_PROVIDER로, 한번은 GPS_PROVIDER로 각각 호출하면 둘다 이용하게 된다.
* 위치 정보 프로바이더를 이용하기 위한 권한 부여
: NETWORK_PROVIDER와 GPS_PROVIDER로부터 정보를 받기 위해서는 ACCESS_COARSE_LOCATION이다 ACCESS_FINE_LOCATION 사용을 허용해줘야한다. AndroidManifest.xml 파일에서 <manifest> 태그 안에다가 사용 permission을 추가해주자.
<manifest ... > <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> ... </manifest>
: 위의 사용 허가는 ACCESS_FINE_LOCATION은 NETWORK_PROVIDER와 GPS_PROVIDER 둘다 이용할 수있도록 허가하는 것이고, ACCESS_COARSE_LOCATION은 NETWORK_PROVIDER만을 허용하도록 하는 설정이다.
: 이렇게 설정하면 위치 정보를 가져오는 것은 끝난 것이다. 한번 테스트를 해보면 에뮬레이터상이라 위치 정보를 가져오지 않고 있는 것을 알 수 있을 것이다. 안드로이드 폰을 디버그모드로 놓고 폰에서 직접 돌린다면 쉽게 테스트가 가능하겠지만 에뮬레이터상에서는 어쩔수 없이 직접 위치 정보를 보내야한다. 그럴려면 Eclipse상의 DDMS 화면을 열어야한다.
* 에뮬레이터로 위치 정보 시뮬레이션하기
: 일단 앱을 실행한 상태에서 Window > Open Perspective > Other... 를 하거나 오른쪽 위에 JAVA, 또는 JAVA EE 등과 같은 탭 왼쪽에 + 표시가 있는 창포양의 아이콘을 클릭하면 아래와 같이 여러 가지 화면 설정이 나오는데 DDMS를 선택한다.
: 그럼 아래와 같이 에뮬레이터를 여러 가지로 디버그, 또는 컨트롤할 수 있는 화면이 나오는데 우측 상단의 Emulator Controls을 누르면 아래쪽에 Location Controls가 있다.
: 컴퓨터에 붙어있는 GPS에서 자동으로 잡아줬는지는 몰라도 값이 자동으로 들어가있다. 값이 들어가 있지 않다면 적당한 값을 입력하고 "Send" 버튼을 누르자. Send 버튼을 누르면 requestLocationUpdates 함수가 갱신이 되어 설정해놓은 LocationListener의 콜백 함수가 호출이 될 것이다.
: 그 결과 위와 같이 Location이 출력되는 것을 볼 수 있다. 이렇게 하면 사용자의 위치를 알 수 있게 된다. 하지만 여기까지만 한다면 그것은 사용자를 생각하는 앱이 아니게 될 것이다. 왜냐하면 위에서 언급했던 여러 가지 어려움들에 대한 고민을 전혀 안했기 때문에 사용자를 위해 한번 쯤 다양한 고민들을 할 필요가 있을 것이다.
* 위치 정보 접근을 위한 최적화 모델
: 이제 사용자의 위치를 이용하는것은 어플리케이션의 기본적인 기능이 되었음에도 위의 어려움들을 고려하는 앱들은 많지 않다. 이렇게 GPS의 정확성과 NETWORK의 배터리 소모를 효율적으로 조합을 하는 모델을 정의해놔야 사용자의 위치를 이용하는 어플리케이션으로서 위치 성능을 최적화 했다고 할 수 있을 것이다.
: 모델의 기본적인 흐름은 다음과 같다.
- 앱을 시작한다.
- 위치 프로바이더들로부터 사용자의 위치 정보를 listen하기 시작한다.
- 사용자의 위치를 추정하여 다양한 소스로부터 들어온 정보를 정확한 것을 보유하고 필터링해준다.
- 위치 프로바이더로부터 listen을 중지한다.
- 최근의 추정한 위치를 사용한다.
: 위의 모델을 간단한 예로 나타내면 아래와 같다.
앱 시작 -> GPS와 Network 위치 프로바이더 listen -> Cache 해둔 위치 정보 사용 -> Cell-ID 기반의 위치 정보 갱신(Cache 위치 폐기) -> WiFi 기반의 위치 갱신(Cell-ID 기반의 위치 폐기) -> GPS 위치 갱신(WiFi 기반의 위치 폐기) -> GPS와 Network 위치 프로바이더 listen 중지 -> GPS 위치 정보 Cache
: 이 모델의 특징은 최근에 받아온 정보를 이용하면서, 가장 정확한 위치 정보 데이터를 최대한 유지하고 계속 새롭게 갱신해나간다는 것이다. 그리고 계속 위치 정보를 받아오는 것이 아니라 listen하는 것을 잠시 중지 시킴으로써 배터리 소모 문제를 해결하고자 한 것이다. 이러한 모델을 사용하고자 한다면 앱의 특성에 따라 다양한 결정들을 해야할 것이다.
* 언제 listen을 시작할 것인가
: 위치 정보의 갱신은 앱을 시작하자마자 사용자 위치를 가져오거나 사용자가 특정한 기능을 실행했을 때 시작하는 것이 좋다. 하지만 지속적인 위치 정보의 갱신은 배터리 소모의 주된 원인이므로 조심해야하지만, 짧은 위치 정보의 갱신은 정확성이 떨어지므로 trade-off를 잘 계산해야한다. 위의 예제에서 이용한 것과 같이 아래와 같이 위치 정보를 listen할 수 있다.
1
2
3
4
5
| LocationProvider locationProvider = LocationManager.NETWORK_PROVIDER; // 또는 GPS 데이터를 이용하고자 한다면.. // LocationProvider locationProvider = LocationManager.GPS_PROVIDER; locationManager.requestLocationUpdates(locationProvider, 0 , 0 , locationListener); |
* 이전에 수집했던 위치 이용하기
: 때로는 위치 listener가 위치를 가져올 때까지 너무 긴 시간이 걸릴지도 모른다. 그럴 대에는 기존에 수집했던, 캐쉬되어있던 위치를 가져와서 사용하면 대략적인 기능을 이용할 수 있을 것이다. 이 경우 LocationManager의 getLastKnownLocation(String) 함수를 이용하면 된다. 아래와 같이 이용할 수 있다.
1
2
3
4
5
| LocationProvider locationProvider = LocationManager.NETWORK_PROVIDER; // 또는 GPS 데이터를 이용하고자 한다면.. // LocationProvider locationProvider = LocationManager.GPS_PROVIDER; Location lastKnownLocation = locationManager.getLastKnownLocation(locationProvider); |
* 언제 listen을 멈출 것인가
: 언제 시작하느냐보다 중요한 것은 위치의 수집을 언제 멈출 것인가이다. 왜냐하면 오랫동안 위치 정보를 한다면, 엄청난 배터리 소모를 감수해야할 것이기 때문이다. 많은 지도를 이용하는 앱들이 수집하는 기능에만 초점을 맞춰서 배터리 소모를 고려하지 않고 있겠지만, 이것을 고민해야하는 것은 지도 관련 앱을 개발하는데 있어서 가장 기본이다. 언제 멈출것인가를 결정하는 것은 다양한 방법이 있다. 현재 측정한 값이 정확한 값이라고 여겨진다면 멈춘다던가, 사용자가 움직이지 않으면 멈춘다던가, 앱의 기능과 특징에 따라 잘 결정해야할 것이다. 위치 정보의 listen은 아래와 같이 멈출 수 있다.
1
| locationManager.removeUpdates(locationListener); |
* 가장 정확성이 높은 위치 값 보유하기
: 일반적으로는 간단하게 가장 최근에 수집된 위치 값이 가장 정확성이 높다고 짐작할 수 있을 것이다. 하지만 실제로는 정확성이 그때그때 달라지기 때문에 몇가지 기준에 의하여 위치의 정확성을 측정하여 가장 정확할 값을 계속 유지하고 사용해야할 것이다. 아래는 정확성을 판단할 수 있는 몇개의 기준이다.
- 기존에 수집된 값보다 일정 시간 이후의 최신 데이터 값인지 체크
- 기존에 수집된 값보다 정확성이 더 좋은지 나쁜지 체크
- 새로 들어온 위치 프로바이더가 더 신뢰할 수 있는지 체크
: 다양한 기준들이 있겠지만, 가장 쉽게 생각할 수 있는 위와 같은 기준으로 정확성 높은 값을 유지하는 함수를 짠다면 다음과 같이 짤 수 있을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| private static final int TWO_MINUTES = 1000 * 60 * 2 ; /** 기존의 위치와 비교하여 더 좋은지 여부를 판단하고 위치 정보를 갱신하면 될 것이다 * @param location 기존의 위치와 비교할 새로운 위치정보 * @param currentBestLocation 현재 보유하고 있는 위치 정보 */ protected boolean isBetterLocation(Location location, Location currentBestLocation) { if (currentBestLocation == null ) { // 기존의 위치 정보가 없다면 새로운게 좋은것이다. return true ; } // 새로 들어온 위치 정보가 최근 것인지 아닌지 여부를 구분 long timeDelta = location.getTime() - currentBestLocation.getTime(); boolean isSignificantlyNewer = timeDelta > TWO_MINUTES; boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES; boolean isNewer = timeDelta > 0 ; // 2분을 지났다면 새로운 위치 정보가 더 좋은 것이다. if (isSignificantlyNewer) { return true ; // 만약 새로들어온 위치 정보가 2분이전의 정보라면 안 좋은 것이다. } else if (isSignificantlyOlder) { return false ; } // 정확성을 가져와서 비교 int accuracyDelta = ( int ) (location.getAccuracy() - currentBestLocation.getAccuracy()); boolean isLessAccurate = accuracyDelta > 0 ; boolean isMoreAccurate = accuracyDelta < 0 ; boolean isSignificantlyLessAccurate = accuracyDelta > 200 ; // 위치 정보 프로바이더의 종류가 같은가? boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider()); // 정확성과 시간을 조합하여 품질을 결정 if (isMoreAccurate) { return true ; } else if (isNewer && !isLessAccurate) { return true ; } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { return true ; } return false ; } /** 두개의 위치 프로바이더가 같은지 비교하는 함수 */ private boolean isSameProvider(String provider1, String provider2) { if (provider1 == null ) { return provider2 == null ; } return provider1.equals(provider2); } |
* 데이터 정확성과 배터리 소모 조절하기
: 데이터 정확성과 배터리 소모는 서로 트레이드 오프가 이루어지기 때문에 적절한 선에서 정확성과 배터리 소모를 잘 고려해야할 것이다. 정확성이 어느정도 나왔다면, 대신 배터리 소모를 줄일 수 있는 몇 가지 방법들이 있다.
- 위치를 listen하는 시간을 조절: 위치를 더 적은 시간동안 수집하게 된다면 위에서 더 좋은 위치 값을 선정할 때 들어오는 위치값들이 줄어들게 되므로 정확성은 조금 떨어지겠지만 배터리 소모는 아낄 수 있을 것이다.
- 위치 프로바이더들이 위치를 업데이트하는 주기를 조절: 위치를 listen하는 중에도 수집되는 rate을 조절하면 배터리 소모를 줄일 수 있지만 정확성이 조금 떨어질 수 있을 것이다. 앱에서 얼마나 위치가 중요한가에 따라 조절을 하면 좋을 것이다. 업데이트 주기는 requestLocationUpdates의 최소 주기와 위치를 수집하게될 변환 거리를 설정함으로써 간단하게 조절할 수 있다.
- 사용하는 위치 프로바이더를 조절: 구현하는 앱이 어디서 자주 이용되는가, 얼마나 높은 정확성을 요구하는가 등에 따라 네트워크 프로바이더나 GPS 프로바이더 중 하나만 선택하거나 정확성이 필요하다면 둘다 선택할 수 있을 것이다. 하나만 선택한다면 역시 배터리 소모가 줄어들겠지만 정확성이 조금 떨어질지도 모른다.
* 어디에 이런 위치 정보를 사용할까?
: 이러한 위치 정보의 활용은 다양한 앱에서 할 수 있을 것이다. 일반적으로 가장 많이 사용되는 것은 컨텐츠와 결합하여 더욱 창의적이고 유용한 컨첸츠로 활용될 때에 위치 정보가 많이 이용된다. 이럴 때에는 위의 listen을 언제 시작할 지 간단한 예를 만들 수 있을 것이다.
- 1) 앱 시작
- 2) 사용자가 컨텐츠 입력 시작
- 3) 사용자가 입력을 저장
: 이러한 순서로 앱이 진행된다고 치자. 여기에 위치 정보도 같이 제공하는 서비스를 제공하고 싶다면, 2)번부터 위치 정보를 listen하다가 3번에 저정하기 전에 멈추고 사용자에게 위치 정보를 제공해주는 것이 배터리 소모와 정확성을 동시에 가져갈 수 있는 선택이 될 것이다. 사용자가 저장하기 전까지는 지속적으로 들어오는 새로운 위치 정보들을 정제하고 더 정확한 위치 정보를 취득하면 되는 것이다.
흐름
|
------ 1 ----->
|
----- 2 ----->
|
----- 3 ----->
|
----- 4 ----->
|
앱 동작
|
앱 시작
|
사용자가 컨텐츠 입력 시작
|
사용자가 입력 중
|
사용자가 입력을 저장
|
위치 정보
|
-
|
위치 정보 listen 시작
|
정확한 위치 정보 도착
|
위치 정보 listen 중지
|
: 두번째 사용처는 사용자가 어디로 갈 것인지 알려줄 수 있는 역할일 것이다. 예를 들면 사용자의 위치 근처에 가까운 식당들을 추천해주는 앱을 제공해 준다고 할 때 이용하면 된다. 그럼 다시 위처럼 사용자의 앱 사용 흐름을 고민해보면 다음과 같을 것이다.
- 이전의 위치 정보보다 정확한 위치 정보가 들어오게 되면 추천 정보를 다시 정렬
- 추천 정보가 안정화 되면 listen을 중지
흐름
|
------ 1 ----->
|
----- 2 ----->
|
----- 3 ----->
|
----- 4 ----->
|
앱 동작
|
앱 시작
|
사용자가 추천 목록 접근
|
추천 목록 갱신
|
추천 목록 안전화
|
위치 정보
|
-
|
위치 정보 listen 시작
|
정확한 위치 정보 도착
|
위치 정보 listen 중지
|
: 이런식으로 위치 정보를 이용한다면 앱에서 사용자가 위치 정보가 필요할 때의 시나리오를 잘 작성하여 언제 얼마나 측정을 할 것인지 계획을 잘 세워야 사용자에게 맞는 앱을 개발 할 수 있을 것이다.
끝.
[[ LocationFactory.java]]
package unikys.icu.util;
import unikys.icu.activity.NearMapFragment;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
public class LocationFactory {
private static LocationListener sLocationListener = null;
private static LocationManager sLocationManager = null;
private static Location sLocation = null;
private static boolean sIsMeasuringLocation = false;
public static void startLocationMeasure(final NearMapFragment fragment) {
if (sIsMeasuringLocation) {
return;
}
if (sLocationManager == null) {
sLocationManager = (LocationManager)fragment.getActivity().getSystemService(Context.LOCATION_SERVICE);
}
if (sLocationListener == null) {
LocationFactory.sLocationListener = new LocationListener() {
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
@Override
public void onLocationChanged(Location location) {
if (isBetterLocation(location, sLocation)) {
Log.d("LocationFactory.java", "Location Acquired: " + location.toString());
sLocation = location;
fragment.moveToLocation(sLocation);
}
}
};
}
sLocation = null;
sIsMeasuringLocation = true;
// sLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, sLocationListener);
sLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, sLocationListener);
}
public static Location getLocation(NearMapFragment fragment) {
if (sIsMeasuringLocation == false) {
startLocationMeasure(fragment);
}
return sLocation; //however return location, check if null
}
public static void stopLocationMeasure() {
if (sLocationManager != null && sLocationListener != null) {
sLocationManager.removeUpdates(sLocationListener);
}
}
private static final int TWO_MINUTES = 1000 * 60 * 2;
private static boolean isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) {
return true;
}
long timeDelta = location.getTime() - currentBestLocation.getTime();
boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
boolean isNewer = timeDelta > 0;
if (isSignificantlyNewer) {
return true;
} else if (isSignificantlyOlder) {
return false;
}
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
boolean isLessAccurate = accuracyDelta > 0;
boolean isMoreAccurate = accuracyDelta < 0;
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
boolean isFromSameProvider = isSameProvider(location.getProvider(),
currentBestLocation.getProvider());
if (isMoreAccurate) {
return true;
} else if (isNewer && !isLessAccurate) {
return true;
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
return true;
}
return false;
}
private static boolean isSameProvider(String provider1, String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}}
댓글 없음:
댓글 쓰기