mukeshsolanki/Google-Places-AutoComplete-EditText

View on GitHub
library/src/main/java/in/madapps/placesautocomplete/PlaceApi.kt

Summary

Maintainability
A
1 hr
Test Coverage
package `in`.madapps.placesautocomplete

import `in`.madapps.placesautocomplete.exception.InitializationException
import `in`.madapps.placesautocomplete.listener.OnPlacesDetailsListener
import `in`.madapps.placesautocomplete.model.Address
import `in`.madapps.placesautocomplete.model.Place
import `in`.madapps.placesautocomplete.model.PlaceDetails
import android.content.Context
import android.text.TextUtils
import android.util.Log
import androidx.annotation.Nullable
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.URL
import java.net.URLEncoder

/**
 * Created by mukeshsolanki on 28/02/19.
 */
class PlaceAPI private constructor(
  var apiKey: String?, var sessionToken: String?, var appContext: Context
) {
  /**
   * Used to get details for the places api to be showed in the auto complete list
   */
  internal fun autocomplete(input: String): ArrayList<Place>? {
    checkInitialization()
    val resultList: ArrayList<Place>? = null
    var conn: HttpURLConnection? = null
    val jsonResults = StringBuilder()
    try {
      val sb = buildApiUrl(PLACES_API_BASE + TYPE_AUTOCOMPLETE + OUT_JSON)
      sb.append("&input=" + URLEncoder.encode(input, "utf8"))
      val url = URL(sb.toString())
      conn = url.openConnection() as HttpURLConnection
      val inputStreamReader = InputStreamReader(conn.inputStream)
      constructData(inputStreamReader, jsonResults)
    } catch (e: Exception) {
      when (e) {
        is MalformedURLException -> logError(e, R.string.error_processing_places_api)
        is IOException -> logError(e, R.string.error_connecting_to_places_api)
      }
      return resultList
    } finally {
      conn?.disconnect()
    }
    return parseAutoCompleteData(jsonResults)
  }

  /**
   * Fetches the details of the place
   */
  @Nullable
  fun fetchPlaceDetails(placeId: String, listener: OnPlacesDetailsListener) {
    checkInitialization()
    Thread(Runnable {
      var conn: HttpURLConnection? = null
      val jsonResults = StringBuilder()
      try {
        val sb = buildApiUrl(PLACES_API_BASE + TYPE_DETAIL + OUT_JSON)
        sb.append("$PARAM_PLACE_ID$placeId")
        conn = URL(sb.toString()).openConnection() as? HttpURLConnection
        val inputStreamReader = InputStreamReader(conn?.inputStream)
        constructData(inputStreamReader, jsonResults)
        parseDetailsData(jsonResults, listener)
      } catch (e: Exception) {
        when (e) {
          is JSONException -> parseDetailsError(jsonResults, listener, e)
          is MalformedURLException -> showDetailsError(
            R.string.error_processing_places_api, listener, e
          )
          is IOException -> showDetailsError(R.string.error_connecting_to_places_api, listener, e)
        }
      } finally {
        conn?.disconnect()
      }
    }).start()
  }

  private fun checkInitialization() {
    if (TextUtils.isEmpty(apiKey)) {
      throw InitializationException(appContext.getString(R.string.error_lib_not_initialized))
    }
  }

  private fun buildApiUrl(apiUrl: String): StringBuilder {
    val sb = StringBuilder(apiUrl)
    sb.append("?key=$apiKey")
    if (!TextUtils.isEmpty(sessionToken)) {
      sb.append("&sessiontoken=$sessionToken")
    }
    return sb
  }

  private fun logError(e: Exception, resource: Int) {
    Log.e(TAG, appContext.getString(resource), e)
  }

  private fun parseAutoCompleteData(jsonResults: StringBuilder): ArrayList<Place>? {
    var resultList: ArrayList<Place>? = ArrayList()
    try {
      val jsonObj = JSONObject(jsonResults.toString())
      val predsJsonArray = jsonObj.getJSONArray("predictions")
      resultList = ArrayList(predsJsonArray.length())
      for (i in 0 until predsJsonArray.length()) {
        resultList.add(
          Place(
            predsJsonArray.getJSONObject(i).getString("place_id"),
            predsJsonArray.getJSONObject(i).getString("description")
          )
        )
      }
      return resultList
    } catch (e: JSONException) {
      val errorJson = JSONObject(jsonResults.toString())
      when {
        errorJson.has(ERROR_MESSAGE) -> Log.e(TAG, errorJson.getString(ERROR_MESSAGE))
        else -> Log.e(TAG, appContext.getString(R.string.error_cannot_process_json_results), e)
      }
      return resultList
    }
  }

  private fun constructData(inputStreamReader: InputStreamReader, jsonResults: StringBuilder) {
    var read: Int
    val buff = CharArray(1024)
    loop@ do {
      read = inputStreamReader.read(buff)
      when {
        read != -1 -> jsonResults.append(buff, 0, read)
        else -> break@loop
      }
    } while (true)
  }

  private fun showDetailsError(resource: Int, listener: OnPlacesDetailsListener, e: Exception) {
    logError(e, resource)
    appContext.getString(resource).let { listener.onError(it) }
  }

  private fun parseDetailsError(
    jsonResults: StringBuilder, listener: OnPlacesDetailsListener, e: Exception
  ) {
    val errorJson = JSONObject(jsonResults.toString())
    if (errorJson.has(ERROR_MESSAGE)) {
      Log.e(TAG, errorJson.getString(ERROR_MESSAGE), e)
      listener.onError(errorJson.getString(ERROR_MESSAGE))
    } else {
      Log.e(TAG, appContext.getString(R.string.error_cannot_process_json_results), e)
      appContext.getString(R.string.error_cannot_process_json_results).let { listener.onError(it) }
    }
  }

  private fun parseDetailsData(jsonResults: StringBuilder, listener: OnPlacesDetailsListener) {
    val jsonObj = JSONObject(jsonResults.toString())
extendlog(jsonObj.toString())
    val resultJsonObject = jsonObj.getJSONObject(RESULT)
    val addressArray = resultJsonObject.getJSONArray(ADDRESS_COMPONENTS)
    val geometry = resultJsonObject.getJSONObject(GEOMETRY)
    val location = geometry.getJSONObject(LOCATION)
    val lat = location.getDouble(LAT)
    val lng = location.getDouble(LNG)
    val placeId = resultJsonObject.getString(PLACE_ID)
    val url = resultJsonObject.getString(URL)
    val utcOffset = resultJsonObject.getInt(UTC_OFFSET)
    val vicinity = resultJsonObject.getString(VICINITY)
    var compoundPlusCode =""
    var globalPlusCode =""
    if(resultJsonObject.has(PLUS_CODE)){
      val plusCode = resultJsonObject.getJSONObject(PLUS_CODE)
      compoundPlusCode = plusCode.getString(COMPOUND_CODE)
      globalPlusCode = plusCode.getString(GLOBAL_CODE)
    }
    val address = ArrayList<Address>()
    getAddress(addressArray, address)
    listener.onPlaceDetailsFetched(
      PlaceDetails(resultJsonObject.getString(NAME), address, lat,
        lng, placeId, url, utcOffset, vicinity, compoundPlusCode, globalPlusCode
      )
    )
  }

  private fun extendlog(veryLongString: String){
    val maxLogSize = 1000
    for (i in 0..veryLongString.length / maxLogSize) {
      val start = i * maxLogSize
      var end = (i + 1) * maxLogSize
      end = if (end > veryLongString.length) veryLongString.length else end
      Log.v("Json=>", veryLongString.substring(start, end))
    }
  }
  private fun getAddress(addressArray: JSONArray, address: ArrayList<Address>) {
    (0 until addressArray.length()).forEach { i ->
      val addressObject = addressArray.getJSONObject(i)
      val addressTypeArray = addressObject.getJSONArray(TYPES)
      val addressType = ArrayList<String>()
      parseAddressType(addressTypeArray, addressType, address, addressObject)
    }
  }

  private fun parseAddressType(
    addressTypeArray: JSONArray, addressType: ArrayList<String>,
    address: ArrayList<Address>, addressObject: JSONObject
  ) {
    (0 until addressTypeArray.length()).forEach { j ->
      addressType.add(
        addressTypeArray.getString(j)
      )
    }
    address.add(
      Address(
        addressObject.getString(LONG_NAME),
        addressObject.getString(SHORT_NAME),
        addressType
      )
    )
  }

  companion object {
    private val TAG = PlaceAPI::class.java.simpleName
    private const val PLACES_API_BASE = "https://maps.googleapis.com/maps/api/place"
    private const val TYPE_AUTOCOMPLETE = "/autocomplete"
    private const val TYPE_DETAIL = "/details"
    private const val PARAM_PLACE_ID = "&placeid="
    private const val OUT_JSON = "/json"
    private const val LONG_NAME = "long_name"
    private const val SHORT_NAME = "short_name"
    private const val PLACE_ID = "place_id"
    private const val URL = "url"
    private const val UTC_OFFSET = "utc_offset"
    private const val VICINITY = "vicinity"
    private const val PLUS_CODE = "plus_code"
    private const val COMPOUND_CODE = "compound_code"
    private const val GLOBAL_CODE = "global_code"
    private const val NAME = "name"
    private const val TYPES = "types"
    private const val ADDRESS_COMPONENTS = "address_components"
    private const val GEOMETRY = "geometry"
    private const val LOCATION = "location"
    private const val LAT = "lat"
    private const val LNG = "lng"
    private const val RESULT = "result"
    private const val ERROR_MESSAGE = "error_message"
  }

  /**
   * The data class used as builder to allow the user to use different configs of the places API
   */
  data class Builder(
    private var apiKey: String? = null,
    private var sessionToken: String? = null
  ) {
    /**
     * Sets the api key for the PlaceAPI
     */
    fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }

    /**
     * Sets a unique session token for billing in the PlaceAPI
     */
    fun sessionToken(sessionToken: String) = apply { this.sessionToken = sessionToken }

    /**
     * Builds and creates an object of the PlaceAPI
     */
    fun build(context: Context) = PlaceAPI(apiKey, sessionToken, context)
  }
}