Map, Location update and AutoComplete Places - Kotlin

Let's see how to show current location in Maps and also show the location which is chosen from AutoComplete in Maps with the updated Google play services. That is, for location listening we gonna use FusedLocationProviderClient and for autocomplete places, we gonna use, GeoDataClient with AutoCompletePrediction, i.e., no need to call places url from our end, Google Playservices will do that work for you. Okiee, Let's start now.

You can find the project in Github.

PreRequisite:

Navigate to https://developers.google.com/maps/documentation/android-api/signup and do as per the instructions to start.

Manifest:

Include your API key, which you got from previous step in AndroidManifest.xml


        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="AIzXXXXXXXXXX_XXXXXXXXXXXXXX" />

Gradle:

Include the following in app-level build 's dependencies.

    implementation 'com.google.android.gms:play-services-maps:11.8.0'
    implementation 'com.google.android.gms:play-services-places:11.8.0'
    implementation 'com.google.android.gms:play-services-location:11.8.0'

Drawables:

search.xml


<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0"
    >
    <path
        android:pathData="M15.5,14l-0.8,0l-0.3,-0.3c1,-1.1 1.6,-2.6 1.6,-4.2C16,5.9 13.1,3 9.5,3C5.9,3 3,
        5.9 3,9.5S5.9,16 9.5,16c1.6,0 3.1,-0.6 4.2,-1.6l0.3,0.3l0,0.8l5,5l1.5,-1.5L15.5,14zM9.5,14C7,14 5,
        12 5,9.5S7,5 9.5,5C12,5 14,7 14,9.5S12,14 9.5,14z"
        android:fillColor="#8e8e8e"/>
</vector>

cancel.xml


<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0"
   >
    <path
        android:pathData="M19,6.41L17.59,5,12,10.59,6.41,5,5,6.41,10.59,12,5,17.59,6.41,19,12,
        13.41,17.59,19,19,17.59,13.41,12z"
        android:fillColor="#8e8e8e"/>
</vector>

Design:

search_place.xml


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:background="@drawable/rounded_edit"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <AutoCompleteTextView
            android:id="@+id/enter_place"
            android:layout_width="match_parent"
            android:layout_height="35dp"
            android:background="@null"
            android:drawableLeft="@drawable/search"
            android:drawablePadding="5dp"
            android:hint="@string/place_autocomplete_search_hint"
            android:lines="1"
            android:paddingLeft="7dp"
            android:paddingRight="7dp" />

        <android.support.v7.widget.AppCompatImageView
            android:id="@+id/cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginRight="7dp"
            android:visibility="gone"
            android:src="@drawable/cancel" />

    </RelativeLayout>

</android.support.constraint.ConstraintLayout>

Code:

SearchPlacesActivity.kt

First, we have to initialize map. Then we have to check for Location permission to get the last accessed location, if present and updated location to set Marker at the particular co-ordinates. on Map. LocationSettingsRequest , mLocationSettingsRequest - will take care of checking whether the GPS has been enabled or not. If it's not enabled, then a dialog will be shown to enable it. LocationRequest mLocationRequest, is to create location update request with desired priority and update intervals. LocationCallback mLocationCallback, should be created to receive the location updates. Then FusedLocationProviderClient mFusedLocationClient should request location updates for the declared LocationCallback and LocationRequest. We have completed Map and location updates part.

Let's proceed with Places AutoComplete now. For that, we have to create an Adapter first, with Filtering operation (See PlacesAdapter.kt). Then we have to set that adapter for AutoCompleteTextView. While typing in that, related places will be displayed as dropdown and it can be clicked by the User. After clicked by the user, GeoDataClient, mGeoDataClient - is to get detail of that place and then to move the marker of the map to its co-ordinates.

import android.Manifest
import android.content.Context
import android.content.IntentSender
import android.content.pm.PackageManager
import android.location.Location
import android.os.Bundle
import android.os.Looper
import android.support.v4.app.ActivityCompat
import android.support.v7.app.AppCompatActivity
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.*
import com.google.android.gms.location.places.GeoDataClient
import com.google.android.gms.location.places.PlaceBufferResponse
import com.google.android.gms.location.places.Places
import com.google.android.gms.maps.*
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.maps.model.MarkerOptions
import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.gms.tasks.OnFailureListener
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.gms.tasks.Task
import kotlinx.android.synthetic.main.search_place.*


/**
 * Created by developer on 2/2/18.
 */
class SearchPlacesActivity : AppCompatActivity(), OnMapReadyCallback {


    var googleMap: GoogleMap? = null
    lateinit var placesAdapter: PlacesAdapter
    lateinit var latLng: LatLng
    lateinit var mLocationRequest: LocationRequest
    lateinit var mFusedLocationClient: FusedLocationProviderClient
    lateinit var mLocationCallback: LocationCallback
    lateinit var mGeoDataClient: GeoDataClient
    lateinit var mSettingsClient: SettingsClient
    lateinit var mLocationSettingsRequest: LocationSettingsRequest
    private val REQUEST_CHECK_SETTINGS = 0x1
    var isAutoCompleteLocation = false
    lateinit var location: Location
    val REQUEST_LOCATION = 1011
    var selectedLocation = ""
    val BOUNDS_INDIA = LatLngBounds(LatLng(23.63936, 68.14712), LatLng(28.20453, 97.34466))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.search_place)

        MapsInitializer.initialize(this)

        val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)

        mGeoDataClient = Places.getGeoDataClient(this, null);

        mLocationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult?) {
                val loc = locationResult!!.lastLocation
                if(!isAutoCompleteLocation) {
                    location = loc
                    latLng = LatLng(location.latitude, location.longitude)
                    assignToMap()
                }
            }

        }

        mLocationRequest = LocationRequest.create()
                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                .setInterval((10 * 1000).toLong())        // 10 seconds, in milliseconds
                .setFastestInterval((6 * 1000).toLong()) // 1 second, in milliseconds

        mSettingsClient = LocationServices.getSettingsClient(this)
        val builder = LocationSettingsRequest.Builder();
        builder.addLocationRequest(mLocationRequest)
        mLocationSettingsRequest = builder.build()

        placesAdapter = PlacesAdapter(this, android.R.layout.simple_list_item_1, mGeoDataClient, null, BOUNDS_INDIA)
        enter_place.setAdapter(placesAdapter)
        enter_place.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
            }

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                if (count > 0) {
                    cancel.visibility = View.VISIBLE
                } else {
                    cancel.visibility = View.GONE
                }
            }
        })
        enter_place.setOnItemClickListener({ parent, view, position, id ->
            //getLatLong(placesAdapter.getPlace(position))
            hideKeyboard()
            val item = placesAdapter.getItem(position)
            val placeId = item?.getPlaceId()
            val primaryText = item?.getPrimaryText(null)

            Log.i("Autocomplete", "Autocomplete item selected: " + primaryText)


            val placeResult = mGeoDataClient.getPlaceById(placeId)
            placeResult.addOnCompleteListener(object : OnCompleteListener<PlaceBufferResponse> {
                override fun onComplete(task: Task<PlaceBufferResponse>) {
                    val places = task.getResult();
                    val place = places.get(0)

                    isAutoCompleteLocation = true
                    latLng = place.latLng
                    assignToMap()

                    places.release()
                }

            })

            Toast.makeText(applicationContext, "Clicked: " + primaryText,
                    Toast.LENGTH_SHORT).show()
        })
        cancel.setOnClickListener {
            enter_place.setText("")
        }
    }

    private fun assignToMap() {
        googleMap?.clear()

        val options = MarkerOptions()
                .position(latLng)
                .title("My Location")
        googleMap?.apply {
            addMarker(options)
            moveCamera(CameraUpdateFactory.newLatLng(latLng))
            animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f))
        }
    }

    private fun getLastLocation() {
        try {
            mFusedLocationClient.getLastLocation()?.addOnCompleteListener { task ->
                if (task.isSuccessful && task.result != null) {
                    location = task.getResult()
                    latLng = LatLng(location.latitude, location.longitude)
                    assignToMap()

                } else {
                    Log.w("Location", "Failed to get location.")
                }
            }
        } catch (unlikely: SecurityException) {
            Log.e("Location", "Lost location permission." + unlikely)
        }

    }

    private fun initLocation() {
        try {
            mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this@SearchPlacesActivity)
            getLastLocation()
            try {

                mSettingsClient.checkLocationSettings(mLocationSettingsRequest)
                        .addOnSuccessListener(this, object : OnSuccessListener<LocationSettingsResponse> {
                            override fun onSuccess(p0: LocationSettingsResponse?) {
                                mFusedLocationClient.requestLocationUpdates(mLocationRequest,
                                        mLocationCallback, Looper.myLooper());
                            }

                        }).addOnFailureListener(this, object : OnFailureListener {
                    override fun onFailure(p0: java.lang.Exception) {
                        val statusCode = (p0 as ApiException).getStatusCode();
                        when (statusCode) {
                            LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> {
                                Log.i("Location", "Location settings are not satisfied. Attempting to upgrade " +
                                        "location settings ");
                                try {
                                    // Show the dialog by calling startResolutionForResult(), and check the
                                    // result in onActivityResult().
                                    val rae = p0 as ResolvableApiException
                                    rae.startResolutionForResult(this@SearchPlacesActivity, REQUEST_CHECK_SETTINGS);
                                } catch (sie: IntentSender.SendIntentException) {
                                    Log.i("Location", "PendingIntent unable to execute request.");
                                }
                            }

                            LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE ->
                                Toast.makeText(this@SearchPlacesActivity, "Location settings are inadequate, and cannot be \"+\n" +
                                        "                                    \"fixed here. Fix in Settings.", Toast.LENGTH_LONG).show();


                        }
                    }

                })

            } catch (unlikely: SecurityException) {
                Log.e("Location", "Lost location permission. Could not request updates. " + unlikely)
            }
        } catch (e: SecurityException) {
            e.printStackTrace()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }


    override fun onMapReady(p0: GoogleMap?) {
        Log.v("googleMap", "googleMap==" + googleMap)
        googleMap = p0
        googleMap?.setMapType(GoogleMap.MAP_TYPE_NORMAL)
        googleMap?.getUiSettings()?.apply {
            isZoomControlsEnabled = false
            isCompassEnabled = true
            isMyLocationButtonEnabled = true
        }
    }


    /* To hide Keyboard */
    fun hideKeyboard() {
        try {
            val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0)
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_LOCATION) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
                initLocation()
            } else {
                Toast.makeText(this@SearchPlacesActivity, R.string.permission_denied, Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun checkPermissions(): Boolean {
        val permissionState = ActivityCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION)
        return permissionState == PackageManager.PERMISSION_GRANTED
    }

    private fun requestPermissions() {
        ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_LOCATION)

    }

    override fun onResume() {
        super.onResume()
        if (checkPermissions()) {
            initLocation()
        } else {
            requestPermissions();
        }
    }
}

PlacesAdapter.kt


import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.google.android.gms.common.data.DataBufferUtils
import com.google.android.gms.location.places.AutocompleteFilter
import com.google.android.gms.location.places.AutocompletePrediction
import com.google.android.gms.location.places.GeoDataClient
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.tasks.RuntimeExecutionException
import com.google.android.gms.tasks.Tasks
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


/**
 * Created by Yamini on 2/2/18.
 */
class PlacesAdapter(context: Context, resourceId: Int, geoData: GeoDataClient, filter: AutocompleteFilter?, boundS_GREATER_SYDNEY: LatLngBounds) : ArrayAdapter<AutocompletePrediction>(context, resourceId), Filterable {

    var resultList: MutableList<AutocompletePrediction> = ArrayList<AutocompletePrediction>()
    private val TAG = "PlaceAutoAdapter"
    val mContext = context
    val bounds = boundS_GREATER_SYDNEY

    val geoDataClient = geoData
    val mPlaceFilter = filter
    override fun getCount(): Int {
        return resultList.size
    }

    override fun getItem(position: Int): AutocompletePrediction? {
        return resultList.get(position)
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val row = super.getView(position, convertView, parent)
        val item = getItem(position)
        val textView1 = row.findViewById<View>(android.R.id.text1) as TextView
        textView1.text = item?.getFullText(null)

        return row
    }

    override fun getFilter(): Filter {
        return object : Filter() {
            override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
                val results = Filter.FilterResults()

                // We need a separate list to store the results, since
                // this is run asynchronously.
                var filterData: ArrayList<AutocompletePrediction>? = ArrayList()

                // Skip the autocomplete query if no constraints are given.
                if (constraint != null) {
                    // Query the autocomplete API for the (constraint) search string.
                    filterData = getAutocomplete(constraint)
                }

                results.values = filterData
                if (filterData != null) {
                    results.count = filterData.size
                } else {
                    results.count = 0
                }

                return results
            }

            override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults?) {
                Log.v("results", "results==" + results);
                if (results != null && results.count > 0) {
                    // The API returned at least one result, update the data.
                    resultList = results.values as ArrayList<AutocompletePrediction>
                    Log.v("resultList", "resultList==" + resultList);
                    notifyDataSetChanged()
                } else {
                    // The API did not return any results, invalidate the data set.
                    notifyDataSetInvalidated()
                }
            }

            override fun convertResultToString(resultValue: Any): CharSequence {
                // Override this method to display a readable result in the AutocompleteTextView
                // when clicked.
                return if (resultValue is AutocompletePrediction) {
                    resultValue.getFullText(null)
                } else {
                    super.convertResultToString(resultValue)
                }
            }
        }
    }

    private fun getAutocomplete(constraint: CharSequence): ArrayList<AutocompletePrediction>? {
        Log.i(TAG, "Starting autocomplete query for:" + constraint)

        // Submit the query to the autocomplete API and retrieve a PendingResult that will
        // contain the results when the query completes.
        val results = geoDataClient.getAutocompletePredictions(constraint.toString(), bounds,
                mPlaceFilter)

        // This method should have been called off the main UI thread. Block and wait for at most
        // 60s for a result from the API.
        try {
            Tasks.await(results, 60, TimeUnit.SECONDS)
        } catch (e: ExecutionException) {
            e.printStackTrace()
        } catch (e: InterruptedException) {
            e.printStackTrace()
        } catch (e: TimeoutException) {
            e.printStackTrace()
        }

        try {
            val autocompletePredictions = results.getResult()

            Log.i(TAG, "Query completed. Received " + autocompletePredictions.getCount()
                    + " predictions.")

            // Freeze the results immutable representation that can be stored safely.
            return DataBufferUtils.freezeAndClose<AutocompletePrediction, AutocompletePrediction>(autocompletePredictions)
        } catch (e: RuntimeExecutionException) {
            // If the query did not complete successfully return null
            Toast.makeText(mContext, "Error contacting API: " + e.toString(),
                    Toast.LENGTH_SHORT).show()
            Log.e(TAG, "Error getting autocomplete prediction API call", e)
            return null
        }

    }

}

Run Application:

Now the app is ready to launch. Output will be displayed as follows:

     

Comments

  1. Can you post this Whole Project on Github that will be Much Helpfull

    ReplyDelete
    Replies
    1. Hi Ronak, You can find the whole project in https://github.com/yamzzzz/ExploreMapLocation

      Delete
  2. i am very new to android and I have been getting an error . the google map doesn't show as well as the results...can u plz help me with this?.

    ReplyDelete
    Replies
    1. nvm I was able to solve it....thank you anyway.

      Delete
  3. Hi I have this error
    com.google.android.gms.tasks.RuntimeExecutionException: com.google.android.gms.common.api.ApiException: 13: ERROR

    ReplyDelete

Post a Comment

Popular posts from this blog

SOAP Client using ksoap2 in Android - Kotlin

RecyclerView with different number of columns using SpanSizeLookup

Using Camera in Android - Kotlin

Databinding in RecyclerView - Android - Kotlin

Room with LiveData, ViewModel - Android Architecture Components - Kotlin

Stripe Integration in Android - Kotlin

Braintree Integration in Android - Kotlin

Android JetPack - Scheduling Tasks with WorkManager

Using RxJava, Retrofit in Android - Kotlin