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
+ }
0 commit comments