🟨 목 차 🟨
1. API
1-1. REST API
2. 실제 API 적용하기
2-1. 위도와 경도를 정해주고 그 위치를 보여주는 기능
2-2. 현재 위치를 지도에 표시하기
3. 느낀점
나는 중고거래 앱을 만들면서 현재 위치 기능을 추가함으로써 가까운 사람끼리 거래 기능을 만들고 싶었다.
그러긴 위해서 지도 API를 제공하는곳에서 지도 서비스를 받아와 쓰고 거기에 현재 위치 기능을 추가해야했다.
구글에 워낙 많은 자료가 있기에 구체적인 사진 첨부는 하지 않겠다. 내가 구글 맵 API를 사용을 하면서 참고를 많이 한 블로그를 첨부하겠다.
구글 맵 API를 사용하기에 앞서 API에 대해서 알아보자.
◼️ 1. API ◼️
API : 정의 및 프로토콜 집합을 사용하여 두 소프트웨어 구성 요소가 서로 통신할 수 있게 하는 메커니즘입니다. 예를 들어, 기상청의 소프트웨어 시스템에는 일일 기상 데이터가 들어 있습니다. 휴대폰의 날씨 앱은 API를 통해 이 시스템과 "대화"하고 휴대폰에 매일 최신 날씨 정보를 표시합니다.
API 통합 : API 통합은 클라이언트와 서버 간의 데이터를 자동으로 업데이트하는 소프트웨어 구성 요소입니다. API 통합의 몇 가지 예로 휴대폰 이미지 갤러리에서 클라우드로 데이터 자동 동기화 또는 다른 시간대 여행 시 노트북에서 시간 및 날짜 자동 동기화가 있습니다. 기업은 또한 API 통합을 사용하여 많은 시스템 함수를 효율적으로 자동화할 수 있습니다.
API 유형 : API는 아키텍처와 사용 범위에 따라 분류된다.
- 프라이빗 API : 프라이빗 API는 기업 내부에 있으며 비즈니스 내에서 시스템과 데이터를 연결하는 데만 사용됩니다.
- 퍼블릭 API : 퍼블릭 API는 일반에 공개되며 누구나 사용할 수 있습니다. 이러한 유형의 API와 관련된 권한 부여와 비용이 있을 수도 있고 없을 수도 있습니다.
- 파트너 API : 이는 B2B 파트너십을 지원하기 위해 권한이 부여된 외부 개발자만 액세스할 수 있습니다.
- 복합 API : 복합 API는 두 개 이상의 서로 다른 API를 결합하여 복잡한 시스템 요구 사항이나 동작을 처리합니다.
API 생성 방법
- API 계획 : OpenAPI와 같은 API 사양은 API 설계를 위한 블루프린트를 제공합니다. 다양한 사용 사례를 미리 생각하고 API가 현재 API 개발 표준을 준수하는지 확인하는 것이 좋습니다.
- API 빌드 : API 디자이너는 상용 코드를 사용하여 API 프로토타입을 생성합니다. 프로토타입이 테스트되면 개발자는 내부 사양에 맞게 이를 사용자 지정할 수 있습니다.
- API 테스트 : API 테스트는 소프트웨어 테스트와 동일하며 버그 및 결함을 방지하기 위해 수행되어야 합니다. API 테스트 도구로 사이버 공격에 대비하여 API를 강화할 수 있습니다.
- API 문서화 : API는 그 자체로 설명이 필요 없지만 API 문서는 사용 편의성은 높이는 가이드 역할을 합니다. 다양한 기능과 사용 사례를 제공하는 잘 문서화된 API는 서비스 지향 아키텍처에서 더 많이 사용되는 경향이 있습니다.
- API 마케팅 : Amazon이 소매용 온라인 마켓플레이스인 것처럼 API 마켓플레이스는 개발자가 다른 API를 사고 팔기 위해 존재합니다. API를 나열하여 수익을 창출할 수 있습니다.
API 사용 방법
- API 키를 받습니다. API 공급 업체의 확인을 받은 계정을 생성하면 됩니다.
- HTTP API 클라이언트를 설정합니다. 이 도구를 사용하면 수신된 API 키를 사용하여 API 요청을 쉽게 구성할 수 있습니다.
- API 클라이언트가 없는 경우 API 설명서를 참조하여 브라우저에서 요청을 직접 구성할 수 있습니다.
- 새 API 구문에 익숙해지면 코드에서 이를 사용하기 시작할 수 있습니다.
API 작동방식 : API 아키텍처는 일반적으로 클라이언트와 서버 측면에서 설명됩니다. 요청을 보내는 애플리케이션을 클라이언트라고 하고 응답을 보내는 애플리케이션을 서버라고 합니다. 따라서 날씨 예에서 기상청의 날씨 데이터베이스는 서버이고 모바일 앱은 클라이언트입니다.
- SOAP API : 이 API는 단순 객체 접근 프로토콜을 사용합니다. 클라이언트와 서버는 XML을 사용하여 메시지를 교환합니다. 과거에 더 많이 사용되었으며 유연성이 떨어지는 API입니다.
- RPC API : 이 API를 원격 프로시저 호출이라고 합니다. 클라이언트가 서버에서 함수나 프로시저를 완료하면 서버가 출력을 클라이언트로 다시 전송합니다.
- Websocket API : Websocket API는 JSON 객체를 사용하여 데이터를 전달하는 또 다른 최신 웹 API 개발입니다. WebSocket API는 클라이언트 앱과 서버 간의 양방향 통신을 지원합니다. 서버가 연결된 클라이언트에 콜백 메시지를 전송할 수 있어 REST API보다 효율적입니다.
- REST API : 오늘날 웹에서 볼 수 있는 가장 많이 사용되고 유연한 API입니다. 클라이언트가 서버에 요청을 데이터로 전송합니다. 서버가 이 클라이언트 입력을 사용하여 내부 함수를 시작하고 출력 데이터를 다시 클라이언트에 반환합니다.
오늘날에서 가장 많이 사용하는 API는 REST API이고, 가장 중요한 API이다. REST API에 대해 구체적으로 알아보자면.
◼️ 1-1. REST API ◼️
REST는 Representational State Transfer의 줄임말입니다. REST는 클라이언트가 서버 데이터에 액세스하는 데 사용할 수 있는 GET, PUT, DELETE 등의 함수 집합을 정의합니다. 클라이언트와 서버는 HTTP를 사용하여 데이터를 교환합니다.
REST API의 주된 특징은 무상태입니다. 무상태는 서버가 요청 간에 클라이언트 데이터를 저장하지 않음을 의미합니다. 서버에 대한 클라이언트 요청은 웹 사이트를 방문하기 위해 브라우저에 입력하는 URL과 유사합니다. 서버의 응답은 웹 페이지의 일반적인 그래픽 렌더링이 없는 일반 데이터입니다.
REST API의 구성
- 자원 (RESOURCE): URI
- 행위(Verb) : HTTP METHOD
- 표현(Representations)
REST API의 특징
- Uniform (유니폼 인터페이스)
Uniform Interface는 URI로 지정한 리소스에 대한 조작을 통일되고 한정적인 인터페이스로 수행하는 아키텍처 스타일을 말합니다. - Stateless (무상태성)
REST는 무상태성 성격을 갖습니다. 다시 말해 작업을 위한 상태정보를 따로 저장하고 관리하지 않습니다. 세션 정보나 쿠키정보를 별도로 저장하고 관리하지 않기 때문에 API 서버는 들어오는 요청만을 단순히 처리하면 됩니다. 때문에 서비스의 자유도가 높아지고 서버에서 불필요한 정보를 관리하지 않음으로써 구현이 단순해집니다. - Cacheable (캐시 가능)
REST의 가장 큰 특징 중 하나는 HTTP라는 기존 웹표준을 그대로 사용하기 때문에, 웹에서 사용하는 기존 인프라를 그대로 활용이 가능합니다. 따라서 HTTP가 가진 캐싱 기능이 적용 가능합니다. HTTP 프로토콜 표준에서 사용하는 Last-Modified태그나 E-Tag를 이용하면 캐싱 구현이 가능합니다. - Self-descriptiveness (자체 표현 구조)
REST의 또 다른 큰 특징 중 하나는 REST API 메시지만 보고도 이를 쉽게 이해 할 수 있는 자체 표현 구조로 되어 있다는 것입니다. - Client - Server 구조
REST 서버는 API 제공, 클라이언트는 사용자 인증이나 컨텍스트(세션, 로그인 정보)등을 직접 관리하는 구조로 각각의 역할이 확실히 구분되기 때문에 클라이언트와 서버에서 개발해야 할 내용이 명확해지고 서로간 의존성이 줄어들게 됩니다. - 계층형 구조
REST 서버는 다중 계층으로 구성될 수 있으며 보안, 로드 밸런싱, 암호화 계층을 추가해 구조상의 유연성을 둘 수 있고 PROXY, 게이트웨이 같은 네트워크 기반의 중간매체를 사용할 수 있게 합니다.
REST API의 이점
- 통합 : API는 새로운 애플리케이션을 기존 소프트웨어 시스템과 통합하는 데 사용됩니다. 그러면 각 기능을 처음부터 작성할 필요가 없기 때문에 개발 속도가 빨라집니다. API를 사용하여 기존 코드를 활용할 수 있습니다.
- 혁신 : 새로운 앱의 등장으로 전체 산업이 바뀔 수 있습니다. 기업은 신속하게 대응하고 혁신적인 서비스의 신속한 배포를 지원해야 합니다. 전체 코드를 다시 작성할 필요 없이 API 수준에서 변경하여 이를 수행할 수 있습니다.
- 확장 : API는 기업이 다양한 플랫폼에서 고객의 요구 사항을 충족할 수 있는 고유한 기회를 제공합니다. 예를 들어 지도 API를 사용하면 웹 사이트, Android, iOS 등을 통해 지도 정보를 통합할 수 있습니다. 어느 기업이나 무료 또는 유료 API를 사용하여 내부 데이터베이스에 유사한 액세스 권한을 부여할 수 있습니다.
- 유지 관리의 용이성 : API는 두 시스템 간의 게이트웨이 역할을 합니다. API가 영향을 받지 않도록 각 시스템은 내부적으로 변경해야 합니다. 이렇게 하면 한 시스템의 향후 코드 변경이 다른 시스템에 영향을 미치지 않습니다.
REST API의 보호
- 인증 토큰 : 인증 토큰은 사용자에게 API 호출을 수행할 수 있는 권한을 부여하는 데 사용됩니다. 인증 토큰은 사용자가 자신이 누구인지 확인하고 해당 특정 API 호출에 대한 액세스 권한이 있는지 확인합니다. 예를 들어, 이메일 서버에 로그인하면 이메일 클라이언트는 보안 액세스를 위해 인증 토큰을 사용합니다.
- API 키 : API 키는 API를 호출하는 프로그램 또는 애플리케이션을 확인합니다. 즉, 애플리케이션을 식별하고 애플리케이션에 특정 API 호출을 수행하는 데 필요한 액세스 권한이 있는지 확인합니다. API 키는 토큰만큼 안전하지 않지만 사용량에 대한 데이터를 수집하기 위해 API 모니터링을 허용합니다. 다른 웹 사이트를 방문할 때 브라우저 URL에서 긴 문자열과 숫자를 본 적이 있을 것입니다. 이 문자열은 웹 사이트가 내부 API 호출을 수행하는 데 사용하는 API 키입니다.
◼️ 2. 실제 API 적용하기 ◼️
◼️ 2-1 위도와 경도를 정해주고 그 위치를 보여주는 기능 ◼️
실제로 구글 맵 API를 적용 시켜 보겠다. 구글에 한 예제를 보며 실습을 했다. 대략적인 말로만 설명을 하겠다.
- Google Developers Console 사이트(https://console.developers.google.com/apis/dashboard)에 접속하여 프로젝트 만들기를 클릭한다.
- 프로젝트 이름을 적고 프로젝트를 생성한다.
- 구글 맵 안드로이드 API를 사용하기 위한 추가설정을 위해 API 및 사용 서비스 사용 설정을 클릭하여 들어간다.
- maps sdk for android를 검색하여 Maps SDK for Anroid를 선택 후 사용을 클릭하여 API를 활성화 시킨다.
- API를 활성화가 되어 있으면 왼쪽 메뉴에 있는 사용자 인증 정보를 선택하고 사용자 인증 정보 만들기를 선택한다.
- 사용자 인증 정보 만들기를 선택하면 메뉴 중에 API키 를 선택 후 API키를 수정 을 클릭한다.
- API키 수정을 클릭하면 API키 사용 제한을 둘 수 있는 창이 보이는데 여기서 애플리케이션 제한사항, API 제한사항을 설정 할 수 있다.
- API키 수정 - 어플리케이션 제한사항 에서 Android 앱 항목을 선택하고 항목 추가를 클릭한다.
- 항목 추가를 하게 되면 Google Maps Andorid API를 사용 할 Google Maps Andorid API의 프로젝트 패키지 이름과 Android Studio에서 생성된 SHA-1인증서 지문을 넣고 완료를 클릭한다.
[ 맥북 사용자 SHA- 1 인증서 얻는법 : 터미널에서 keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android 를 입력한다. 내 경우에 자꾸 형식에 맞지 않는다며 오류가 떴었으나 알고보니 터미널로 볼 때 한 칸 씩 밀려서 SHA1 값이 MD5로 보여지고, SHA256 값이 SHA1 값으로 보여지는 것이다. 즉, 형식이 맞지 않는다고 오류뜨는 사람은 MD5 인증서 값으로 다시 한번 시도 해보면 좋을 것이다.] - 애플리케이션 제한 사항을 선택했으니 이번엔 API 제한사항으로 가서 키 제한을 선택하고 Maps SDK for Android를 선택하고 클릭한다.
- 모든 설정을 완료했으므로 저장을 클릭하여 준다.(시간이 조금 소요된다.)
- 저장을 하고 기다리면 구글 맵 안드로이드 API를 사용하기 위한 API 키를 부여하여 따로 복사하여 준다.
- 부여받은 API키는 안드로이드 스튜디오의 AndroidManifest.xml의 <application> 태그 하위 요소로 <meta-data>태그를 사용하여 넣어준다.
ex) <meta-data
android:name = "com.example.shopproject"
android:value = "부여받은 API 키 넣기"
/> - Google Maps Andorid API 를 사용하기 위해서는 다른 작업을 해줘야 하는데 그것은 Android Studio의 Google Play services 라이브러리 패키지를 설치해줘야한다. 방법은 Android Studio의 내가 사용 할 프로젝트를 들어가서 SDK Tools 탭에 들어가 Google Play services 항목을 체크하고 OK를 클릭하여 설치를 진행한다.
- 또한 build.gradle(Moudle)에다가
implementation 'com.google.android.gms:play-services-maps:18.0.2'
implementation 'com.google.android.gms:play-services-location:20.0.0' 를 입력해주고 Sync now(동기화)를 해준다.
[ 참고 : compile 과 implementation의 차이 : https://bluayer.com/13 ] - 이번 작업은 구글 맵 지도가 표시 될 xml 파일을 수정하는 것인데, 구글 맵 지도가 표시 될 xml 파일에 표시 될 구글 지도를 <fragment>태그를 이용하여 적용시켜준다.
ex)
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/map"
tools:context=".MapsActivity"
android:name="com.google.android.gms.maps.SupportMapFragment" /> - 다음으로는 java 파일을 수정하는것이다
[ 안드로이드 구글 맵 Marker Option 사용법 참고 : https://mailmail.tistory.com/19 ]
ex)
public class MainActivity extends AppCompatActivity
implements OnMapReadyCallback {
private GoogleMap mMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map); // getMapAsync메소드 실행을 위한 SupportMapFragment 핸들을 가져온다.
mapFragment.getMapAsync(this); // GoogleMap 객체가 준비 될 때 실행될 콜백을 등록한다.
//getMapAsync() 메소드가 메인 쓰레드에서 호출되어야 메인 쓰레드에서 onMapReady 콜백이 실행된다.
}
@Override
public void onMapReady(final GoogleMap googleMap) {
mMap = googleMap;
LatLng SEOUL = new LatLng(37.56, 126.97);
MarkerOptions markerOptions = new MarkerOptions();
markerOptions.position(SEOUL); // 마커가 표시 될 위치
markerOptions.title("서울"); //마커의 제목
markerOptions.snippet("한국의 수도"); //마커의 내용
mMap.addMarker(markerOptions);
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(SEOUL, 10)); //카메라를 지정한 경도, 위도로 이동시 이동시켜준다. 안에 넣는 변수를 1로 지정하면 세계 지도 수준으로 보이면 숫자가 커질수록 상세지도가 보인다.
}
}
◼️ 2-2 현재 위치를 지도에 표시하기 ◼️
FusedLocationProviderClient를 사용하여 Google Map에 현재 위치를 표시한다.
현재 위치를 표시해주기 위해서 필요한 사항
- 현재 위치를 지도 상에 마커로 표시
- 기기의 위치 서비스(GPS)가 비활성화 되어 있을 경우 활성화되도록 요구해야 한다.
- 현재 위치가 변경되면 카메라가 이동하여 현재 위치를 중심으로 지도를 보여주게 한다.
- 디바이스의 운영체제 버전이 안드로이드 6.0이상일 경우에는 위치 관련 퍼미션을 런타임에 요구한다.
전반적인 작동 흐름
- 특정한 초기 위치를 지정하고 기기 위치 서비스(GPS)을 위한 퍼미션 허가를 사용자에게 요청(권한 요청)한다.[ Android 6.0 미만 운영체제를 사용하는 안드로이드 디바이스에서는 보이지 않는다, 처음 실행하면 현재 위치를 찾는데 시간이 걸려, 특정한 초기 위치를 우리가 흔히 아는 서울로 지정해준다. 그렇지 않으면 모르는 대륙의 위치가 찍히기 때문이다. , 특정 초기 위치를 설정하는 부분은 onMapReady() 메소드에서 설정한다. 그러면 GPS(기기 위치 정보 서비스)를 사용 못하더라도 서울을 보여준다.]
- 기기 위치 서비스를 활성화시키면 현재 위치가 표시된다. 현재 위치를 표시하는데 파란색 마커와 파란색 동그라미가 표시된다.
마커를 클릭하면 현재 위치의 주소와 좌표가 표시된다. - 기기를 움직이면 현재 위치에 따라서 파란색 마커의 위치가 이동하게 된다.
실행 과정
1. 기기 위치 서비스(GPS) 사용을 위한 퍼미션(권한 요청)을 추가 해주기 위해
Andorid Studio의 메니페스트 파일 AndroidManifest.xml의 <manifest> 태그 하위요소로 <uses-permission> 태그를 사용하여 위치 정보 접근을 위한 퍼미션을 추가한다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
2. build.gradle(Moudle)에 Snackbar를 사용하기 위해
implementation 'com.google.android.material:material:1.4.0-alpha02' 를 입력해주고 Sync now(동기화)를 해준다.
3. Snackbar를 사용하기 위해 지도를 사용 할 레이아웃(xml)에 ID를 추가한다.
4. 현재 위치 지도를 사용 할 Java 코드
[ 안드로이드 구글 맵 Marker Option 사용법 참고 : https://mailmail.tistory.com/19 ]
package com.example.shopproject;
import static android.content.Context.LOCATION_SERVICE;
import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.gms.location.FusedLocationProviderClient;
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.location.LocationSettingsRequest;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.material.snackbar.Snackbar;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
public class Fragment2_near extends Fragment implements OnMapReadyCallback {
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
static String current;
private GoogleMap mMap;
private MapView googlemap = null;
private Marker currentMarker=null;
private static final String TAG = "googlemap_example";
private static final int GPS_ENABLE_REQUEST_CODE = 2001;
private static final int UPDATE_INTERVAL_MS = 1000; //1초
private final int FASTEST_UPDATE_INTERVAL_MS = 500; // 0.5초
// onRequestPermissionsResult에서 수신된 결과에서
// ActivityCompat.requestPermissions를 사용한 퍼미션 요청을 구별하기 위해 사용됩니다.
private static final int PERMISSIONS_REQUEST_CODE=100;
boolean needRequest =false;
// 앱을 실행하기 위해 필요한 퍼미션을 정의합니다.
String[]REQUIRED_PERMISSIONS={Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION}; //외부저장소
Location mCurrentLocatiion;
LatLng currentPosition;
private View mLayout; // Snackbar 사용하기 위해서는 View가 필요합니다.
// (참고로 Toast에서는 Context가 필요했습니다.)
private FusedLocationProviderClient mFusedLocationClient;
private LocationRequest locationRequest;
private Location location;
private Button fra2;
private Bundle savedInstanceState;
SendEventListener sendEventListener;
@Override
public void onAttach(@NonNull Context context) { // Fragment에서 Activity로 값을 전달하기 위한 함수
super.onAttach(context);
try{
sendEventListener = (SendEventListener) context;
}catch (ClassCastException e) {
throw new ClassCastException(context.toString()+"must implements SendEventlistener");
}
}
public Fragment2_near() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment2_near, container, false);
getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mLayout = view.findViewById(R.id.map_layout);
locationRequest = new com.google.android.gms.location.LocationRequest()
.setPriority(com.google.android.gms.location.LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
.setInterval(UPDATE_INTERVAL_MS)
.setFastestInterval(FASTEST_UPDATE_INTERVAL_MS);
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
builder.addLocationRequest(locationRequest);
mFusedLocationClient= LocationServices.getFusedLocationProviderClient(getContext());
googlemap = (MapView) view.findViewById(R.id.map);
googlemap.getMapAsync(this);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(googlemap!=null){
googlemap.onCreate(savedInstanceState);
}
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG, "onStart");
if (checkPermission()) {
Log.d(TAG, "onStart : call mFusedLocationClient.requestLocationUpdates");
mFusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null);
if (mMap!=null)
mMap.setMyLocationEnabled(true);
}
}
@Override
public void onStop() {
super.onStop();
if (mFusedLocationClient != null) {
Log.d(TAG, "onStop : call stopLocationUpdates");
mFusedLocationClient.removeLocationUpdates(locationCallback);
}
}
@Override
public void onResume(){
super.onResume();
googlemap.onResume();
}
@Override
public void onMapReady(GoogleMap googleMap) {
Log.d(TAG,"onMapReady");
mMap = googleMap;
//런타임 퍼미션 요청 대화상자나 GPS 활성 요청 대화상자 보이기전에
//지도의 초기위치를 서울로 이동
setDefaultLocation();
//런타임 퍼미션 처리
// 1. 위치 퍼미션을 가지고 있는지 체크합니다.
int hasFineLocationPermission = ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.ACCESS_FINE_LOCATION);
int hasCoarseLocationPermission = ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.ACCESS_COARSE_LOCATION);
if (hasFineLocationPermission == PackageManager.PERMISSION_GRANTED &&
hasCoarseLocationPermission == PackageManager.PERMISSION_GRANTED ) {
// 2. 이미 퍼미션을 가지고 있다면
// ( 안드로이드 6.0 이하 버전은 런타임 퍼미션이 필요없기 때문에 이미 허용된 걸로 인식합니다.)
startLocationUpdates(); // 3. 위치 업데이트 시작
}else { //2. 퍼미션 요청을 허용한 적이 없다면 퍼미션 요청이 필요합니다. 2가지 경우(3-1, 4-1)가 있습니다.
// 3-1. 사용자가 퍼미션 거부를 한 적이 있는 경우에는
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), REQUIRED_PERMISSIONS[0])) {
// 3-2. 요청을 진행하기 전에 사용자가에게 퍼미션이 필요한 이유를 설명해줄 필요가 있습니다.
Snackbar.make(mLayout, "이 앱을 실행하려면 위치 접근 권한이 필요합니다.",
Snackbar.LENGTH_INDEFINITE).setAction("확인", new View.OnClickListener() {
@Override
public void onClick(View view) {
// 3-3. 사용자게에 퍼미션 요청을 합니다. 요청 결과는 onRequestPermissionResult에서 수신됩니다.
ActivityCompat.requestPermissions(getActivity(), REQUIRED_PERMISSIONS,
PERMISSIONS_REQUEST_CODE);
}
}).show();
} else {
// 4-1. 사용자가 퍼미션 거부를 한 적이 없는 경우에는 퍼미션 요청을 바로 합니다.
// 요청 결과는 onRequestPermissionResult에서 수신됩니다.
ActivityCompat.requestPermissions(getActivity(), REQUIRED_PERMISSIONS,
PERMISSIONS_REQUEST_CODE);
}
}
mMap.getUiSettings().setMyLocationButtonEnabled(true);
// 현재 오동작을 해서 주석처리
//mMap.animateCamera(CameraUpdateFactory.zoomTo(15));
mMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() {
@Override
public void onMapClick(LatLng latLng) {
Log.d( TAG, "onMapClick :");
}
});
}
final LocationCallback locationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
List<Location> locationList = locationResult.getLocations();
if (locationList.size() > 0) {
location = locationList.get(locationList.size() - 1);
//location = locationList.get(0);
currentPosition
= new LatLng(location.getLatitude(), location.getLongitude());
String markerTitle = getCurrentAddress(currentPosition); //makerTitle 변수에 현재 위치 주소를 넣는다.
String markerSnippet = "위도:" + String.valueOf(location.getLatitude())
+ " 경도:" + String.valueOf(location.getLongitude());
Log.d(TAG, "onLocationResult : " + markerSnippet);
//현재 위치에 마커 생성하고 이동
setCurrentLocation(location, markerTitle, markerSnippet);
mCurrentLocatiion = location;
Log.d("markerTitle : " , markerTitle); // 현재 위치 정보를 가지고 있는 markerTitle 로그 값 확인
String[] array = markerTitle.split(","); // markerTitle(현재 위치 정보)의 값을 split 메소드를 이용하여 , 기준으로 자른다. (, 기준 4등분 나옴)
for (int i = 0; i < array.length; i++) { // array의 크기만큼 for문을 돌린다.
Log.d("array[2]의 값 : ", array[2]); //markerTitle(현재 위치 정보)의 값이 , 기준으로 4등분이므로 그것의 지역 값인 array[2]의 값의 로그를 띄운다.
current = array[2]; // array[2]의 값을 변수 current의 값에 넣는다.
}
sendEventListener.sendmail(current); // 인터페이스를 이용하여 현재 구글 맵을 보여주는 Fragment 에서 current(현재 지역 위치 값)을 activity로 전달한다.
}
}
};
private void startLocationUpdates() {
if (!checkLocationServicesStatus()) {
Log.d(TAG, "startLocationUpdates : call showDialogForLocationServiceSetting");
showDialogForLocationServiceSetting();
}else {
int hasFineLocationPermission = ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.ACCESS_FINE_LOCATION);
int hasCoarseLocationPermission = ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.ACCESS_COARSE_LOCATION);
if (hasFineLocationPermission != PackageManager.PERMISSION_GRANTED ||
hasCoarseLocationPermission != PackageManager.PERMISSION_GRANTED ) {
Log.d(TAG, "startLocationUpdates : 퍼미션 안가지고 있음");
return;
}
Log.d(TAG, "startLocationUpdates : call mFusedLocationClient.requestLocationUpdates");
mFusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper());
if (checkPermission())
mMap.setMyLocationEnabled(true);
}
}
private boolean checkPermission() {
int hasFineLocationPermission = ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.ACCESS_FINE_LOCATION);
int hasCoarseLocationPermission = ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.ACCESS_COARSE_LOCATION);
if (hasFineLocationPermission == PackageManager.PERMISSION_GRANTED &&
hasCoarseLocationPermission == PackageManager.PERMISSION_GRANTED ) {
return true;
}
return false;
}
private void showDialogForLocationServiceSetting() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
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();
}
});
builder.create().show();
}
private boolean checkLocationServicesStatus() {
LocationManager locationManager = (LocationManager)getActivity().getSystemService(LOCATION_SERVICE);
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|| locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
private void setDefaultLocation() {
//디폴트 위치, Seoul
LatLng DEFAULT_LOCATION = new LatLng(37.56, 126.97);
String markerTitle = "위치정보 가져올 수 없음";
String markerSnippet = "위치 퍼미션과 GPS 활성 요부 확인하세요";
if (currentMarker != null) currentMarker.remove();
MarkerOptions markerOptions = new MarkerOptions();
markerOptions.position(DEFAULT_LOCATION);
markerOptions.title(markerTitle);
markerOptions.snippet(markerSnippet);
markerOptions.draggable(true);
markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED));
currentMarker = mMap.addMarker(markerOptions);
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(DEFAULT_LOCATION, 15);
mMap.moveCamera(cameraUpdate);
}
public String getCurrentAddress(LatLng latlng) { //현재 주소를 받아오는 메소드
//지오코더... GPS를 주소로 변환
Geocoder geocoder = new Geocoder(getContext(), Locale.getDefault());
List<Address> addresses;
try {
addresses = geocoder.getFromLocation(
latlng.latitude,
latlng.longitude,
1);
} catch (IOException ioException) {
//네트워크 문제
Toast.makeText(getActivity(), "지오코더 서비스 사용불가", Toast.LENGTH_LONG).show();
return "지오코더 서비스 사용불가";
} catch (IllegalArgumentException illegalArgumentException) {
Toast.makeText(getContext(), "잘못된 GPS 좌표", Toast.LENGTH_LONG).show();
return "잘못된 GPS 좌표";
}
if (addresses == null || addresses.size() == 0) {
Toast.makeText(getContext(), "주소 미발견", Toast.LENGTH_LONG).show();
return "주소 미발견";
} else {
Address address = addresses.get(0);
return address.getAddressLine(0).toString();
}
}
public void setCurrentLocation(Location location, String markerTitle, String markerSnippet) { //현재 위치 저장 메소드
if (currentMarker != null) currentMarker.remove();
LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());
MarkerOptions markerOptions = new MarkerOptions();
markerOptions.position(currentLatLng);
markerOptions.title(markerTitle);
markerOptions.snippet(markerSnippet);
markerOptions.draggable(true);
currentMarker = mMap.addMarker(markerOptions);
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng);
mMap.moveCamera(cameraUpdate); //GoogleMap 에서 제공하는 메소드
}
@Override
public void onRequestPermissionsResult(int permsRequestCode,
@NonNull String[] permissions,
@NonNull int[] grandResults) {
if ( permsRequestCode == PERMISSIONS_REQUEST_CODE && grandResults.length == REQUIRED_PERMISSIONS.length) {
// 요청 코드가 PERMISSIONS_REQUEST_CODE 이고, 요청한 퍼미션 개수만큼 수신되었다면
boolean check_result = true;
// 모든 퍼미션을 허용했는지 체크합니다.
for (int result : grandResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
check_result = false;
break;
}
}
if ( check_result ) {
// 퍼미션을 허용했다면 위치 업데이트를 시작합니다.
startLocationUpdates();
}
else {
// 거부한 퍼미션이 있다면 앱을 사용할 수 없는 이유를 설명해주고 앱을 종료합니다.2 가지 경우가 있습니다.
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), REQUIRED_PERMISSIONS[0])
|| ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), REQUIRED_PERMISSIONS[1])) {
// 사용자가 거부만 선택한 경우에는 앱을 다시 실행하여 허용을 선택하면 앱을 사용할 수 있습니다.
Snackbar.make(mLayout, "퍼미션이 거부되었습니다. 앱을 다시 실행하여 퍼미션을 허용해주세요. ",
Snackbar.LENGTH_INDEFINITE).setAction("확인", new View.OnClickListener() {
@Override
public void onClick(View view) {
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction().remove(Fragment2_near.this).commit();
fragmentManager.popBackStack();
}
}).show();
}else {
// "다시 묻지 않음"을 사용자가 체크하고 거부를 선택한 경우에는 설정(앱 정보)에서 퍼미션을 허용해야 앱을 사용할 수 있습니다.
Snackbar.make(mLayout, "퍼미션이 거부되었습니다. 설정(앱 정보)에서 퍼미션을 허용해야 합니다. ",
Snackbar.LENGTH_INDEFINITE).setAction("확인", new View.OnClickListener() {
@Override
public void onClick(View view) {
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction().remove(Fragment2_near.this).commit();
fragmentManager.popBackStack();
}
}).show();
}
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case GPS_ENABLE_REQUEST_CODE:
//사용자가 GPS 활성 시켰는지 검사
if (checkLocationServicesStatus()) {
if (checkLocationServicesStatus()) {
Log.d(TAG, "onActivityResult : GPS 활성화 되있음");
needRequest = true;
return;
}
}
break;
}
}
}
◼️ 3. 느낀점 ◼️
구글 지도 API를 쓰고 이것을 응용하여 현재 위치까지 받아 올 수 있는 기능을 만들어 보았다. 현재 위치까지는 보여주는 예제들이 많지만 이렇게 보여주는 값들을 내가 원하는 데이터(도시 이름만의 데이터)를 가공하고 이것을 다른 레이아웃(액티비티나 or Fragment)로 전달해주는 것까지 해보았다.
그 과정에서 구글 지도 API를 적용시키는 것까진 쉬웠고, 이것을 내가 원하는 데이터 형태로 만드는것까지 시간이 걸리긴 했지만 완성을 하였다. 내가원하는 데이터 형태로 만들었으니 이것을 다른 레이아웃(액티비티나 or Fragment)로 전달 할 수 있었느나, 이 부분에서 많은 오류가 발생하고 잘 되지 않았다. 왜냐하면
- 첫번째, 나는 화면 전환을 하지 않으며 데이터만 전달을 해야한다 -->가장 쉬운 인텐트 사용이 불가하다.
- 두번째, 나의 상황의 경우 Fragment 에서 액티비티로 데이터 전달을 하는 것이기 때문이다. --> 프래그먼트간의 데이터 전달인 Bundle 을 사용할 수 없다.
- 세번째, Fragment 에서 액티비티로 데이터 전달의 글이 별로 없었고, 그 글들을 다 따라 해봤는데 오류가 발생하였고, 이 오류의 원인을 파악하는데 너무 오래걸렸다.
이 글에서는 API의 글을 다루고 있기 때문에 Fragment 에서 액티비티로 데이터 전달을 하는 글은 다른글로 쓰겠다.
모쪼록 느낀것이 API 를 다룬다고 해서 API를 잘 다루는거 뿐만 아니라 다른 상호작용하는 부분도 잘 알아야 하므로 기초를 튼튼히 해야겠다.