현재 위치 주변의 장소정보를 가져오는 Places SDK for Android의 예제를 실행시켜 보았습니다.
사용한 원본 코드는 아래 링크에 있습니다. v.3.x BETA 버전과 v.2.x 버전 예제가 구분되어 존재합니다.
https://github.com/googlemaps/android-places-demos
2019. 08. 23 최초작성 - Places SDK for Android v.3.0.0 BETA 예제
2021. 01. 09 BETA 버전대신에 v.2.4로 변경
1. 실행 결과
1-1. CURRENT PLACE를 클릭합니다.
1-2. FIND CURRENT PLACE를 터치하면
1-2. 런타임 퍼미션을 물어봅니다. 앱 사용 중에만 허용을 터치하고
1-3. FIND CURRENT PLACE를 다시 터치하면 현재 위치 주변 정보를 20개 가져와 보여줍니다.
주변 장소의 이름과 주소를 보여줍니다.
1-4. Display raw results를 체크하고 다시 FIND CURRENT PLACE를 터치하면
RAW 데이터로 주변 장소 정보를 보여줍니다.
1-5. Manually set Place Fields를 체크하고 FIND CURRENT PLACE를 터치합니다.
1-6. 선택한 PLACE 필드만 선택하여 볼 수 있습니다. ADDRESS만 체크하고 DONE을 터치해봅니다.
1-7. 주소 필드만 보이게 됩니다.
이제 API키를 생성하는 방법을 설명합니다.
2. API 키 생성하기
2-1. Google Developers Console 사이트 ( https://console.developers.google.com/apis/dashboard )에 접속하여 만들기를 클릭합니다.
자주 구성이 바뀌는 편이어서 포스팅에서 진행한 캡쳐 화면과 차이가 있을 수 있습니다.
2-2. 프로젝트 이름을 적어주고 만들기를 클릭합니다.
2-3. API 및 서비스 사용 설정을 클릭합니다.
2-4. 검색창에 places를 입력하면 보이는 Places API를 클릭합니다.
2-5. 사용을 클릭합니다.
2-6. 탐색 메뉴(1)을 누르고 메뉴에서 API 및 서비스 > 사용자 인증 정보를 선택합니다.
2-7. 사용자 인증 정보 만들기를 클릭하고 API키를 선택합니다.
2-8. 키 제한을 클릭합니다.
2-9. Android 앱을 체크하고, 항목 추가를 클릭합니다.
2-10. 다음 두 항목을 붙여넣고 완료를 클릭합니다.
안드로이드 프로젝트를 생성하여 패키지 이름을 복사하여 붙여넣기합니다.
명령 프롬프트에서 다음 명령을 실행하여 SHA1 값을 복사하여 붙여넣기합니다.
keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android |
2-11. 키 제한을 클릭하고 콤보박스에서 Places API를 선택 후, 저장을 클릭합니다.
2-12. API 키를 복사해둡니다.
이제 할당받은 API키를 사용하여 안드로이드 코드를 실행시켜 봅니다.
3. 안드로이드 코드
3-1. Places SDK for Android를 위한 dependency를 app을 위한 build.gradle에 추가하고 Sync Now를 클릭합니다.
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'com.google.android.libraries.places:places:2.4.0' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } |
3-2. AndroidManifest.xml에 다음 권한들을 추가합니다.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tistory.webnautes.placeexample">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<application |
다음 액티비티를 추가합니다.
</activity>
<activity android:name=".CurrentPlaceTestActivity" /> </application> |
3-3. activity_main.xml 파일의 내용을 다음으로 대체합니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="4dp" android:orientation="vertical" tools:context=".MainActivity">
<Button android:id="@+id/current_place_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="2dp" android:text="Current Place"/>
</LinearLayout> |
3-4. current_place_test_activity.xml 파일을 추가하고 다음 내용을 복사하여 붙여넣습니다.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="4dp" tools:context=".CurrentPlaceTestActivity">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">
<CheckBox android:id="@+id/use_custom_fields" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Manually set Place Fields?"/>
<TextView android:id="@+id/custom_fields_list" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"/>
</LinearLayout>
<Button android:id="@+id/find_current_place_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Find Current Place"/>
<CheckBox android:id="@+id/display_raw_results" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="false" android:text="Display raw results?"/>
<ProgressBar android:id="@+id/loading" style="?android:attr/progressBarStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:visibility="invisible"/>
<TextView android:id="@+id/response" android:textIsSelectable="true" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView> |
3-5. FieldSelector.java 파일을 추가하고 다음 내용을 복사하여 붙여넣기 합니다.
package com.tistory.webnautes.placeexample;
import android.content.Context; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.CheckedTextView; import android.widget.ListView; import android.widget.TextView;
import com.google.android.libraries.places.api.model.Place.Field;
import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map;
import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog;
/** Helper class for selecting {@link Field} values. */ public final class FieldSelector { private static final String SELECTED_PLACE_FIELDS_KEY = "selected_place_fields";
private final Map<Field, State> fieldStates;
private final TextView outputView;
/** * Returns all {@link Field} values except those passed in. * * <p>Convenience method for when most {@link Field} values are desired. Useful for APIs that do * no support all {@link Field} values. */ static List<Field> allExcept(Field... placeFieldsToOmit) { // Arrays.asList is immutable, create a mutable list to allow removing fields List<Field> placeFields = new ArrayList<>(Arrays.asList(Field.values())); placeFields.removeAll(Arrays.asList(placeFieldsToOmit));
return placeFields; }
public FieldSelector(CheckBox enableView, TextView outputView, @Nullable Bundle savedState) { this(enableView, outputView, Arrays.asList(Field.values()), savedState); }
public FieldSelector( CheckBox enableView, TextView outputView, List<Field> validFields, @Nullable Bundle savedState) { fieldStates = new HashMap<>(); for (Field field : validFields) { fieldStates.put(field, new State(field)); }
if (savedState != null) { List<Integer> selectedFields = savedState.getIntegerArrayList(SELECTED_PLACE_FIELDS_KEY); if (selectedFields != null) { restoreState(selectedFields); } outputView.setText(getSelectedString()); }
outputView.setOnClickListener( v -> { if (v.isEnabled()) { showDialog(v.getContext()); } });
enableView.setOnClickListener( view -> { boolean isChecked = enableView.isChecked(); outputView.setEnabled(isChecked); if (isChecked) { showDialog(view.getContext()); } else { outputView.setText(""); for (State state : fieldStates.values()) { state.checked = false; } } });
this.outputView = outputView; }
/** * Shows dialog to allow user to select {@link Field} values they want. */ public void showDialog(Context context) { ListView listView = new ListView(context); PlaceFieldArrayAdapter adapter = new PlaceFieldArrayAdapter(context, fieldStates.values()); listView.setAdapter(adapter); listView.setOnItemClickListener(adapter);
new AlertDialog.Builder(context) .setTitle("Select Place Fields") .setPositiveButton( "Done", (dialog, which) -> { outputView.setText(getSelectedString()); }) .setView(listView) .show(); }
/** * Returns all {@link Field} that are selectable. */ public List<Field> getAllFields() { return new ArrayList<>(fieldStates.keySet()); }
/** * Returns all {@link Field} values the user selected. */ public List<Field> getSelectedFields() { List<Field> selectedList = new ArrayList<>(); for (Map.Entry<Field, State> entry : fieldStates.entrySet()) { if (entry.getValue().checked) { selectedList.add(entry.getKey()); } }
return selectedList; }
/** * Returns a String representation of all selected {@link Field} values. See {@link * #getSelectedFields()}. */ public String getSelectedString() { StringBuilder builder = new StringBuilder(); for (Field field : getSelectedFields()) { builder.append(field).append("\n"); }
return builder.toString(); }
public void onSaveInstanceState(@NonNull Bundle bundle) { List<Field> fields = getSelectedFields();
ArrayList<Integer> serializedFields = new ArrayList<>(); for (Field field : fields) { serializedFields.add(field.ordinal()); } bundle.putIntegerArrayList(SELECTED_PLACE_FIELDS_KEY, serializedFields); }
private void restoreState(List<Integer> selectedFields) { for (Integer serializedField : selectedFields) { Field field = Field.values()[serializedField]; State state = fieldStates.get(field); if (state != null) { state.checked = true; } } }
////////////////////////// // Helper methods below // //////////////////////////
/** * Holds selection state for a place field. */ public static final class State { public final Field field; public boolean checked;
public State(Field field) { this.field = field; } }
private static final class PlaceFieldArrayAdapter extends ArrayAdapter<State> implements OnItemClickListener {
public PlaceFieldArrayAdapter(Context context, Collection<State> states) { super(context, android.R.layout.simple_list_item_multiple_choice, new ArrayList<>(states)); }
private static void updateView(View view, State state) { if (view instanceof CheckedTextView) { CheckedTextView checkedTextView = (CheckedTextView) view; checkedTextView.setText(state.field.toString()); checkedTextView.setChecked(state.checked); } }
@NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view = super.getView(position, convertView, parent); State state = getItem(position); updateView(view, state);
return view; }
@Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { State state = getItem(position); state.checked = !state.checked; updateView(view, state); } } } |
3-6. StringUtil.java 파일을 추가하고 다음 내용을 복사하여 붙여넣기 합니다.
package com.tistory.webnautes.placeexample;
import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.libraries.places.api.model.AutocompletePrediction; import com.google.android.libraries.places.api.model.Place; import com.google.android.libraries.places.api.model.PlaceLikelihood; import com.google.android.libraries.places.api.net.FetchPlaceResponse; import com.google.android.libraries.places.api.net.FindAutocompletePredictionsResponse; import com.google.android.libraries.places.api.net.FindCurrentPlaceResponse;
import android.graphics.Bitmap; import android.text.TextUtils; import android.widget.TextView;
import androidx.annotation.Nullable;
import java.util.Arrays; import java.util.List;
/** * Utility class for converting objects to viewable strings and back. */ public final class StringUtil {
private static final String FIELD_SEPARATOR = "\n\t"; private static final String RESULT_SEPARATOR = "\n---\n\t";
static void prepend(TextView textView, String prefix) { textView.setText(prefix + "\n\n" + textView.getText()); }
@Nullable static LatLngBounds convertToLatLngBounds( @Nullable String southWest, @Nullable String northEast) { LatLng soundWestLatLng = convertToLatLng(southWest); LatLng northEastLatLng = convertToLatLng(northEast); if (soundWestLatLng == null || northEast == null) { return null; }
return new LatLngBounds(soundWestLatLng, northEastLatLng); }
@Nullable static LatLng convertToLatLng(@Nullable String value) { if (TextUtils.isEmpty(value)) { return null; }
String[] split = value.split(",", -1); if (split.length != 2) { return null; }
try { return new LatLng(Double.parseDouble(split[0]), Double.parseDouble(split[1])); } catch (NullPointerException | NumberFormatException e) { return null; } }
static List<String> countriesStringToArrayList(String countriesString) { // Allow these delimiters: , ; | / \ List<String> countries = Arrays.asList(countriesString .replaceAll("\\s", "|") .split("[,;|/\\\\]",-1));
return countries; }
static String stringify(FindAutocompletePredictionsResponse response, boolean raw) { StringBuilder builder = new StringBuilder();
builder .append(response.getAutocompletePredictions().size()) .append(" Autocomplete Predictions Results:");
if (raw) { builder.append(RESULT_SEPARATOR); appendListToStringBuilder(builder, response.getAutocompletePredictions()); } else { for (AutocompletePrediction autocompletePrediction : response.getAutocompletePredictions()) { builder .append(RESULT_SEPARATOR) .append(autocompletePrediction.getFullText(/* matchStyle */ null)); } }
return builder.toString(); }
static String stringify(FetchPlaceResponse response, boolean raw) { StringBuilder builder = new StringBuilder();
builder.append("Fetch Place Result:").append(RESULT_SEPARATOR); if (raw) { builder.append(response.getPlace()); } else { builder.append(stringify(response.getPlace())); }
return builder.toString(); }
static String stringify(FindCurrentPlaceResponse response, boolean raw) { StringBuilder builder = new StringBuilder();
builder.append(response.getPlaceLikelihoods().size()).append(" Current Place Results:");
if (raw) { builder.append(RESULT_SEPARATOR); appendListToStringBuilder(builder, response.getPlaceLikelihoods()); } else { for (PlaceLikelihood placeLikelihood : response.getPlaceLikelihoods()) { builder .append(RESULT_SEPARATOR) .append("Likelihood: ") .append(placeLikelihood.getLikelihood()) .append(FIELD_SEPARATOR) .append("Place: ") .append(stringify(placeLikelihood.getPlace())); } }
return builder.toString(); }
static String stringify(Place place) { return place.getName() + " (" + place.getAddress() + ")" + " is open now? " + place.isOpen(System.currentTimeMillis()); }
static String stringify(Bitmap bitmap) { StringBuilder builder = new StringBuilder();
builder .append("Photo size (width x height)") .append(RESULT_SEPARATOR) .append(bitmap.getWidth()) .append(", ") .append(bitmap.getHeight());
return builder.toString(); }
public static String stringifyAutocompleteWidget(Place place, boolean raw) { StringBuilder builder = new StringBuilder();
builder.append("Autocomplete Widget Result:").append(RESULT_SEPARATOR);
if (raw) { builder.append(place); } else { builder.append(stringify(place)); }
return builder.toString(); }
private static <T> void appendListToStringBuilder(StringBuilder builder, List<T> items) { if (items.isEmpty()) { return; }
builder.append(items.get(0)); for (int i = 1; i < items.size(); i++) { builder.append(RESULT_SEPARATOR); builder.append(items.get(i)); } } } |
3-7. MainActivity.java를 다음 코드로 대체합니다.
package com.tistory.webnautes.placeexample;
import com.google.android.libraries.places.api.Places;
import android.content.Intent; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity; import androidx.annotation.Nullable;
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
final String apiKey = "API키";
// Setup Places Client if (!Places.isInitialized()) { Places.initialize(getApplicationContext(), apiKey); }
setLaunchActivityClickListener(R.id.current_place_button, CurrentPlaceTestActivity.class);
}
private void setLaunchActivityClickListener( int onClickResId, Class<? extends AppCompatActivity> activityClassToLaunch) { findViewById(onClickResId) .setOnClickListener( v -> { Intent intent = new Intent(MainActivity.this, activityClassToLaunch); startActivity(intent); }); } } |
3-8. CurrentPlaceTestActivity.java 파일을 추가하고 다음 내용을 복사하여 붙여넣기 합니다.
package com.tistory.webnautes.placeexample;
import com.google.android.gms.tasks.Task; import com.google.android.libraries.places.api.Places; import com.google.android.libraries.places.api.model.Place.Field; import com.google.android.libraries.places.api.model.PlaceLikelihood; import com.google.android.libraries.places.api.net.FindCurrentPlaceRequest; import com.google.android.libraries.places.api.net.FindCurrentPlaceResponse; import com.google.android.libraries.places.api.net.PlacesClient;
import android.Manifest.permission; import android.content.pm.PackageManager; import android.os.Bundle; import android.view.View; import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast;
import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat;
import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static android.Manifest.permission.ACCESS_WIFI_STATE;
public class CurrentPlaceTestActivity extends AppCompatActivity {
private PlacesClient placesClient; private TextView responseView; private FieldSelector fieldSelector;
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.current_place_test_activity);
// Retrieve a PlacesClient (previously initialized - see MainActivity) placesClient = Places.createClient(this);
// Set view objects List<Field> placeFields = FieldSelector.allExcept( Field.ADDRESS_COMPONENTS, Field.OPENING_HOURS, Field.PHONE_NUMBER, Field.UTC_OFFSET, Field.WEBSITE_URI); fieldSelector = new FieldSelector( findViewById(R.id.use_custom_fields), findViewById(R.id.custom_fields_list), placeFields, savedInstanceState); responseView = findViewById(R.id.response); setLoading(false);
// Set listeners for programmatic Find Current Place findViewById(R.id.find_current_place_button).setOnClickListener((view) -> findCurrentPlace()); }
@Override protected void onSaveInstanceState(@NonNull Bundle bundle) { super.onSaveInstanceState(bundle); fieldSelector.onSaveInstanceState(bundle); }
/** * Fetches a list of {@link PlaceLikelihood} instances that represent the Places the user is * most * likely to be at currently. */ private void findCurrentPlace() { if (ContextCompat.checkSelfPermission(this, permission.ACCESS_WIFI_STATE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { Toast.makeText( this, "Both ACCESS_WIFI_STATE & ACCESS_FINE_LOCATION permissions are required", Toast.LENGTH_SHORT) .show(); }
// Note that it is not possible to request a normal (non-dangerous) permission from // ActivityCompat.requestPermissions(), which is why the checkPermission() only checks if // ACCESS_FINE_LOCATION is granted. It is still possible to check whether a normal permission // is granted or not using ContextCompat.checkSelfPermission(). if (checkPermission(ACCESS_FINE_LOCATION)) { findCurrentPlaceWithPermissions(); } }
/** * Fetches a list of {@link PlaceLikelihood} instances that represent the Places the user is * most * likely to be at currently. */ @RequiresPermission(allOf = {ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE}) private void findCurrentPlaceWithPermissions() { setLoading(true);
FindCurrentPlaceRequest currentPlaceRequest = FindCurrentPlaceRequest.newInstance(getPlaceFields()); Task<FindCurrentPlaceResponse> currentPlaceTask = placesClient.findCurrentPlace(currentPlaceRequest);
currentPlaceTask.addOnSuccessListener( (response) -> responseView.setText(StringUtil.stringify(response, isDisplayRawResultsChecked())));
currentPlaceTask.addOnFailureListener( (exception) -> { exception.printStackTrace(); responseView.setText(exception.getMessage()); });
currentPlaceTask.addOnCompleteListener(task -> setLoading(false)); }
////////////////////////// // Helper methods below // //////////////////////////
private List<Field> getPlaceFields() { if (((CheckBox) findViewById(R.id.use_custom_fields)).isChecked()) { return fieldSelector.getSelectedFields(); } else { return fieldSelector.getAllFields(); } }
private boolean checkPermission(String permission) { boolean hasPermission = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED; if (!hasPermission) { ActivityCompat.requestPermissions(this, new String[]{permission}, 0); } return hasPermission; }
private boolean isDisplayRawResultsChecked() { return ((CheckBox) findViewById(R.id.display_raw_results)).isChecked(); }
private void setLoading(boolean loading) { findViewById(R.id.loading).setVisibility(loading ? View.VISIBLE : View.INVISIBLE); } }
|
3-9. MainActivity.java에서 다음 부분의 API키를 앞에서 생성한 API키로 대체합니다.
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
final String apiKey = "API키"; |
참고
https://developers.google.com/places/android-sdk/start
https://developers.google.com/places/android-sdk/get-api-key