2023.03.06 - 안드로이드 앱개발자 과정

CHA·2023년 3월 6일
0

Android



위치기반 서비스 LBS(Location Based Service)


Location Manager

위치정보의 관리자를 활용하여 위치정보를 가져올 수 있습니다.

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    tv = findViewById(R.id.tv);

	List<String> providers = locationManager.getAllProviders();
    StringBuffer buffer = new StringBuffer();
    for(String provider : providers){
        buffer.append(provider+", ");
    }

    tv.setText(buffer.toString());
}

  • Passive

    다른 앱에서 취득했던 위치정보를 가져오는 방법입니다. 단, 마지막에 취득했던 위치정보이기 때문에 언제 취득된 정보인지는 알 수 없습니다. 그래서 가장 신뢰도가 낮은 방법입니다.

  • network

    네트워크를 통해 위치정보를 가져오는 방법입니다.

  • fused

    권장되는 방법입니다. 실내에서는 network 로 실외에서는 GPS 를 사용하게 되며, 자동으로 처리해줍니다.

  • GPS

    위치 정보의 정확성이 가장 높은 방법입니다.

내 위치 얻어오기

버튼을 누르면 내 위치를 얻어와 보는 테스트를 해봅시다.

protected void onCreate(Bundle savedInstanceState) {

	... 중략
    
    findViewById(R.id.btn).setOnClickListener(view -> clickBtn());

    int checkPermission = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
    if (checkPermission == PackageManager.PERMISSION_DENIED) {
        launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION);
    }
}

ActivityResultLauncher<String> launcher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<Boolean>() {
    @Override
    public void onActivityResult(Boolean result) {
        if (result) Toast.makeText(MainActivity.this, "퍼미션 허용", Toast.LENGTH_SHORT).show();
        else Toast.makeText(MainActivity.this, "퍼미션 불가", Toast.LENGTH_SHORT).show();
    }
});

void clickBtn() {
    Location location = null;
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
        return;
    } 
    
    if (locationManager.isProviderEnabled("fused")) {
        location = locationManager.getLastKnownLocation("fused");
    }else if( locationManager.isProviderEnabled("gps") ){
        location = locationManager.getLastKnownLocation("gps");
    }else if( locationManager.isProviderEnabled("network") ){
        location = locationManager.getLastKnownLocation("network");
    }
    
    if(location == null){
        tv2.setText("위치를 찾을수 없습니다.");
    } else {
        double latitude = location.getLatitude();
        double longitude = location.getLongitude();

        tv2.setText(latitude + " , " + longitude);
    }
}

위치 정보 제공을 받기 위해서는 퍼미션이 필요합니다. 그래서 Manifest 파일에 퍼미션을 추가해줍시다.

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

위쪽 퍼미션은 GPS 사용을 위한 퍼미션이며, 아래쪽 퍼미션은 network 사용을 위한 퍼미션 입니다. 추가로, GPS 사용을 위한 퍼미션은 동적 퍼미션으로 처리해주어야 하므로, 동적 퍼미션 다이얼로그를 띄워주러 가봅시다.

  • int checkPermission = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
    checkSelfPermission 을 이용하여 퍼미션의 허용 상태를 checkPermission 에 넣어줍시다. 허용이라면 0, 아니라면 -1 을 리턴합니다. 또한 ACCESS_FINE_LOCATION 에 대한 동적퍼미션만 설정하면 ACCESS_COARSE_LOCATION 는 자동으로 허용됩니다.

  • ActivityResultLauncher<String> launcher
    퍼미션 요청 및 결과를 받아주는 런처를 만들어줍시다. RequestPermission() 계약을 처리해주고, 퍼미션이 허용된다면 허용되었다는 토스트를, 허용되지 않았다면 불가라는 토스트를 띄워서 테스트합시다.

  • checkPermission == PackageManager.PERMISSION_DENIED
    퍼미션의 결과가 PERMISSION_DENIED 라면 퍼미션이 허용되지 않았다는 의미이므로, 런처에게 퍼미션 허용 요청을 사용자에게 받아올 수 있도록 요청합시다.

여기까지 했으면, 동적 퍼미션은 완료했습니다. 그러면 사용자에게는 위치 기반 서비스를 사용할것이냐 요청하는 다이얼로그가 뜨게 됩니다. 자 그러면 버튼을 눌렀을 때, 위치 정보를 받아오는 코드를 짜봅시다. 단, 우리가 퍼미션을 허용해준곳은 onCreate() 메소드 이며, 버튼을 눌렀을 때, 퍼미션이 허용되었음을 체크하는 코드가 추가로 필요합니다.

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
    return;
}

if 문의 조건으로는 ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION 가 둘다 모두 허용되었다면 위치기반 서비스를 시작할 수 있으며, 둘 중 하나라도 허용되지 않는다면 return 으로 메소드를 종료시켜주어야 합니다.

만일 둘 다 허용이 되었다면 이제 위치 정보를 받아와야 합니다. 단, 앞서 이야기 했듯 위치정보를 받아올 수 있는 서비스에는 4가지가 있었습니다. 그 중 3가지, fused, gps, network 를 분기문으로 처리해주어야 합니다.

if (locationManager.isProviderEnabled("fused")) {
    location = locationManager.getLastKnownLocation("fused");
}else if( locationManager.isProviderEnabled("gps") ){
    location = locationManager.getLastKnownLocation("gps");
}else if( locationManager.isProviderEnabled("network") ){
    location = locationManager.getLastKnownLocation("network");
}

어떠한 위치기반 서비스를 사용하느냐에 따라, location 객체에 들어갈 Provider 가 달라집니다.

자 그러면, 여기까지 왔다면, location 객체 안에는 어떠한 위치정보가 들어있을 겁니다. 이 위치정보를 위도와 경도로 얻어와봅시다.

if(location == null){
    tv2.setText("위치를 찾을수 없습니다.");
} else {
    double latitude = location.getLatitude();
    double longitude = location.getLongitude();

    tv2.setText(latitude + " , " + longitude);
}

위도와 경도를 location 객체에게 요청하고 받아와서, 텍스트뷰에 뿌려주었습니다. 이런식으로 LocationManager 를 이용하면 위치의 정보를 받아올 수 있습니다.

테스트를 위한 구글맵 위치 갱신 및 위치 업데이트 기능

void clickBtn2(){
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
        return;
    }
    if(locationManager.isProviderEnabled("fused")){
        locationManager.requestLocationUpdates("fused",5000, 2, listener );
    } else if(locationManager.isProviderEnabled("gps")){
        locationManager.requestLocationUpdates("gps",5000, 2, listener );
    } else if(locationManager.isProviderEnabled("network")){
        locationManager.requestLocationUpdates("network",5000, 2, listener );
    }
}

LocationListener listener = new LocationListener() {
    @Override
    public void onLocationChanged(@NonNull Location location) {
        double latitude = location.getLatitude();
        double longitude = location.getLongitude();
        tv3.setText(latitude + ", " + longitude);
    }
};

void clickBtn3(){
    locationManager.removeUpdates(listener);
}

퍼미션을 허용하는것은 앞선 테스트와 동일합니다. 다만, 앞선 테스트와는 좀 다르게 내 위치의 정보를 업데이트를 해주어야 합니다. requestLocationUpdates() 를 이용하면 위치정보를 업데이트 할 수 있습니다.

  • locationManager.requestLocationUpdates("fused",5000, 2, listener );
    첫번째 파라미터는 어떠한 Provider 를 사용할건지, 두번째 파라미터는 몇초마다 정보를 갱신할건지, 어느정도의 거리가 되면 갱신할건지, 마지막 파라미터로는 리스너를 달아줘야 변경된 위치정보가 업데이트 됩니다.
LocationListener listener = new LocationListener() {
    @Override
    public void onLocationChanged(@NonNull Location location) {
        double latitude = location.getLatitude();
        double longitude = location.getLongitude();
        tv3.setText(latitude + ", " + longitude);
    }
};

리스너를 만들어 변경된 위치 정보를 받아올 수 있습니다. 각각의 위도와 경도 정보를 변수에 담아 텍스트뷰에 뿌려줄 수 있습니다.


Fused API

Google 지도앱에 사용되고 있는 위치정보 제공자 최적화 라이브러리 입니다. 라이브러리이기 떄문에 play-services-location 을 추가해주어야 합니다.

public class MainActivity extends AppCompatActivity {

    FusedLocationProviderClient providerClient;
    TextView tv;
    LocationCallback locationCallback;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = findViewById(R.id.tv);
        findViewById(R.id.btn).setOnClickListener(view -> clickBtn());
    }

    void clickBtn(){
        int checkPermission = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
        if(checkPermission == PackageManager.PERMISSION_DENIED){
            launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION);
            return;
        }
        providerClient = LocationServices.getFusedLocationProviderClient(this);

        LocationRequest locationRequest = LocationRequest.create();
        locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); 
        locationRequest.setInterval(5000); 

        providerClient.requestLocationUpdates(locationRequest,locationCallback, Looper.getMainLooper());
        locationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(@NonNull LocationResult locationResult) {
                super.onLocationResult(locationResult);

                Location location = locationResult.getLastLocation();

                Double latitude = location.getLatitude();
                Double longitude = location.getLongitude();
                tv.setText(latitude +", " + longitude);
            }
        };

    }
    @Override
    protected void onPause() {
        super.onPause();

        if(providerClient != null){
            providerClient.removeLocationUpdates(locationCallback);
        }
    }

    ActivityResultLauncher<String> launcher = registerForActivityResult(new ActivityResultContracts.RequestPermission(),
            result -> {
                if(result) Toast.makeText(MainActivity.this, "퍼미션 허용됨", Toast.LENGTH_SHORT).show();
                else Toast.makeText(MainActivity.this, "퍼미션 거부됨", Toast.LENGTH_SHORT).show();
    });
}
  • FusedLocationProviderClient providerClient;
    play-services-location 라이브러리를 사용하기 위한 참조변수 입니다.

Fused API 또한 위치정보를 받아오는 API 이므로, 동적 퍼미션이 필수입니다. 동적 퍼미션을 받는 코드는 앞선 테스트와 동일하기 때문에 넘어가겠습니다.

  • providerClient = LocationServices.getFusedLocationProviderClient(this);
    FusedLocationProviderClient 의 객체를 소환합시다.

  • LocationRequest locationRequest = LocationRequest.create();
    위치정보의 최적화를 위해서는 기준이 필요합니다. 정확한 위치정보가 우선일수도, 배터리의 효율최적화가 우선일수도 있습니다.locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); 이나 locationRequest.setInterval(5000); 와 같이 그 기준을 설정해줄 수 있습니다.

  • providerClient.requestLocationUpdates(locationRequest,locationCallback, Looper.getMainLooper());
    requestLocationUpdates 을 이용하여 위치정보를 갱신할 수 있습니다. 첫번째 파라미터는 앞서 만들었던 LocationRequest 의 객체가, 두번째 파라미터로는 LocationCallback 의 리스너가 와야 합니다.

locationCallback = new LocationCallback() {
    @Override
    public void onLocationResult(@NonNull LocationResult locationResult) {
        super.onLocationResult(locationResult);

        Location location = locationResult.getLastLocation();

        double latitude = location.getLatitude();
        double longitude = location.getLongitude();
        tv.setText(latitude +", " + longitude);
    }
};

위치정보가 변경되면 onLocationResult 콜백메서드로 위치정보를 받아옵니다. 메소드 내부에서 위치정보를 받아와 위도와 경도로 받아오는 작업을 해줄 수 있습니다.

@Override
protected void onPause() {
    super.onPause();

    if(providerClient != null){
        providerClient.removeLocationUpdates(locationCallback);
    }
}

화면이 없어졌을때, 위치정보를 받아오는 작업을 멈춰야하기 때문에 onPause() 를 이용하여 위치정보 업데이트를 중단해줍시다. removeLocationUpdates() 의 파라미터로 LocationCallback 의 리스너를 전달해주면 됩니다.

Fused API 가 편리한 점은, Location Manager 와는 다르게 어떠한 Provider 를 사용하는지에 대한 분기문이 필요없다는 점입니다. Fused API 가 알아서 network 를 쓸지, gps 를 쓸지 결정하기 때문입니다.

추가적으로, 앱이 종료되어있더라도 위치정보를 받아오고 싶을수도 있겠죠. 그럴때는 아래와 같은 퍼미션이 필요합니다. 일단 퍼미션이 필요하다는 사실만 알고 넘어갑시다.

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

GeoCoder

지오코딩의 작업은 Google 지도 서버를 이용합니다. 그렇기 때문에 GeoCoder 를 사용하기 위해서 인터넷과 관련한 퍼미션을 하나 허용해줍시다. 정적퍼미션이기 때문에 아래 코드 한줄만 메니페스트 파일에 추가해줍시다.

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

첫번째 버튼을 누르면, 지오코딩을 통해 입력된 주소를 좌표로 반환하며, 두번째 버튼은 역 지오코딩을 통해 입력된 좌표를 주소로, 세번째 버튼을 누르면 첫번째 버튼을 눌러 입력된 주소를 좌표로 바꾸고, 그 좌표를 기반으로 맵을 띄워주는 코드를 짜봅시다. 즉, 세번째 버튼을 눌러 맵을 띄우려면 주소를 입력하고 첫번째 버튼을 누른 뒤 세번째 버튼을 눌러주어야 합니다. 자 그럼 코드를 봅시다.

void clickBtn(){
    String addr = et.getText().toString();
    Geocoder geocoder = new Geocoder(this, Locale.KOREA);
    
    try {
        List<Address> addresses = geocoder.getFromLocationName(addr,3);
        StringBuffer buffer = new StringBuffer();
        for(Address address : addresses){
            buffer.append(address.getLatitude() + ", " + address.getLongitude() + "\n");
        }

        latitude = addresses.get(0).getLatitude();
        longitude = addresses.get(0).getLongitude();

        new AlertDialog.Builder(this).setMessage(buffer.toString()).create().show();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

첫번째 버튼을 눌렀을 때의 코드입니다.

  • String addr = et.getText().toString();
    EditText 를 통해 입력받은 주소를 좌표로 반환해주기 위해 String 문자열변수에 먼저 담아둡시다.

  • Geocoder geocoder = new Geocoder(this, Locale.KOREA);
    Geocoder 객체를 하나 만들어줍시다. 파라미터로는 Context 객체하나와, 어느지역인지를 전달합니다.

  • List<Address> addresses = geocoder.getFromLocationName(addr,3);
    getFromLocationName() 의 파라미터로 입력받은 주솟값을 전달하고, 그 주솟값에 해당하는 좌표를 3개까지만 리턴합니다. 그리고 리턴값은 Address 를 제네릭타입으로 가지는 List 로 반환합니다.

  • StringBuffer buffer = new StringBuffer();
    좌표값을 넣어주기 위해 버퍼를 만들어줍니다.

  • buffer.append(address.getLatitude() + ", " + address.getLongitude() + "\n");
    버퍼에 주소의 위도와 경도를 append 해줍니다.

  • latitude = addresses.get(0).getLatitude();longitude = addresses.get(0).getLongitude(); 은 세번째 버튼을 눌렀을 때 사용할 좌표값을 미리 설정해놓았습니다.

  • new AlertDialog.Builder(this).setMessage(buffer.toString()).create().show();
    다이얼로그를 통해 만든 좌표값을 화면에 띄워줍니다.

void clickBtn2(){
    double latitude = Double.parseDouble(etLat.getText().toString());
    double longitude = Double.parseDouble(etLong.getText().toString());

    Geocoder geocoder = new Geocoder(this,Locale.KOREA);

    try {
        List<Address> addresses = geocoder.getFromLocation(latitude,longitude,3);
        StringBuffer buffer = new StringBuffer();
        for(Address address : addresses){
            buffer.append(address.getCountryName() +"\n");
            buffer.append(address.getCountryCode() +"\n"); 
            buffer.append(address.getPostalCode() +"\n"); 
            buffer.append(address.getAddressLine(0) +"\n"); 
            buffer.append(address.getAddressLine(1) +"\n");
            buffer.append(address.getAddressLine(2) +"\n"); 
            buffer.append("========================\n");
        }
        new AlertDialog.Builder(this).setMessage(buffer.toString()).create().show();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

두번째 버튼을 눌렀을 때의 코드입니다.

  • List<Address> addresses = geocoder.getFromLocation(latitude,longitude,3);
    나머지는 첫번째 버튼의 테스트와 거의 동일합니다. 다만, 첫번째 버튼 테스트에서는 Geocoder 객체를 통해 좌표를 가져왔다면 이번에는 getFromLocation() 를 이용하여 좌표를 주고 주소값을 받아왔습니다. 역시 반환값은 Address 를 제네릭 타입으로 갖는 List 입니다.
void clickBtn3(){
    Intent intent = new Intent(Intent.ACTION_VIEW);

    Uri uri = Uri.parse("geo:"+latitude+","+longitude);
    intent.setData(uri);
    startActivity(intent);
}

세번째 버튼을 눌렀을 때의 코드입니다. 맵을 띄워줘야 하기 때문에 Intent 객체를 활용했습니다. 인텐트에 setDate() 를 이용하여 좌표값을 넣어준 뒤, startActivity() 로 맵을 실행시켜 주었습니다.


Map API

앱 안에 지도를 보여줄 수 있는 API 입니다. Google 이나 Naver 에서도 Map API 를 제공하나, 여러가지 제약사항들이 있어, 일단 이번 테스트에서는 Kakao Map API 를 활용하여 테스트를 진행해봅시다.


Kakao Map API

Kakao Map API 의 경우 AVD 에서 동작을 확인해볼수 없습니다. 실디바이스 혹은 Mac OS 의 M1,M2 chip AVD 에서만 동작한다는걸 참고합시다. 또한 이번 테스트는 어떠한 자바의 문법이 중요한게 아닙니다. 이 Kakao Map API 는 안드로이드 고유의 기능이 아니라, 카카오라는 회사에서 제공하는 API 입니다. 그렇기 때문에 카카오에서 제공하는 개발 튜토리얼에 친숙해지는걸 목표로 합시다. 아래쪽에는 Google, Naver, Kakao 의 Map API 사이트의 링크를 남겨놓았습니다.

Google Map API 사이트
Naver Map API 사이트
Kakao Map API 사이트

카카오 Map API 의 라이브러리를 추가하는 방법이나, 네이티브 앱 키 발급, API 사용법, Permission 등은 사이트에 모두 수록되어 있으니 보고 그대로 따라하면 됩니다.

다만, 라이브러리를 추가하고 앱과 연결시켜주어야 하는데 그 부분은 빠져 있어 그 부분만 보고, 플랫폼 설정과 키 해시를 받는법에 대해서만 알아봅시다.

라이브러리 등록하기

카카오 사이트에서 라이브러리를 다운받고 내 앱에 추가를 시켜줬다고 라이브러리를 사용할 수 있는건 아닙니다. 이 라이브러리를 등록을 시켜줘야하는데요, 예전에 circleImageView 이나 Glide 등을 사용했을 때 사용했었던 라이브러리 등록방법을 사용해주면 됩니다.

안드로이드 스튜디오의 File 탭에 들어가면 Project Structure 메뉴가 있습니다. 들어가서 왼편 메뉴에 Dependencies 탭으로 이동하고, app 으로 가면 디펜던시를 추가할 수 있는 창이 뜹니다. + 를 눌러보면 라이브러리 디펜던시와 JAR/AAR 디펜던시를 추가할 수 있다고 합니다. 우리는 JAR 를 추가해야 하므로 두번째를 클릭합시다.


그러면 위 그림과 같은 창이 뜹니다. 그럼 여기에서 jar 파일이 있는 경로를 입력해주면 됩니다. 그러면 이제 라이브러리가 모두 등록되었으므로 카카오의 Map API 를 사용할 수 있습니다.

플랫폼 설정 & 키 해시 받아오기

카카오의 개발자 페이지로 가서, 내 애플리케이션으로 들어가보면 카카오 로그인을 통해 내 앱을 등록할 수 있습니다. 그리고 플랫폼 또한 설정이 가능합니다. 우리는 안드로이드에서 Map API 를 사용할 예정이므로, 안드로이드로 플랫폼을 설정해주면 됩니다. 또한 여기에서 네이티브 앱 키를 받아올 수 있습니다. 이 네이티브 앱 키를 메니페스트 파일의 <meta-data> 에 설정해주면 됩니다.

이제 플랫폼 설정을 봅시다. 플랫폼 등록을 하기 위해 우리가 사용할 앱의 패키지명과, 키 해시를 주어야 합니다. 패키지명은 Gradle 파일에 보면 나와있으니 참고합시다. 키 해시값은 네이티브 키 값을 주면 앱 내부에 키 해시값이 설정됩니다. 그래서 로그값을 이용하여 키 해시값을 알아와서, 카카오 개발자 페이지에 등록해주면 됩니다.

String keyHash = Utility.getKeyHash(this);
Log.i("keyHash",keyHash);

위 코드를 작성하고, Logcat 창에서 keyHash 를 검색하면, 키 해시값을 받아올 수 있습니다. 받아온 키 해시값을 플랫폼 등록시 넣어주면 끝입니다.

profile
Developer

0개의 댓글