Skip to content

Commit

Permalink
feat: add autocomplete address sample for Kotlin (#460)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Angela Yu <[email protected]>
  • Loading branch information
kikoso and wangela authored Mar 25, 2023
1 parent 551f778 commit 9d8d968
Show file tree
Hide file tree
Showing 10 changed files with 657 additions and 1 deletion.
1 change: 1 addition & 0 deletions demo-kotlin/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {

// Places SDK for Android
implementation 'com.google.android.libraries.places:places:3.0.0'
implementation 'com.google.maps.android:android-maps-utils:2.4.0'
}
repositories {
mavenCentral()
Expand Down
9 changes: 9 additions & 0 deletions demo-kotlin/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light">

<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />

<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />

<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
Expand All @@ -38,6 +46,7 @@
</activity>

<activity android:name=".PlaceAutocompleteActivity" />
<activity android:name=".AutocompleteAddressActivity" />
<activity android:name=".PlaceDetailsAndPhotosActivity" />
<activity android:name=".CurrentPlaceActivity" />
<activity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.placesdemo

import android.Manifest.permission
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.res.Resources.NotFoundException
import android.location.Location
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewStub
import android.widget.Button
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.placesdemo.databinding.AutocompleteAddressActivityBinding
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.*
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MapStyleOptions
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.api.model.TypeFilter
import com.google.android.libraries.places.widget.Autocomplete
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
import com.google.maps.android.SphericalUtil.computeDistanceBetween
import java.util.*

/**
* Activity for using Place Autocomplete to assist filling out an address form.
*/
class AutocompleteAddressActivity : AppCompatActivity(R.layout.autocomplete_address_activity),
OnMapReadyCallback {
private lateinit var mapPanel: View

private var mapFragment: SupportMapFragment? = null
private lateinit var coordinates: LatLng
private var map: GoogleMap? = null
private var marker: Marker? = null
private var checkProximity = false
private lateinit var binding: AutocompleteAddressActivityBinding
private var deviceLocation: LatLng? = null
private val acceptedProximity = 150.0
private var startAutocompleteIntentListener = View.OnClickListener { view: View ->
view.setOnClickListener(null)
startAutocompleteIntent()
}

// [START maps_solutions_android_autocomplete_define]
private val startAutocomplete = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
ActivityResultCallback { result: ActivityResult ->
binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener)
if (result.resultCode == RESULT_OK) {
val intent = result.data
if (intent != null) {
val place = Autocomplete.getPlaceFromIntent(intent)

// Write a method to read the address components from the Place
// and populate the form with the address components
Log.d(TAG, "Place: " + place.addressComponents)
fillInAddress(place)
}
} else if (result.resultCode == RESULT_CANCELED) {
// The user canceled the operation.
Log.i(TAG, "User canceled autocomplete")
}
} as ActivityResultCallback<ActivityResult>)
// [END maps_solutions_android_autocomplete_define]

// [START maps_solutions_android_autocomplete_intent]
private fun startAutocompleteIntent() {
// Set the fields to specify which types of place data to
// return after the user has made a selection.
val fields = listOf(
Place.Field.ADDRESS_COMPONENTS,
Place.Field.LAT_LNG, Place.Field.VIEWPORT
)

// Build the autocomplete intent with field, country, and type filters applied
val intent = Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields)
.setCountry("US")
//TODO: https://developers.google.com/maps/documentation/places/android-sdk/autocomplete
.setTypesFilter(listOf(TypeFilter.ADDRESS.toString().lowercase()))
.build(this)
startAutocomplete.launch(intent)
}
// [END maps_solutions_android_autocomplete_intent]

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = AutocompleteAddressActivityBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)

// Attach an Autocomplete intent to the Address 1 EditText field
binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener)

// Update checkProximity when user checks the checkbox
val checkProximityBox = findViewById<CheckBox>(R.id.checkbox_proximity)
checkProximityBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
// Set the boolean to match user preference for when the Submit button is clicked
checkProximity = isChecked
}

// Submit and optionally check proximity
val saveButton = findViewById<Button>(R.id.autocomplete_save_button)
saveButton.setOnClickListener { saveForm() }

// Reset the form
val resetButton = findViewById<Button>(R.id.autocomplete_reset_button)
resetButton.setOnClickListener { clearForm() }
}

private fun saveForm() {
Log.d(TAG, "checkProximity = $checkProximity")
if (checkProximity) {
checkLocationPermissions()
} else {
Toast.makeText(this, R.string.autocomplete_skipped_message, Toast.LENGTH_SHORT).show()
}
}

// [START maps_solutions_android_location_permissions]
private fun checkLocationPermissions() {
if (ContextCompat.checkSelfPermission(this, permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
) {
getAndCompareLocations()
} else {
requestPermissionLauncher.launch(
permission.ACCESS_FINE_LOCATION
)
}
}
// [END maps_solutions_android_location_permissions]

@SuppressLint("MissingPermission")
private fun getAndCompareLocations() {
// TODO: Detect and handle if user has entered or modified the address manually and update
// the coordinates variable to the Lat/Lng of the manually entered address. May use
// Geocoding API to convert the manually entered address to a Lat/Lng.
val enteredLocation = coordinates
map!!.isMyLocationEnabled = true

// [START maps_solutions_android_location_get]
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation
.addOnSuccessListener(this) { location: Location? ->
// Got last known location. In some rare situations this can be null.
if (location == null) {
return@addOnSuccessListener
}
deviceLocation = LatLng(location.latitude, location.longitude)
// [START_EXCLUDE]
Log.d(TAG, "device location = " + deviceLocation.toString())
Log.d(TAG, "entered location = $enteredLocation")

// [START maps_solutions_android_location_distance]
// Use the computeDistanceBetween function in the Maps SDK for Android Utility Library
// to use spherical geometry to compute the distance between two Lat/Lng points.
val distanceInMeters: Double =
computeDistanceBetween(deviceLocation, enteredLocation)
if (distanceInMeters <= acceptedProximity) {
Log.d(TAG, "location matched")
// TODO: Display UI based on the locations matching
} else {
Log.d(TAG, "location not matched")
// TODO: Display UI based on the locations not matching
}
// [END maps_solutions_android_location_distance]
// [END_EXCLUDE]
}
}
// [END maps_solutions_android_location_get]

private fun fillInAddress(place: Place) {
val components = place.addressComponents
val address1 = StringBuilder()
val postcode = StringBuilder()

// Get each component of the address from the place details,
// and then fill-in the corresponding field on the form.
// Possible AddressComponent types are documented at https://goo.gle/32SJPM1
if (components != null) {
for (component in components.asList()) {
when (component.types[0]) {
"street_number" -> {
address1.insert(0, component.name)
}
"route" -> {
address1.append(" ")
address1.append(component.shortName)
}
"postal_code" -> {
postcode.insert(0, component.name)
}
"postal_code_suffix" -> {
postcode.append("-").append(component.name)
}
"locality" -> binding.autocompleteCity.setText(component.name)
"administrative_area_level_1" -> {
binding.autocompleteState.setText(component.shortName)
}
"country" -> binding.autocompleteCountry.setText(component.name)
}
}
}
binding.autocompleteAddress1.setText(address1.toString())
binding.autocompletePostal.setText(postcode.toString())

// After filling the form with address components from the Autocomplete
// prediction, set cursor focus on the second address line to encourage
// entry of sub-premise information such as apartment, unit, or floor number.
binding.autocompleteAddress2.requestFocus()

// Add a map for visual confirmation of the address
showMap(place)
}

// [START maps_solutions_android_autocomplete_map_add]
private fun showMap(place: Place) {
coordinates = place.latLng as LatLng

// It isn't possible to set a fragment's id programmatically so we set a tag instead and
// search for it using that.
mapFragment =
supportFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as SupportMapFragment?

// We only create a fragment if it doesn't already exist.
if (mapFragment == null) {
mapPanel = (findViewById<View>(R.id.stub_map) as ViewStub).inflate()
val mapOptions = GoogleMapOptions()
mapOptions.mapToolbarEnabled(false)

// To programmatically add the map, we first create a SupportMapFragment.
mapFragment = SupportMapFragment.newInstance(mapOptions)

// Then we add it using a FragmentTransaction.
supportFragmentManager
.beginTransaction()
.add(
R.id.confirmation_map,
mapFragment!!,
MAP_FRAGMENT_TAG
)
.commit()
mapFragment!!.getMapAsync(this)
} else {
updateMap(coordinates)
}
}
// [END maps_solutions_android_autocomplete_map_add]

private fun updateMap(latLng: LatLng) {
marker!!.position = latLng
map!!.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f))
if (mapPanel.visibility == View.GONE) {
mapPanel.visibility = View.VISIBLE
}
}

// [START maps_solutions_android_autocomplete_map_ready]
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
try {
// Customise the styling of the base map using a JSON object defined
// in a string resource.
val success = map!!.setMapStyle(
MapStyleOptions.loadRawResourceStyle(this, R.raw.style_json)
)
if (!success) {
Log.e(TAG, "Style parsing failed.")
}
} catch (e: NotFoundException) {
Log.e(TAG, "Can't find style. Error: ", e)
}
map!!.moveCamera(CameraUpdateFactory.newLatLngZoom(coordinates, 15f))
marker = map!!.addMarker(MarkerOptions().position(coordinates))
}
// [END maps_solutions_android_autocomplete_map_ready]

private fun clearForm() {
binding.autocompleteAddress1.setText("")
binding.autocompleteAddress2.text.clear()
binding.autocompleteCity.text.clear()
binding.autocompleteState.text.clear()
binding.autocompletePostal.text.clear()
binding.autocompleteCountry.text.clear()
mapPanel.visibility = View.GONE
binding.autocompleteAddress1.requestFocus()
}

// [START maps_solutions_android_permission_request]
// Register the permissions callback, which handles the user's response to the
// system permissions dialog. Save the return value, an instance of
// ActivityResultLauncher, as an instance variable.
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Since ACCESS_FINE_LOCATION is the only permission in this sample,
// run the location comparison task once permission is granted.
// Otherwise, check which permission is granted.
getAndCompareLocations()
} else {
// Fallback behavior if user denies permission
Log.d(TAG, "User denied permission")
}
}
// [END maps_solutions_android_permission_request]

companion object {
private val TAG = AutocompleteAddressActivity::class.java.simpleName
private const val MAP_FRAGMENT_TAG = "MAP"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018 Google LLC
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -42,6 +42,7 @@ class MainActivity : AppCompatActivity() {
}

setLaunchActivityClickListener(R.id.programmatic_autocomplete_button, ProgrammaticAutocompleteToolbarActivity::class.java)
setLaunchActivityClickListener(R.id.autocomplete_address_button, AutocompleteAddressActivity::class.java)
setLaunchActivityClickListener(R.id.autocomplete_button, PlaceAutocompleteActivity::class.java)
setLaunchActivityClickListener(R.id.place_and_photo_button, PlaceDetailsAndPhotosActivity::class.java)
setLaunchActivityClickListener(R.id.current_place_button, CurrentPlaceActivity::class.java)
Expand Down
Loading

0 comments on commit 9d8d968

Please sign in to comment.