LocationPickerController.kt 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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.Manifest
  22. import android.app.SearchManager
  23. import android.content.Context
  24. import android.content.pm.PackageManager
  25. import android.graphics.drawable.ColorDrawable
  26. import android.location.Location
  27. import android.location.LocationListener
  28. import android.location.LocationManager
  29. import android.os.Build
  30. import android.os.Bundle
  31. import android.text.InputType
  32. import android.util.Log
  33. import android.view.Menu
  34. import android.view.MenuInflater
  35. import android.view.MenuItem
  36. import android.view.View
  37. import android.view.inputmethod.EditorInfo
  38. import android.widget.Toast
  39. import androidx.appcompat.widget.SearchView
  40. import androidx.core.content.PermissionChecker
  41. import androidx.core.content.res.ResourcesCompat
  42. import androidx.core.view.MenuItemCompat
  43. import androidx.preference.PreferenceManager
  44. import autodagger.AutoInjector
  45. import com.bluelinelabs.conductor.RouterTransaction
  46. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  47. import com.nextcloud.talk.R
  48. import com.nextcloud.talk.api.NcApi
  49. import com.nextcloud.talk.application.NextcloudTalkApplication
  50. import com.nextcloud.talk.controllers.base.NewBaseController
  51. import com.nextcloud.talk.controllers.util.viewBinding
  52. import com.nextcloud.talk.databinding.ControllerLocationBinding
  53. import com.nextcloud.talk.models.json.generic.GenericOverall
  54. import com.nextcloud.talk.utils.ApiUtils
  55. import com.nextcloud.talk.utils.DisplayUtils
  56. import com.nextcloud.talk.utils.bundle.BundleKeys
  57. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
  58. import com.nextcloud.talk.utils.database.user.UserUtils
  59. import fr.dudie.nominatim.client.JsonNominatimClient
  60. import fr.dudie.nominatim.model.Address
  61. import io.reactivex.Observer
  62. import io.reactivex.android.schedulers.AndroidSchedulers
  63. import io.reactivex.disposables.Disposable
  64. import io.reactivex.schedulers.Schedulers
  65. import kotlinx.android.synthetic.main.controller_location.view.*
  66. import kotlinx.coroutines.CoroutineScope
  67. import kotlinx.coroutines.Dispatchers
  68. import kotlinx.coroutines.launch
  69. import kotlinx.coroutines.withContext
  70. import org.apache.http.client.HttpClient
  71. import org.apache.http.conn.ClientConnectionManager
  72. import org.apache.http.conn.scheme.Scheme
  73. import org.apache.http.conn.scheme.SchemeRegistry
  74. import org.apache.http.conn.ssl.SSLSocketFactory
  75. import org.apache.http.impl.client.DefaultHttpClient
  76. import org.apache.http.impl.conn.SingleClientConnManager
  77. import org.osmdroid.config.Configuration.getInstance
  78. import org.osmdroid.events.DelayedMapListener
  79. import org.osmdroid.events.MapListener
  80. import org.osmdroid.events.ScrollEvent
  81. import org.osmdroid.events.ZoomEvent
  82. import org.osmdroid.tileprovider.tilesource.TileSourceFactory
  83. import org.osmdroid.util.GeoPoint
  84. import org.osmdroid.views.overlay.CopyrightOverlay
  85. import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
  86. import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
  87. import javax.inject.Inject
  88. @AutoInjector(NextcloudTalkApplication::class)
  89. class LocationPickerController(args: Bundle) :
  90. NewBaseController(
  91. R.layout.controller_location,
  92. args
  93. ),
  94. SearchView.OnQueryTextListener,
  95. LocationListener,
  96. GeocodingController.GeocodingResultListener {
  97. private val binding: ControllerLocationBinding by viewBinding(ControllerLocationBinding::bind)
  98. @Inject
  99. lateinit var ncApi: NcApi
  100. @Inject
  101. lateinit var userUtils: UserUtils
  102. var nominatimClient: JsonNominatimClient? = null
  103. var roomToken: String?
  104. var myLocation: GeoPoint = GeoPoint(0.0, 0.0)
  105. private var locationManager: LocationManager? = null
  106. private lateinit var locationOverlay: MyLocationNewOverlay
  107. var moveToCurrentLocationWasClicked: Boolean = true
  108. var readyToShareLocation: Boolean = false
  109. var searchItem: MenuItem? = null
  110. var searchView: SearchView? = null
  111. var receivedChosenGeocodingResult: Boolean = false
  112. var geocodedLat: Double = 0.0
  113. var geocodedLon: Double = 0.0
  114. var geocodedName: String = ""
  115. init {
  116. setHasOptionsMenu(true)
  117. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  118. getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
  119. roomToken = args.getString(KEY_ROOM_TOKEN)
  120. }
  121. override fun onAttach(view: View) {
  122. super.onAttach(view)
  123. initMap()
  124. }
  125. @Suppress("Detekt.TooGenericExceptionCaught")
  126. override fun onDetach(view: View) {
  127. super.onDetach(view)
  128. try {
  129. locationManager!!.removeUpdates(this)
  130. } catch (e: Exception) {
  131. Log.e(TAG, "error when trying to remove updates for location Manager", e)
  132. }
  133. locationOverlay.disableMyLocation()
  134. }
  135. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  136. super.onCreateOptionsMenu(menu, inflater)
  137. inflater.inflate(R.menu.menu_locationpicker, menu)
  138. searchItem = menu.findItem(R.id.location_action_search)
  139. initSearchView()
  140. }
  141. override fun onPrepareOptionsMenu(menu: Menu) {
  142. super.onPrepareOptionsMenu(menu)
  143. actionBar?.title = context!!.getString(R.string.nc_share_location)
  144. }
  145. override val title: String
  146. get() =
  147. resources!!.getString(R.string.nc_share_location)
  148. override fun onViewBound(view: View) {
  149. setLocationDescription(false, receivedChosenGeocodingResult)
  150. binding.shareLocation.isClickable = false
  151. binding.shareLocation.setOnClickListener {
  152. if (readyToShareLocation) {
  153. shareLocation(
  154. binding.map.mapCenter?.latitude,
  155. binding.map.mapCenter?.longitude,
  156. binding.placeName.text.toString()
  157. )
  158. } else {
  159. Log.w(TAG, "readyToShareLocation was false while user tried to share location.")
  160. }
  161. }
  162. }
  163. private fun initSearchView() {
  164. if (activity != null) {
  165. val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
  166. if (searchItem != null) {
  167. searchView = MenuItemCompat.getActionView(searchItem) as SearchView
  168. searchView?.maxWidth = Int.MAX_VALUE
  169. searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
  170. var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
  171. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
  172. imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
  173. }
  174. searchView?.imeOptions = imeOptions
  175. searchView?.queryHint = resources!!.getString(R.string.nc_search)
  176. searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
  177. searchView?.setOnQueryTextListener(this)
  178. }
  179. }
  180. }
  181. override fun onQueryTextSubmit(query: String?): Boolean {
  182. if (!query.isNullOrEmpty()) {
  183. val bundle = Bundle()
  184. bundle.putString(BundleKeys.KEY_GEOCODING_QUERY, query)
  185. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
  186. router.pushController(
  187. RouterTransaction.with(GeocodingController(bundle, this))
  188. .pushChangeHandler(HorizontalChangeHandler())
  189. .popChangeHandler(HorizontalChangeHandler())
  190. )
  191. }
  192. return true
  193. }
  194. override fun onQueryTextChange(newText: String?): Boolean {
  195. return true
  196. }
  197. private fun initMap() {
  198. if (!isFineLocationPermissionGranted()) {
  199. requestFineLocationPermission()
  200. }
  201. binding.map.setTileSource(TileSourceFactory.MAPNIK)
  202. binding.map.onResume()
  203. locationManager = activity!!.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  204. try {
  205. locationManager!!.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0f, this)
  206. } catch (ex: SecurityException) {
  207. }
  208. val copyrightOverlay = CopyrightOverlay(context)
  209. binding.map.overlays?.add(copyrightOverlay)
  210. binding.map.setMultiTouchControls(true)
  211. binding.map.isTilesScaledToDpi = true
  212. locationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map)
  213. locationOverlay.enableMyLocation()
  214. locationOverlay.setPersonHotspot(20.0F,20.0F)
  215. locationOverlay.setPersonIcon(
  216. DisplayUtils.getBitmap(ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null)))
  217. binding.map.overlays?.add(locationOverlay)
  218. val mapController = binding.map.controller
  219. if (receivedChosenGeocodingResult) {
  220. mapController?.setZoom(14.0)
  221. } else {
  222. mapController?.setZoom(12.0)
  223. }
  224. val zoomToCurrentPositionOnFirstFix = !receivedChosenGeocodingResult
  225. locationOverlay.runOnFirstFix {
  226. myLocation = locationOverlay.myLocation
  227. if (zoomToCurrentPositionOnFirstFix) {
  228. activity!!.runOnUiThread {
  229. mapController?.setZoom(12.0)
  230. mapController?.setCenter(myLocation)
  231. }
  232. }
  233. }
  234. if (receivedChosenGeocodingResult && geocodedLat != 0.0 && geocodedLon != 0.0) {
  235. mapController?.setCenter(GeoPoint(geocodedLat, geocodedLon))
  236. }
  237. binding.centerMapButton.setOnClickListener {
  238. mapController?.animateTo(myLocation)
  239. moveToCurrentLocationWasClicked = true
  240. }
  241. binding.map.addMapListener(
  242. DelayedMapListener(
  243. object : MapListener {
  244. override fun onScroll(paramScrollEvent: ScrollEvent): Boolean {
  245. when {
  246. moveToCurrentLocationWasClicked -> {
  247. setLocationDescription(isGpsLocation = true, isGeocodedResult = false)
  248. moveToCurrentLocationWasClicked = false
  249. }
  250. receivedChosenGeocodingResult -> {
  251. binding.shareLocation.isClickable = true
  252. setLocationDescription(isGpsLocation = false, isGeocodedResult = true)
  253. receivedChosenGeocodingResult = false
  254. }
  255. else -> {
  256. binding.shareLocation.isClickable = true
  257. setLocationDescription(isGpsLocation = false, isGeocodedResult = false)
  258. }
  259. }
  260. readyToShareLocation = true
  261. return true
  262. }
  263. override fun onZoom(event: ZoomEvent): Boolean {
  264. return false
  265. }
  266. })
  267. )
  268. }
  269. private fun setLocationDescription(isGpsLocation: Boolean, isGeocodedResult: Boolean) {
  270. when {
  271. isGpsLocation -> {
  272. binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_current_location)
  273. binding.placeName.visibility = View.GONE
  274. binding.placeName.text = ""
  275. }
  276. isGeocodedResult -> {
  277. binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location)
  278. binding.placeName.visibility = View.VISIBLE
  279. binding.placeName.text = geocodedName
  280. }
  281. else -> {
  282. binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location)
  283. binding.placeName.visibility = View.GONE
  284. binding.placeName.text = ""
  285. }
  286. }
  287. }
  288. private fun shareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) {
  289. if (selectedLat != null || selectedLon != null) {
  290. val name = locationName
  291. if (name.isNullOrEmpty()) {
  292. initGeocoder()
  293. searchPlaceNameForCoordinates(selectedLat!!, selectedLon!!)
  294. } else {
  295. executeShareLocation(selectedLat, selectedLon, locationName)
  296. }
  297. }
  298. }
  299. private fun executeShareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) {
  300. val objectId = "geo:$selectedLat,$selectedLon"
  301. val metaData: String =
  302. "{\"type\":\"geo-location\",\"id\":\"geo:$selectedLat,$selectedLon\",\"latitude\":\"$selectedLat\"," +
  303. "\"longitude\":\"$selectedLon\",\"name\":\"$locationName\"}"
  304. ncApi.sendLocation(
  305. ApiUtils.getCredentials(userUtils.currentUser?.username, userUtils.currentUser?.token),
  306. ApiUtils.getUrlToSendLocation(userUtils.currentUser?.baseUrl, roomToken),
  307. "geo-location",
  308. objectId,
  309. metaData
  310. )
  311. .subscribeOn(Schedulers.io())
  312. .observeOn(AndroidSchedulers.mainThread())
  313. .subscribe(object : Observer<GenericOverall> {
  314. override fun onSubscribe(d: Disposable) {
  315. }
  316. override fun onNext(t: GenericOverall) {
  317. router.popCurrentController()
  318. }
  319. override fun onError(e: Throwable) {
  320. Log.e(TAG, "error when trying to share location", e)
  321. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  322. router.popCurrentController()
  323. }
  324. override fun onComplete() {
  325. }
  326. })
  327. }
  328. private fun isFineLocationPermissionGranted(): Boolean {
  329. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  330. if (PermissionChecker.checkSelfPermission(
  331. context!!,
  332. Manifest.permission.ACCESS_FINE_LOCATION
  333. ) == PermissionChecker.PERMISSION_GRANTED
  334. ) {
  335. Log.d(TAG, "Permission is granted")
  336. return true
  337. } else {
  338. Log.d(TAG, "Permission is revoked")
  339. return false
  340. }
  341. } else {
  342. Log.d(TAG, "Permission is granted")
  343. return true
  344. }
  345. }
  346. private fun requestFineLocationPermission() {
  347. requestPermissions(
  348. arrayOf(
  349. Manifest.permission.ACCESS_FINE_LOCATION
  350. ),
  351. REQUEST_PERMISSIONS_REQUEST_CODE
  352. )
  353. }
  354. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  355. if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE && grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  356. initMap()
  357. } else {
  358. Toast.makeText(context, context!!.getString(R.string.nc_location_permission_required), Toast.LENGTH_LONG)
  359. .show()
  360. }
  361. }
  362. override fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String) {
  363. receivedChosenGeocodingResult = true
  364. geocodedLat = lat
  365. geocodedLon = lon
  366. geocodedName = name
  367. }
  368. private fun initGeocoder() {
  369. val registry = SchemeRegistry()
  370. registry.register(Scheme("https", SSLSocketFactory.getSocketFactory(), 443))
  371. val connexionManager: ClientConnectionManager = SingleClientConnManager(null, registry)
  372. val httpClient: HttpClient = DefaultHttpClient(connexionManager, null)
  373. val baseUrl = context!!.getString(R.string.osm_geocoder_url)
  374. val email = context!!.getString(R.string.osm_geocoder_contact)
  375. nominatimClient = JsonNominatimClient(baseUrl, httpClient, email)
  376. }
  377. private fun searchPlaceNameForCoordinates(lat: Double, lon: Double): Boolean {
  378. CoroutineScope(Dispatchers.IO).launch {
  379. executeGeocodingRequest(lat, lon)
  380. }
  381. return true
  382. }
  383. @Suppress("Detekt.TooGenericExceptionCaught")
  384. private suspend fun executeGeocodingRequest(lat: Double, lon: Double) {
  385. var address: Address? = null
  386. try {
  387. address = nominatimClient!!.getAddress(lon, lat)
  388. } catch (e: Exception) {
  389. Log.e(TAG, "Failed to get geocoded addresses", e)
  390. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  391. }
  392. updateResultOnMainThread(lat, lon, address?.displayName)
  393. }
  394. private suspend fun updateResultOnMainThread(lat: Double, lon: Double, addressName: String?) {
  395. withContext(Dispatchers.Main) {
  396. executeShareLocation(lat, lon, addressName)
  397. }
  398. }
  399. override fun onLocationChanged(location: Location?) {
  400. myLocation = GeoPoint(location)
  401. }
  402. override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
  403. // empty
  404. }
  405. override fun onProviderEnabled(provider: String?) {
  406. // empty
  407. }
  408. override fun onProviderDisabled(provider: String?) {
  409. // empty
  410. }
  411. companion object {
  412. private const val TAG = "LocPicker"
  413. private const val REQUEST_PERMISSIONS_REQUEST_CODE = 1
  414. }
  415. }