GeocodingController.kt 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Marcel Hibbe
  5. * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.nextcloud.talk.controllers
  21. import android.app.SearchManager
  22. import android.content.Context
  23. import android.os.Build
  24. import android.os.Bundle
  25. import android.text.InputType
  26. import android.util.Log
  27. import android.view.Menu
  28. import android.view.MenuInflater
  29. import android.view.MenuItem
  30. import android.view.View
  31. import android.view.inputmethod.EditorInfo
  32. import android.widget.AdapterView
  33. import android.widget.Toast
  34. import androidx.appcompat.widget.SearchView
  35. import androidx.core.view.MenuItemCompat
  36. import androidx.preference.PreferenceManager
  37. import autodagger.AutoInjector
  38. import com.nextcloud.talk.R
  39. import com.nextcloud.talk.adapters.GeocodingAdapter
  40. import com.nextcloud.talk.api.NcApi
  41. import com.nextcloud.talk.application.NextcloudTalkApplication
  42. import com.nextcloud.talk.controllers.base.BaseController
  43. import com.nextcloud.talk.controllers.util.viewBinding
  44. import com.nextcloud.talk.databinding.ControllerGeocodingBinding
  45. import com.nextcloud.talk.utils.bundle.BundleKeys
  46. import fr.dudie.nominatim.client.TalkJsonNominatimClient
  47. import fr.dudie.nominatim.model.Address
  48. import kotlinx.coroutines.CoroutineScope
  49. import kotlinx.coroutines.Dispatchers.IO
  50. import kotlinx.coroutines.Dispatchers.Main
  51. import kotlinx.coroutines.launch
  52. import kotlinx.coroutines.withContext
  53. import okhttp3.OkHttpClient
  54. import org.osmdroid.config.Configuration
  55. import javax.inject.Inject
  56. @AutoInjector(NextcloudTalkApplication::class)
  57. class GeocodingController(args: Bundle) :
  58. BaseController(
  59. R.layout.controller_geocoding,
  60. args
  61. ),
  62. SearchView.OnQueryTextListener {
  63. private val binding: ControllerGeocodingBinding by viewBinding(ControllerGeocodingBinding::bind)
  64. @Inject
  65. lateinit var ncApi: NcApi
  66. @Inject
  67. lateinit var okHttpClient: OkHttpClient
  68. var roomToken: String?
  69. var nominatimClient: TalkJsonNominatimClient? = null
  70. var searchItem: MenuItem? = null
  71. var searchView: SearchView? = null
  72. var query: String? = null
  73. lateinit var adapter: GeocodingAdapter
  74. private var geocodingResults: List<Address> = ArrayList()
  75. constructor(args: Bundle, listener: LocationPickerController) : this(args) {
  76. targetController = listener
  77. }
  78. init {
  79. setHasOptionsMenu(true)
  80. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  81. Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
  82. query = args.getString(BundleKeys.KEY_GEOCODING_QUERY)
  83. roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN)
  84. }
  85. private fun initAdapter(addresses: List<Address>) {
  86. adapter = GeocodingAdapter(binding.geocodingResults.context!!, addresses)
  87. binding.geocodingResults.adapter = adapter
  88. }
  89. override fun onAttach(view: View) {
  90. super.onAttach(view)
  91. initAdapter(geocodingResults)
  92. initGeocoder()
  93. if (!query.isNullOrEmpty()) {
  94. searchLocation()
  95. } else {
  96. Log.e(TAG, "search string that was passed to GeocodingController was null or empty")
  97. }
  98. binding.geocodingResults.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
  99. val address: Address = adapter.getItem(position) as Address
  100. val listener: GeocodingResultListener? = targetController as GeocodingResultListener?
  101. listener?.receiveChosenGeocodingResult(address.latitude, address.longitude, address.displayName)
  102. router.popCurrentController()
  103. }
  104. }
  105. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  106. super.onCreateOptionsMenu(menu, inflater)
  107. inflater.inflate(R.menu.menu_geocoding, menu)
  108. searchItem = menu.findItem(R.id.geocoding_action_search)
  109. initSearchView()
  110. searchItem?.expandActionView()
  111. searchView?.setQuery(query, false)
  112. searchView?.clearFocus()
  113. }
  114. override fun onQueryTextSubmit(query: String?): Boolean {
  115. this.query = query
  116. searchLocation()
  117. searchView?.clearFocus()
  118. return true
  119. }
  120. override fun onQueryTextChange(newText: String?): Boolean {
  121. return true
  122. }
  123. private fun initSearchView() {
  124. if (activity != null) {
  125. val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
  126. if (searchItem != null) {
  127. searchView = MenuItemCompat.getActionView(searchItem) as SearchView
  128. searchView?.maxWidth = Int.MAX_VALUE
  129. searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
  130. var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
  131. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
  132. imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
  133. }
  134. searchView?.imeOptions = imeOptions
  135. searchView?.queryHint = resources!!.getString(R.string.nc_search)
  136. searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
  137. searchView?.setOnQueryTextListener(this)
  138. searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
  139. override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
  140. return true
  141. }
  142. override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
  143. router.popCurrentController()
  144. return true
  145. }
  146. })
  147. }
  148. }
  149. }
  150. private fun initGeocoder() {
  151. val baseUrl = context!!.getString(R.string.osm_geocoder_url)
  152. val email = context!!.getString(R.string.osm_geocoder_contact)
  153. nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email)
  154. }
  155. private fun searchLocation(): Boolean {
  156. CoroutineScope(IO).launch {
  157. executeGeocodingRequest()
  158. }
  159. return true
  160. }
  161. @Suppress("Detekt.TooGenericExceptionCaught")
  162. private suspend fun executeGeocodingRequest() {
  163. var results: ArrayList<Address> = ArrayList()
  164. try {
  165. results = nominatimClient!!.search(query) as ArrayList<Address>
  166. for (address in results) {
  167. Log.d(TAG, address.displayName)
  168. Log.d(TAG, address.latitude.toString())
  169. Log.d(TAG, address.longitude.toString())
  170. }
  171. } catch (e: Exception) {
  172. Log.e(TAG, "Failed to get geocoded addresses", e)
  173. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  174. }
  175. updateResultsOnMainThread(results)
  176. }
  177. private suspend fun updateResultsOnMainThread(results: ArrayList<Address>) {
  178. withContext(Main) {
  179. initAdapter(results)
  180. }
  181. }
  182. interface GeocodingResultListener {
  183. fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String)
  184. }
  185. companion object {
  186. private const val TAG = "GeocodingController"
  187. }
  188. }