Skip to content

Commit 9d8d968

Browse files
kikosowangela
andauthored
feat: add autocomplete address sample for Kotlin (#460)
--------- Co-authored-by: Angela Yu <[email protected]>
1 parent 551f778 commit 9d8d968

File tree

10 files changed

+657
-1
lines changed

10 files changed

+657
-1
lines changed

demo-kotlin/app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838

3939
// Places SDK for Android
4040
implementation 'com.google.android.libraries.places:places:3.0.0'
41+
implementation 'com.google.maps.android:android-maps-utils:2.4.0'
4142
}
4243
repositories {
4344
mavenCentral()

demo-kotlin/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
android:supportsRtl="true"
2929
android:theme="@style/Theme.AppCompat.Light">
3030

31+
<meta-data
32+
android:name="com.google.android.gms.version"
33+
android:value="@integer/google_play_services_version" />
34+
35+
<meta-data
36+
android:name="com.google.android.geo.API_KEY"
37+
android:value="${MAPS_API_KEY}" />
38+
3139
<activity android:name=".MainActivity"
3240
android:exported="true">
3341
<intent-filter>
@@ -38,6 +46,7 @@
3846
</activity>
3947

4048
<activity android:name=".PlaceAutocompleteActivity" />
49+
<activity android:name=".AutocompleteAddressActivity" />
4150
<activity android:name=".PlaceDetailsAndPhotosActivity" />
4251
<activity android:name=".CurrentPlaceActivity" />
4352
<activity
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.placesdemo
17+
18+
import android.Manifest.permission
19+
import android.annotation.SuppressLint
20+
import android.content.pm.PackageManager
21+
import android.content.res.Resources.NotFoundException
22+
import android.location.Location
23+
import android.os.Bundle
24+
import android.util.Log
25+
import android.view.View
26+
import android.view.ViewStub
27+
import android.widget.Button
28+
import android.widget.CheckBox
29+
import android.widget.CompoundButton
30+
import android.widget.Toast
31+
import androidx.activity.result.ActivityResult
32+
import androidx.activity.result.ActivityResultCallback
33+
import androidx.activity.result.contract.ActivityResultContracts
34+
import androidx.appcompat.app.AppCompatActivity
35+
import androidx.core.content.ContextCompat
36+
import com.example.placesdemo.databinding.AutocompleteAddressActivityBinding
37+
import com.google.android.gms.location.LocationServices
38+
import com.google.android.gms.maps.*
39+
import com.google.android.gms.maps.model.LatLng
40+
import com.google.android.gms.maps.model.MapStyleOptions
41+
import com.google.android.gms.maps.model.Marker
42+
import com.google.android.gms.maps.model.MarkerOptions
43+
import com.google.android.libraries.places.api.model.Place
44+
import com.google.android.libraries.places.api.model.TypeFilter
45+
import com.google.android.libraries.places.widget.Autocomplete
46+
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
47+
import com.google.maps.android.SphericalUtil.computeDistanceBetween
48+
import java.util.*
49+
50+
/**
51+
* Activity for using Place Autocomplete to assist filling out an address form.
52+
*/
53+
class AutocompleteAddressActivity : AppCompatActivity(R.layout.autocomplete_address_activity),
54+
OnMapReadyCallback {
55+
private lateinit var mapPanel: View
56+
57+
private var mapFragment: SupportMapFragment? = null
58+
private lateinit var coordinates: LatLng
59+
private var map: GoogleMap? = null
60+
private var marker: Marker? = null
61+
private var checkProximity = false
62+
private lateinit var binding: AutocompleteAddressActivityBinding
63+
private var deviceLocation: LatLng? = null
64+
private val acceptedProximity = 150.0
65+
private var startAutocompleteIntentListener = View.OnClickListener { view: View ->
66+
view.setOnClickListener(null)
67+
startAutocompleteIntent()
68+
}
69+
70+
// [START maps_solutions_android_autocomplete_define]
71+
private val startAutocomplete = registerForActivityResult(
72+
ActivityResultContracts.StartActivityForResult(),
73+
ActivityResultCallback { result: ActivityResult ->
74+
binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener)
75+
if (result.resultCode == RESULT_OK) {
76+
val intent = result.data
77+
if (intent != null) {
78+
val place = Autocomplete.getPlaceFromIntent(intent)
79+
80+
// Write a method to read the address components from the Place
81+
// and populate the form with the address components
82+
Log.d(TAG, "Place: " + place.addressComponents)
83+
fillInAddress(place)
84+
}
85+
} else if (result.resultCode == RESULT_CANCELED) {
86+
// The user canceled the operation.
87+
Log.i(TAG, "User canceled autocomplete")
88+
}
89+
} as ActivityResultCallback<ActivityResult>)
90+
// [END maps_solutions_android_autocomplete_define]
91+
92+
// [START maps_solutions_android_autocomplete_intent]
93+
private fun startAutocompleteIntent() {
94+
// Set the fields to specify which types of place data to
95+
// return after the user has made a selection.
96+
val fields = listOf(
97+
Place.Field.ADDRESS_COMPONENTS,
98+
Place.Field.LAT_LNG, Place.Field.VIEWPORT
99+
)
100+
101+
// Build the autocomplete intent with field, country, and type filters applied
102+
val intent = Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields)
103+
.setCountry("US")
104+
//TODO: https://developers.google.com/maps/documentation/places/android-sdk/autocomplete
105+
.setTypesFilter(listOf(TypeFilter.ADDRESS.toString().lowercase()))
106+
.build(this)
107+
startAutocomplete.launch(intent)
108+
}
109+
// [END maps_solutions_android_autocomplete_intent]
110+
111+
override fun onCreate(savedInstanceState: Bundle?) {
112+
super.onCreate(savedInstanceState)
113+
114+
binding = AutocompleteAddressActivityBinding.inflate(layoutInflater)
115+
val view = binding.root
116+
setContentView(view)
117+
118+
// Attach an Autocomplete intent to the Address 1 EditText field
119+
binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener)
120+
121+
// Update checkProximity when user checks the checkbox
122+
val checkProximityBox = findViewById<CheckBox>(R.id.checkbox_proximity)
123+
checkProximityBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
124+
// Set the boolean to match user preference for when the Submit button is clicked
125+
checkProximity = isChecked
126+
}
127+
128+
// Submit and optionally check proximity
129+
val saveButton = findViewById<Button>(R.id.autocomplete_save_button)
130+
saveButton.setOnClickListener { saveForm() }
131+
132+
// Reset the form
133+
val resetButton = findViewById<Button>(R.id.autocomplete_reset_button)
134+
resetButton.setOnClickListener { clearForm() }
135+
}
136+
137+
private fun saveForm() {
138+
Log.d(TAG, "checkProximity = $checkProximity")
139+
if (checkProximity) {
140+
checkLocationPermissions()
141+
} else {
142+
Toast.makeText(this, R.string.autocomplete_skipped_message, Toast.LENGTH_SHORT).show()
143+
}
144+
}
145+
146+
// [START maps_solutions_android_location_permissions]
147+
private fun checkLocationPermissions() {
148+
if (ContextCompat.checkSelfPermission(this, permission.ACCESS_FINE_LOCATION)
149+
== PackageManager.PERMISSION_GRANTED
150+
) {
151+
getAndCompareLocations()
152+
} else {
153+
requestPermissionLauncher.launch(
154+
permission.ACCESS_FINE_LOCATION
155+
)
156+
}
157+
}
158+
// [END maps_solutions_android_location_permissions]
159+
160+
@SuppressLint("MissingPermission")
161+
private fun getAndCompareLocations() {
162+
// TODO: Detect and handle if user has entered or modified the address manually and update
163+
// the coordinates variable to the Lat/Lng of the manually entered address. May use
164+
// Geocoding API to convert the manually entered address to a Lat/Lng.
165+
val enteredLocation = coordinates
166+
map!!.isMyLocationEnabled = true
167+
168+
// [START maps_solutions_android_location_get]
169+
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
170+
fusedLocationClient.lastLocation
171+
.addOnSuccessListener(this) { location: Location? ->
172+
// Got last known location. In some rare situations this can be null.
173+
if (location == null) {
174+
return@addOnSuccessListener
175+
}
176+
deviceLocation = LatLng(location.latitude, location.longitude)
177+
// [START_EXCLUDE]
178+
Log.d(TAG, "device location = " + deviceLocation.toString())
179+
Log.d(TAG, "entered location = $enteredLocation")
180+
181+
// [START maps_solutions_android_location_distance]
182+
// Use the computeDistanceBetween function in the Maps SDK for Android Utility Library
183+
// to use spherical geometry to compute the distance between two Lat/Lng points.
184+
val distanceInMeters: Double =
185+
computeDistanceBetween(deviceLocation, enteredLocation)
186+
if (distanceInMeters <= acceptedProximity) {
187+
Log.d(TAG, "location matched")
188+
// TODO: Display UI based on the locations matching
189+
} else {
190+
Log.d(TAG, "location not matched")
191+
// TODO: Display UI based on the locations not matching
192+
}
193+
// [END maps_solutions_android_location_distance]
194+
// [END_EXCLUDE]
195+
}
196+
}
197+
// [END maps_solutions_android_location_get]
198+
199+
private fun fillInAddress(place: Place) {
200+
val components = place.addressComponents
201+
val address1 = StringBuilder()
202+
val postcode = StringBuilder()
203+
204+
// Get each component of the address from the place details,
205+
// and then fill-in the corresponding field on the form.
206+
// Possible AddressComponent types are documented at https://goo.gle/32SJPM1
207+
if (components != null) {
208+
for (component in components.asList()) {
209+
when (component.types[0]) {
210+
"street_number" -> {
211+
address1.insert(0, component.name)
212+
}
213+
"route" -> {
214+
address1.append(" ")
215+
address1.append(component.shortName)
216+
}
217+
"postal_code" -> {
218+
postcode.insert(0, component.name)
219+
}
220+
"postal_code_suffix" -> {
221+
postcode.append("-").append(component.name)
222+
}
223+
"locality" -> binding.autocompleteCity.setText(component.name)
224+
"administrative_area_level_1" -> {
225+
binding.autocompleteState.setText(component.shortName)
226+
}
227+
"country" -> binding.autocompleteCountry.setText(component.name)
228+
}
229+
}
230+
}
231+
binding.autocompleteAddress1.setText(address1.toString())
232+
binding.autocompletePostal.setText(postcode.toString())
233+
234+
// After filling the form with address components from the Autocomplete
235+
// prediction, set cursor focus on the second address line to encourage
236+
// entry of sub-premise information such as apartment, unit, or floor number.
237+
binding.autocompleteAddress2.requestFocus()
238+
239+
// Add a map for visual confirmation of the address
240+
showMap(place)
241+
}
242+
243+
// [START maps_solutions_android_autocomplete_map_add]
244+
private fun showMap(place: Place) {
245+
coordinates = place.latLng as LatLng
246+
247+
// It isn't possible to set a fragment's id programmatically so we set a tag instead and
248+
// search for it using that.
249+
mapFragment =
250+
supportFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as SupportMapFragment?
251+
252+
// We only create a fragment if it doesn't already exist.
253+
if (mapFragment == null) {
254+
mapPanel = (findViewById<View>(R.id.stub_map) as ViewStub).inflate()
255+
val mapOptions = GoogleMapOptions()
256+
mapOptions.mapToolbarEnabled(false)
257+
258+
// To programmatically add the map, we first create a SupportMapFragment.
259+
mapFragment = SupportMapFragment.newInstance(mapOptions)
260+
261+
// Then we add it using a FragmentTransaction.
262+
supportFragmentManager
263+
.beginTransaction()
264+
.add(
265+
R.id.confirmation_map,
266+
mapFragment!!,
267+
MAP_FRAGMENT_TAG
268+
)
269+
.commit()
270+
mapFragment!!.getMapAsync(this)
271+
} else {
272+
updateMap(coordinates)
273+
}
274+
}
275+
// [END maps_solutions_android_autocomplete_map_add]
276+
277+
private fun updateMap(latLng: LatLng) {
278+
marker!!.position = latLng
279+
map!!.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f))
280+
if (mapPanel.visibility == View.GONE) {
281+
mapPanel.visibility = View.VISIBLE
282+
}
283+
}
284+
285+
// [START maps_solutions_android_autocomplete_map_ready]
286+
override fun onMapReady(googleMap: GoogleMap) {
287+
map = googleMap
288+
try {
289+
// Customise the styling of the base map using a JSON object defined
290+
// in a string resource.
291+
val success = map!!.setMapStyle(
292+
MapStyleOptions.loadRawResourceStyle(this, R.raw.style_json)
293+
)
294+
if (!success) {
295+
Log.e(TAG, "Style parsing failed.")
296+
}
297+
} catch (e: NotFoundException) {
298+
Log.e(TAG, "Can't find style. Error: ", e)
299+
}
300+
map!!.moveCamera(CameraUpdateFactory.newLatLngZoom(coordinates, 15f))
301+
marker = map!!.addMarker(MarkerOptions().position(coordinates))
302+
}
303+
// [END maps_solutions_android_autocomplete_map_ready]
304+
305+
private fun clearForm() {
306+
binding.autocompleteAddress1.setText("")
307+
binding.autocompleteAddress2.text.clear()
308+
binding.autocompleteCity.text.clear()
309+
binding.autocompleteState.text.clear()
310+
binding.autocompletePostal.text.clear()
311+
binding.autocompleteCountry.text.clear()
312+
mapPanel.visibility = View.GONE
313+
binding.autocompleteAddress1.requestFocus()
314+
}
315+
316+
// [START maps_solutions_android_permission_request]
317+
// Register the permissions callback, which handles the user's response to the
318+
// system permissions dialog. Save the return value, an instance of
319+
// ActivityResultLauncher, as an instance variable.
320+
private val requestPermissionLauncher = registerForActivityResult(
321+
ActivityResultContracts.RequestPermission()
322+
) { isGranted: Boolean ->
323+
if (isGranted) {
324+
// Since ACCESS_FINE_LOCATION is the only permission in this sample,
325+
// run the location comparison task once permission is granted.
326+
// Otherwise, check which permission is granted.
327+
getAndCompareLocations()
328+
} else {
329+
// Fallback behavior if user denies permission
330+
Log.d(TAG, "User denied permission")
331+
}
332+
}
333+
// [END maps_solutions_android_permission_request]
334+
335+
companion object {
336+
private val TAG = AutocompleteAddressActivity::class.java.simpleName
337+
private const val MAP_FRAGMENT_TAG = "MAP"
338+
}
339+
}

demo-kotlin/app/src/main/java/com/example/placesdemo/MainActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018 Google LLC
2+
* Copyright 2022 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@ class MainActivity : AppCompatActivity() {
4242
}
4343

4444
setLaunchActivityClickListener(R.id.programmatic_autocomplete_button, ProgrammaticAutocompleteToolbarActivity::class.java)
45+
setLaunchActivityClickListener(R.id.autocomplete_address_button, AutocompleteAddressActivity::class.java)
4546
setLaunchActivityClickListener(R.id.autocomplete_button, PlaceAutocompleteActivity::class.java)
4647
setLaunchActivityClickListener(R.id.place_and_photo_button, PlaceDetailsAndPhotosActivity::class.java)
4748
setLaunchActivityClickListener(R.id.current_place_button, CurrentPlaceActivity::class.java)

0 commit comments

Comments
 (0)