본문 바로가기

AppDev/Android

[ Android ] 안드로이드 구글 맵 내 위치 표시

구글 맵 SDK 를 이용하여 구글 맵으로 내 위치를 표시하는 기능을 구현하여 보겠습니다.

 

[ Empty Activity ] → [ Next ]
[ 프로젝트 이름 패키지 명을 입력 ] → [ Finish ]

buildscript 
{
    repositories 
    {
        google()
        mavenCentral()
    }
    dependencies 
    {
        classpath "com.android.tools.build:gradle:7.0.3"
        classpath 'com.google.gms:google-services:4.3.10'
    }
}

task clean(type: Delete) 
{
    delete rootProject.buildDir
}

[ build.gradle (Project: 프로젝트명) ] → [ classpath 'com.google.gms:google-services:4.3.10' ] 추가

 

plugins 
{
    id 'com.android.application'
}

android 
{
    compileSdk 31

    defaultConfig 
    {
        applicationId "com.sample.google.map"
        minSdk 23
        targetSdk 31
        versionCode 1
        versionName "1.0"
    }

    buildTypes 
    {
        release 
        {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions 
    {
        sourceCompatibility JavaVersion.VERSION_15
        targetCompatibility JavaVersion.VERSION_15
    }
}

dependencies 
{

    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'

    implementation 'com.google.android.gms:play-services-location:19.0.0'
    implementation 'com.google.android.gms:play-services-maps:18.0.2'
}

[ build.gradle (Module: 프로젝트명) ] →
[ implementation 'com.google.android.gms:play-services-location:19.0.0' ] 추가 
[ implementation 'com.google.android.gms:play-services-maps:18.0.2' ] 추가

 

[ 해당 이미지를 복사 ] → [ res ] → [ drawable ]  → [ img_google_map.png ] 저장

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/transparent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageView4"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/img_google_map" />
        
</LinearLayout>

[ res ] → [ layout ]  → [ layout_goolge.xml ] 저장

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/textView5"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:gravity="center"
            android:text="위도 : "
            android:textSize="18dp" />

        <TextView
            android:id="@+id/tv_latitude"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="8"
            android:gravity="center|start"
            android:hint="현재 위도가 표시됩니다."
            android:paddingStart="15dp"
            android:textSize="18dp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:gravity="center"
            android:text="경도 : "
            android:textSize="18dp" />

        <TextView
            android:id="@+id/tv_longitude"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="8"
            android:gravity="center|start"
            android:hint="현재 경도가 표시됩니다."
            android:paddingStart="15dp"
            android:textSize="18dp" />
    </LinearLayout>

    <Button
        android:id="@+id/btn_detect_pos"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="위치 감지 시작" />

    <Button
        android:id="@+id/btn_back_detect_pos"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="백그라운드 위치 감지 시작" />

    <fragment
        android:id="@+id/gv_map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="600dp"
        tools:layout="@layout/layout_goolge" />

</LinearLayout>

 

[ res ] → [ layout ]  → [ activity_main ] 저장

 

package com.sample.google.map;

import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;

public class LocationService extends Service
{
	public static final String ACTION_NAME = "com.android.service.LOCATION";

	public static Intent intent;

	private static LocationRequest locationRequest = null;

	private final int LOCATION_SERVICE_ID = 101;

	private NotificationManager notificationManager;

	private NotificationCompat.Builder builder;

	@Nullable
	@Override
	public IBinder onBind(Intent intent)
	{
		return null;
	}

	@Override
	public void onCreate()
	{
		super.onCreate();

		if(locationRequest == null)
		{
			locationRequest = LocationRequest.create();
		}
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		LocationService.intent = intent;

		if(locationRequest != null)
		{
			startLocationBackService();
		}

		return START_STICKY;
	}

	@Override
	public void onDestroy()
	{
		super.onDestroy();

		stopLocationBackService();

	}

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

			double latitude = locationResult.getLastLocation().getLatitude();
			double longitude = locationResult.getLastLocation().getLatitude();

			if(notificationManager != null && builder != null)
			{
				builder.setContentText("위도 : " + latitude + " 경도 : " + longitude);
				notificationManager.notify(LOCATION_SERVICE_ID, builder.build());
			}

			Log.i(this.getClass().getName(), "위도 : " + latitude + " 경도 : " + longitude);
		}
	};

	@SuppressLint("MissingPermission")
	private void startLocationBackService()
	{
		locationRequest.setInterval(1000);

		locationRequest.setFastestInterval(1000);

		locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

		LocationServices.getFusedLocationProviderClient(this).requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper());

		builder = getDefaultBuilder();

		// 백그라운드 서비스를 사용할 경우 사용자에게 알리도록 되어있다.
		// startForeground 사용하여 Notification 을 통해 사용자에게 앱이 사용중임을 알려준다.
		startForeground(LOCATION_SERVICE_ID, builder.build());
	}

	private void stopLocationBackService()
	{
		LocationServices.getFusedLocationProviderClient(this).removeLocationUpdates(locationCallback);
		stopForeground(true);
	}

	private NotificationCompat.Builder getDefaultBuilder()
	{
		String channelID = "loc_notification_channel";

		notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

		// API 26 버전 이상부터 알림 통지를 위해서 알림을 받을 수 있는 채널을 생성하여야 한다.
		if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
		{
			if(notificationManager != null && notificationManager.getNotificationChannel(channelID) == null)
			{
				// IMPORTANCE_HIGH : 알림의 중요도 설정
				NotificationChannel notificationChannel = new NotificationChannel
				(
					channelID,
					"location Notification Channel",
					NotificationManager.IMPORTANCE_NONE
				);

				notificationChannel.setDescription("지도 알림 채널");
				notificationChannel.setSound(null, null);
				notificationChannel.setShowBadge(false);
				notificationChannel.setVibrationPattern(new long[]{0});
				notificationChannel.enableVibration(true);

				notificationManager.createNotificationChannel(notificationChannel);
			}
		}

		Intent resultIntent = new Intent();

		PendingIntent pendingIntent = null;

		// FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE : 이미 존재할경우 해당 인텐트로 대체
		pendingIntent = PendingIntent.getActivity(getApplicationContext(), 1, resultIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

		NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channelID);

		builder.setSmallIcon(R.mipmap.ic_launcher);

		builder.setContentTitle("맵");

		builder.setDefaults(NotificationCompat.DEFAULT_SOUND);

		builder.setVibrate(new long[]{- 1});

		builder.setOnlyAlertOnce(true);

		builder.setContentText("맵 정보 호출중입니다.");

		builder.setContentIntent(pendingIntent);

		builder.setAutoCancel(false);

		builder.setPriority(NotificationCompat.PRIORITY_HIGH);

		return builder;
	}
}

[ com.sample.google.map ] → [ LocationService.java ] 저장

 

package com.sample.google.map;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;

public class MainActivity extends AppCompatActivity
{
	private final int REQUEST_MAP_ACCESS = 1005;

	private Activity activity;

	private TextView tv_latitude, tv_longitude;

	private Button btn_detect_pos;

	private Button btn_back_detect_pos;

	private Boolean locDetectStatus = false;

	private Boolean locBackDetectStatus = false;

	private LocationRequest locationRequest;

	private Intent locIntent;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);

		try
		{
			setContentView(R.layout.activity_main);

			init();

			setting();

			addListener();
		}
		catch(Exception ex)
		{
			Log.e(this.getClass().getName(), ex.getMessage(), ex);
		}
	}

	private void init()
	{
		activity = this;

		tv_latitude = findViewById(R.id.tv_latitude);

		tv_longitude = findViewById(R.id.tv_longitude);

		btn_detect_pos = findViewById(R.id.btn_detect_pos);

		btn_back_detect_pos = findViewById(R.id.btn_back_detect_pos);
	}

	private void setting()
	{
		locationRequest = LocationRequest.create();
	}

	private void addListener()
	{
		tv_latitude = findViewById(R.id.tv_latitude);

		tv_longitude = findViewById(R.id.tv_longitude);

		btn_detect_pos.setOnClickListener(listener_detect_pos);

		btn_back_detect_pos.setOnClickListener(listener_back_detect_pos);
	}

	private final View.OnClickListener listener_back_detect_pos = new View.OnClickListener()
	{
		@Override
		public void onClick(View v)
		{
			// shouldShowRequestPermissionRationale : 사용자가 명시적으로 권한을 거부한 경우 true 가 발생
			// ACCESS_BACKGROUND_LOCATION 의 경우 항상 명시적 권한을 거부한 경우로 처리하기 떄문에
			// 사용자가 직접 설정에서 변경해주어야 하며 항상 허용 설정이 설정하여야 한다.
			if(ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_BACKGROUND_LOCATION))
			{
				requestUserPermission("백그라운드 위치 확인을 위해\n사용자가 직접 위치에 대한 권한을\n항상 허용으로 설정할 필요가 있습니다.\n변경화면으로 이동하시겠습니까?");
			}
			else
			{
				checkLocationIntent();

				locBackDetectStatus = ! locBackDetectStatus;

				if(locBackDetectStatus)
				{
					startService(locIntent);

					btn_back_detect_pos.setText("백그라운드 위치 감지 중지");
				}
				else
				{
					stopService(locIntent);

					btn_back_detect_pos.setText("백그라운드 위치 감지 시작");
				}
			}
		}
	};

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

			double latitude = locationResult.getLastLocation().getLatitude();

			double longitude = locationResult.getLastLocation().getLongitude();

			tv_latitude.setText(String.valueOf(latitude));

			tv_longitude.setText(String.valueOf(longitude));

			SupportMapFragment supportMapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.gv_map);

			if(supportMapFragment != null)
			{
				supportMapFragment.getMapAsync(new OnMapReadyCallback()
				{
					@Override
					public void onMapReady(@NonNull GoogleMap googleMap)
					{
						LatLng myPosition = new LatLng(latitude, longitude);

						googleMap.addMarker(new MarkerOptions().position(myPosition).title("나의 위치"));

						googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(myPosition, 16));
					}
				});
			}
		}
	};

	private final View.OnClickListener listener_detect_pos = new View.OnClickListener()
	{
		@SuppressLint("MissingPermission")
		@Override
		public void onClick(View v)
		{
			if(checkMapPermission())
			{
				locDetectStatus = ! locDetectStatus;

				if(locDetectStatus)
				{
					locationRequest.setInterval(1000);
					locationRequest.setFastestInterval(1000);
					locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

					LocationServices.getFusedLocationProviderClient(activity)
					                .requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper());

					btn_detect_pos.setText("위치 감지 중지");

					Toast.makeText(activity, "위치 감지를 시작합니다.", Toast.LENGTH_SHORT).show();
				}
				else
				{
					LocationServices.getFusedLocationProviderClient(activity)
					                .removeLocationUpdates(locationCallback);

					btn_detect_pos.setText("위치 감지 시작");

					Toast.makeText(activity, "위치 감지를 중지합니다.", Toast.LENGTH_SHORT).show();
				}
			}
		}
	};

	@Override
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
	{
		super.onRequestPermissionsResult(requestCode, permissions, grantResults);

		if(grantResults.length > 0)
		{
			if(requestCode == REQUEST_MAP_ACCESS)
			{
				if
				(
					grantResults[0] != PackageManager.PERMISSION_GRANTED ||
					grantResults[1] != PackageManager.PERMISSION_GRANTED
				)
				{
					requestUserPermission("위치 접근 권한이 거부된 상태입니다.\n해당 기능 권한 취득을 위해 앱 정보\n화면에서 권한 변경이 필요합니다.\n이동하시겠습니까?");
				}
			}
		}
	}

	private Boolean checkMapPermission()
	{
		// ACCESS_FINE_LOCATION : 기기의 위치 추정치 데이터 접근 권한 요청
		// ACCESS_COARSE_LOCATION  : 최대한 정확한 기기의 위치 추정치 데이터 접근 권한 요청
		// ACCESS_BACKGROUND_LOCATION : 포그라운드 실행의 경우 문제 없으나 서비스 등을 이용해 백그라운드 에서 위치 접근시 권한 필요(API 31 버전부터)
		if
		(
			checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
			checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
		)
		{
			String[] permissions =
			{
				Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION
			};
			requestPermissions(permissions, REQUEST_MAP_ACCESS);
		}
		else
		{
			return true;
		}


		return false;
	}

	private void checkLocationIntent()
	{
		if(LocationService.intent == null)
		{
			locIntent = new Intent(LocationService.ACTION_NAME);
			locIntent.setPackage(getPackageName());
		}
		else
		{
			locIntent = LocationService.intent;
		}
	}

	private void requestUserPermission(String message)
	{
		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
		builder.setMessage(message);

		builder.setPositiveButton("예", new DialogInterface.OnClickListener()
		{
			@Override
			public void onClick(DialogInterface dialog, int which)
			{
				// 어플리케이션 디테일 화면 이동
				Intent appDetail = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + getPackageName()));

				// 카테고리를 디폴트로 지정(시스템 카테고리)
				appDetail.addCategory(Intent.CATEGORY_DEFAULT);

				// 현재 작업을 백그라운도르 이동시키고 새 작업 실행
				appDetail.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

				startActivity(appDetail);
			}
		});

		builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialog, int which)
			{
				dialog.dismiss();
			}
		});

		builder.setCancelable(false);
		builder.show();
	}
}

[ com.sample.google.map ] → [ MainActivity.java ] 저장

 

[ Google Developers Console ] 구글 검색
[ 프로젝트 만들기 ] 클릭
[ 만들기 ] 클릭
[ 왼쪽 상단 메뉴 바 ] &rarr; [ Gooogle Maps Platform ] &rarr; [ API ]
[ API ] &rarr; [ 프로젝트 선택 ] &rarr; [ 생성 프로젝트 선택 ] &rarr; [ 열기 ]
[ Maps SDK for Android ] 선택
[ 사용 ] 선택
[ 왼쪽 상단 메뉴 바 ] &rarr; [ API 및 서비스 ] &rarr; [ 사용자 인증 정보 ]
[ 사용자 인증 정보 만들기 ] &rarr; [ API 키 ]
[ 인증 API 키 복사 ] &rarr; [ 키 제한 ]
[ cmd 실행 ] &rarr; [ keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android ] 명령어 붙여넣기 &rarr; SHA1 키 복사
[ API 키 이름 입력 ] &rarr; [ 제한 사항 Android 앱 선택 후 항목 추가 ] &rarr; [ 생성한 안드로이드 프로젝트 패키지 명 및 CMD 에서 확인한 SHA1 키 입력 ] &rarr; [ 저장]

<resources>
    <string name="app_name">GoogleMapSample</string>
    <string name="google_map_key" templateMergeStrategy="preserve" translatable="false">AIzaSyBJuwo93NhuvUxiag18llbfK80-q8NbHvY</string>
</resources>

[ values ] → [ strings.xml ] API 키 저장

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sample.google.map">

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

    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.GoogleMapSample">

        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="@string/google_map_key" />

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".LocationService"
            android:enabled="true"
            android:exported="false">
            <intent-filter>
                <action android:name="com.android.service.LOCATION" />
            </intent-filter>
        </service>

    </application>

</manifest>

[ manifests ] → [ AndroidManifest.xml ] 저장

위치 감지 시작 버튼 클릭시 나의 위치 감지
백그라운드 위치 감지 버튼 클릭시 푸시로 나의 위치 전송