티스토리 뷰

728x90

앱에서 종종 네비게이션 기능을 구현해야 할 때가 있습니다. 이 기능을 구현하려면 반드시 내위치를 따라 지도에 마커를 생성하는 기능이 있어야합니다. 오늘은 구글지도에 내위치 변화에 따라 마커를 생성하는 방법에 대해 알아보겠습니다.

 

gps정보를 얻기 위해서는 사용자로부터 위치정보제공 동의를 받아 권한을 얻어야합니다. 권한을 얻는시점은 앱의 시작부분일수도 있고 해당 정보가 필요한 시점일수도 있지만 올바른 권장 앱설계는 해당 정보가 필요한 시점에 권한을 요청하는 것입니다. 

 

gps정보를 얻기위해 메니페스트 파일에 필요한 권한을 추가하겠습니다. 메니페스트 파일에 다음과 같은 코드를 추가합니다.

 

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

그리고 gps정보를 얻기위한 클래스를 하나 생성합니다. 이름은 자유롭게 작성해주시고 Service클래스를 상속받고 LocationListener 인터페이스를 구현해야합니다. 

Location객체에 경도,위도 값을 저장하고 LocationManager객체를 통해 gps정보를 가져오도록 하겠습니다.

매번 gps정보를 가져올때마다 권한을 체크해줘야 하며 중간에 권한을 잃었을경우 null을 리턴해야합니다. 제가 생성한 클래스의 소스코드입니다.

package guitar.student;

import android.Manifest;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

import androidx.core.content.ContextCompat;

public class GPSManager extends Service implements LocationListener {
    Context context;
    Location location;//위치정보저장
    double latitude;
    double longitude;
    private static final long MIN_DISTANCE_CHANGE_FOR_UPDATES = 0;
    private static final long MIN_TIME_BW_UPDATES = 1000;
    protected LocationManager locationManager;

    public GPSManager(Context _context){
        context = _context;
        getLocation();
    }
    public Location getLocation(){
        try {

            locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
            boolean isGPSEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
            boolean isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

            if (isGPSEnabled == false && !isNetworkEnabled == false) {
                Log.d("gpsTest", "gps off");
            } else {
                //권한을 보유하고 있는지 검사
                int hasFineLocationPermission = ContextCompat.checkSelfPermission(context,
                        Manifest.permission.ACCESS_FINE_LOCATION);
                int hasCoarseLocationPermission = ContextCompat.checkSelfPermission(context,
                        Manifest.permission.ACCESS_COARSE_LOCATION);

                //PERMISSION_GRANTED일 경우 권한허용 상태 PERMISSION_DENIED일 경우 거부상태
                if (hasFineLocationPermission == PackageManager.PERMISSION_DENIED ||
                        hasCoarseLocationPermission == PackageManager.PERMISSION_DENIED) {
                    //둘중 하나라도 거부상태일 경우 null을 리턴
                    Log.d("gpsTest", "permission denied");
                    return null;
                }


                //네트워크를 통해 gps정보를 얻고 만약 실패했을때 gps_provider를 통해 정보를 제공받음.
                if (isNetworkEnabled) {
                    locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, MIN_TIME_BW_UPDATES, MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
                    if (locationManager != null)
                    {
                        location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
                        if (location != null)
                        {
                            latitude = location.getLatitude();
                            longitude = location.getLongitude();
                        }
                    }
                }

                if (isGPSEnabled)
                {
                    if (location == null)
                    {
                        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, MIN_TIME_BW_UPDATES, MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
                        if (locationManager != null)
                        {
                            location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
                            if (location != null)
                            {
                                latitude = location.getLatitude();
                                longitude = location.getLongitude();
                            }
                        }
                        else{
                            Log.d("gpsTest", "null...");
                        }
                    }
                }
            }
        }
        catch (Exception e)
        {
            Log.d("locationError@@", ""+e.toString());
        }
        return location;
    }
    public double getLatitude(){
        if(location != null){
            latitude = location.getLatitude();
        }
        return latitude;
    }

    public double getLongitude(){
        if(location != null){
            longitude = location.getLongitude();
        }
        return longitude;
    }

    @Override
    public void onLocationChanged(Location location){
        Log.d("gpsTest", "LocationChanged");

    }
    @Override
    public void onProviderDisabled(String provider){
        Log.d("gpsTest", "ProviderDisable");

    }
    @Override
    public void onProviderEnabled(String provider){
        Log.d("gpsTest", "ProviderAble");

    }
    @Override
    public void onStatusChanged(String provider, int status, Bundle extras){
        Log.d("gpsTest", "StatusChanged");
    }
    @Override
    public IBinder onBind(Intent arg0)
    {
        Log.d("gpsTest", "OnBind");
        return null;
    }
}

 

이제 다음으로 권한을 얻는 시점의 액티비티에서 권한을 요청하겠습니다. 이는 각 개인마다 소스코드가 상이할 수 있으니 필요한 부분부분만 보여드리고 각자 설계에 맞춰 적용시켜 보시길 바랍니다.

 

우선 액티비티의 속성으로 gps활성화 결과를 얻기위한 인텐트 코드, gps권한 요청결과를 위한 인텐트 코드, gps권한 요청을 위한 권한종류를 추가합니다.

    private static final int GPS_ENABLE_REQUEST_CODE = 2000;
    private static final int PERMISSIONS_REQUEST_CODE = 100;
    String[] REQUIRED_PERMISSIONS  = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};

위의 int값은 변경되어도 무방합니다. 저 변수를 선언하지 않고 나중에 코드에 직접 2000, 100을 입력하여도 되지만 가독성을 위해 변수명을 사용하는것을 권장합니다. 

 

그다음 액티비티가 활성화되는 시점인 onCreate()에서 권한을 요청합니다.

        if(!checkLocationServicesStatus()){
            showDialogForLocationServiceSetting();
        }
        else{
            checkGPSPermission();
        }

 

이제 여기서 사용한 3개의 메서드를 생성하고 다음 코드를 작성합니다.

 

    public boolean checkLocationServicesStatus() {
        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
                || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
    }

checkLocationServicesStatus()는 현재 gps를 제공받을 수 있는 환경인지 체크합니다.

 

    private void showDialogForLocationServiceSetting() {

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("위치 서비스 비활성화");
        builder.setMessage("앱을 사용하기 위해서는 위치 서비스가 필요합니다.\n"
                + "위치 설정을 수정하실래요?");
        builder.setCancelable(true);
        builder.setPositiveButton("설정", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int id) {
                Intent callGPSSettingIntent
                        = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                startActivityForResult(callGPSSettingIntent, GPS_ENABLE_REQUEST_CODE);
            }
        });
        builder.setNegativeButton("취소", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int id) {
                dialog.cancel();
                finish();
            }
        });
        builder.create().show();
    }

showDialogForLocationServiceSetting()메서드는 사용자가 gps기능을 활성화할지 여부를 묻는 팝업창을 생성합니다

사용자가 설정을 누르면 설정앱으로 인텐트하여 사용자가 gps 설정을 하도록 합니다. 그리고 startActivityForResult()메서드를 호출할때 위에서 선언한 gps설정 결과를 얻기위해 선언한 코드를 입력하고 사용자가 설정을 마친후에 앱을 돌아왔을때 gps활성화 여부를 onActivityResult에서 확인합니다. 사용자가 설정앱으로 이동하였지만 gps기능을 활성화하지 않고 그대로 닫아버릴수도 있기 때문입니다.

 

이미 있을수도 있고 없을수도 있는 onActivityResult메서드에 다음 코드를 추가합니다.

    protected void onActivityResult(int requestCode, int resultCode, Intent data){
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode){
            case GPS_ENABLE_REQUEST_CODE:
                if (checkLocationServicesStatus()) {
                    Log.d("checkGPSEnable", "onActivityResult : GPS 활성화 되있음");
                    checkGPSPermission();
                    return;
                }
                else{
                    Log.d("checkGPSEnable", "GPS 비활성화 되있음");
                }
                break;
        }
    }

gps기능을 활성화 하였을 경우 이제 gps권한이 있는지 확인합니다. 기능이 켜져있지 않을 경우 여러분의 의도에 맞게 코드를 구현합니다. 앱을 종료시켜도 되고 사용자가 시도하려는 기능을 막고 재시도 하였을때 다시 gps기능 활성화를 유도하여도 좋습니다.

 

이제 checkGPSPermission()메서드를 추가합니다.

 

    public void checkGPSPermission(){
        int locationPermissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION);
        int coarsePermissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION);
        if(locationPermissionCheck == PackageManager.PERMISSION_DENIED && coarsePermissionCheck == PackageManager.PERMISSION_DENIED){
            Log.d("permissionCheck", "denied");
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, PERMISSIONS_REQUEST_CODE);
        }
        else if(locationPermissionCheck == PackageManager.PERMISSION_GRANTED){
            Log.d("permissionCheck", "granted");
        }
    }

메니페스트 파일에 추가했던 2가지 gps관련 권한이 허용되어있는지 체크합니다. 권한 체크 결과값이 GRANTED일경우 허용됨을 의미하고 DENIED일 경우 거부됨을 의미합니다. 그럼 권한이 없을경우 권한을 요청을 하기위한 팝업창을 생성합니다. 여기서 2번째 매개변수에 위에서 액티비티의 속성으로 선언하였던 필요한 권한의 종류를 넘겨줍니다. 그리고 gps활성화때와 마찬가지로 gps권한 허용결과를 얻기위해 onRequestPermissionResult()메서드를 생성합니다. 이는 AppCompatActivity의 메서드를 오버라이딩 하는것입니다. 위에서 사용한 onActivityResult도 마찬가지이구요.

 

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults){
        if ( requestCode == PERMISSIONS_REQUEST_CODE && grantResults.length == REQUIRED_PERMISSIONS.length) {
            boolean check_result = true;
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    check_result = false;
                    break;
                }
            }
            if(check_result == false) {
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, REQUIRED_PERMISSIONS[0])
                        || ActivityCompat.shouldShowRequestPermissionRationale(this, REQUIRED_PERMISSIONS[1])) {

                    Toast.makeText(LoginActivity.this, "퍼미션이 거부되었습니다. 앱을 다시 실행하여 퍼미션을 허용해주세요.", Toast.LENGTH_LONG).show();
                    finish();
                }else {
                    Toast.makeText(LoginActivity.this, "퍼미션이 거부되었습니다. 설정(앱 정보)에서 퍼미션을 허용해야 합니다. ", Toast.LENGTH_LONG).show();
                    finish();
                }
            }
        }
    }

권한요청의 결과가 저희가 요청했던 권한의 종류개수가 일치하는지 우선 체크합니다. 그 후에 권한 결과에 따라 소스코드를 작성합니다. 저는 아래 권한이 없을 경우 현재 액티비티를 종료하게 하였습니다. 그런데 권한이 거부되었을 경우 두가지의 경우로 또 나뉘게 되는데 두가지의 차이점은 위 상태의 경우 이전의 권한거부를 한 경험이 없는 경우입니다. 아래의 경우는 한번 같은 권한요청에 대해 거부한 이력이 있는 경우 입니다. 두가지 상황에 따라 다르게 처리하고 싶은것이 있다면 앱 설계에 맞게 구현하시면 됩니다.

 

이제 gps정보를 얻기위한 준비를 모두 마쳤습니다. 이제 위에서 LocationListener인터페이스를 구현한 클래스의 인스턴스를 만들어 위치정보를 가져오도록 하겠습니다.

GPSManager gpsManager = new GPSManager(this);
gpsManager.getLocation();

gps정보가 필요한 시점에 GPSManager의 getLocation()메서드를 호출하면 됩니다. 여기서 GPSManager는 제가 생성한 클래스의 이름이지 라이브러리가 아닙니다. 

 

이제 이 gps정보를 내위치에 따라 반복적으로 얻어야 하기 때문에 일정시간마다 gps정보를 가져오고 여기서 획득한 위도, 경도값을 가지고 구글지도에 마커를 생성해보겠습니다. 일정시간마다 반복적인 작업을 위해서는 TimerTask클래스와 Timer클래스를 사용하여야합니다. 그리고 안드로이드는 UI를 갱신할때 메인스레드를 사용하는데 타이머는 서브스레드에서 동작하므로 서브스레드에서도 UI작업(마커 갱신)을 하기 위해 runOnUiTreand()메서드를 활용하겠습니다.

 

        TimerTask poolGPSLocationTask = new TimerTask(){
            @Override
            public void run(){
                DriveActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        gpsManager.getLocation();
                        if(mMap != null){
                            if(myLocationMarker == null) {
                                locationMarkerOption = new MarkerOptions();
                                locationMarkerOption.position(new LatLng(gpsManager.getLatitude(), gpsManager.getLongitude()));
                                myLocationMarker = mMap.addMarker(locationMarkerOption);
                            }
                            else{
                                myLocationMarker.setPosition(new LatLng(gpsManager.getLatitude(), gpsManager.getLongitude()));
                                Log.d("location_test", "location = " + gpsManager.getLatitude() + " , " + gpsManager.getLongitude());
                            }
                        }
//                        Toast.makeText(DriveActivity.this, gpsManager.getLatitude() + "," + gpsManager.getLongitude(), Toast.LENGTH_SHORT).show();순서에 따라
                    }
                });
            }
        };
        
        poolGPSLocationTimer = new Timer();
        poolGPSLocationTimer.schedule(poolGPSLocationTask, 0, POOL_GPS_TIME); 
        //get gps location and make marker every POOL_GPS_TIME seconds.
        

지도와 마커의 활용법은 지난 포스팅을 참고해주시길 바랍니다. 여기서 짚고 넘어가야할 것은 최초에 마커가 없을때는 addMarker()메서드의 반환값으로 마커를 저장해놓고 이후에 2번째 호출때는 마커의 위치값만 갱신해야 하는 것입니다. 만약 이렇게 하지 않을 경우 계속해서 새로운 마커가 내위치의 변화에 따라 생성되게 됩니다.

글에서 올린 GPSManager코드를 제외한 모든 소스코드는 제 프로젝트의 일부를 발췌한것입니다. 따라서 그대로 복사해서 사용할 경우 실행되지 않을 확률이 매우높습니다. 여러분의 앱 환경에 맞춰 수정하여서 사용하시길 바라며 만약 계속해서 안되거나 이해가 되지 않는 부분이 있다면 댓글로 질문 남겨주시고 한분한분 답변드리도록 하겠습니다.

 

도움이 되셨다면 꼭 좋아요 한번 부탁드립니다~

댓글