add location search with nominatim

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2021-06-01 14:29:35 +02:00 committed by Andy Scherzinger
parent d5face5941
commit 2f0105abd3
No known key found for this signature in database
GPG key ID: 6CADC7E3523C308B
11 changed files with 407 additions and 16 deletions

View file

@ -114,6 +114,7 @@ android {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/rxjava.properties'
}
@ -267,7 +268,10 @@ dependencies {
implementation 'com.github.Kennyc1012:BottomSheet:2.4.1'
implementation 'com.github.nextcloud:PopupBubble:master-SNAPSHOT'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'eu.medsea.mimeutil:mime-util:2.1.3'
implementation('eu.medsea.mimeutil:mime-util:2.1.3', {
exclude group: 'org.slf4j'
})
implementation "com.afollestad.material-dialogs:core:${materialDialogsVersion}"
implementation "com.afollestad.material-dialogs:datetime:${materialDialogsVersion}"
@ -287,6 +291,7 @@ dependencies {
implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0'
implementation 'org.osmdroid:osmdroid-android:6.1.10'
implementation 'fr.dudie:nominatim-api:3.4'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:3.11.0'

View file

@ -0,0 +1,40 @@
package com.nextcloud.talk.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import com.nextcloud.talk.R
import fr.dudie.nominatim.model.Address
class GeocodingAdapter(context: Context, val dataSource: List<Address>) : BaseAdapter() {
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
override fun getCount(): Int {
return dataSource.size
}
override fun getItem(position: Int): Any {
return dataSource[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val rowView = inflater.inflate(R.layout.geocoding_item, parent, false)
val nameView = rowView.findViewById(R.id.name) as TextView
val address = getItem(position) as Address
nameView.text = address.displayName
return rowView
}
}

View file

@ -824,7 +824,7 @@ class ChatController(args: Bundle) :
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN,roomToken)
router.pushController(
RouterTransaction.with(LocationController(bundle))
RouterTransaction.with(LocationPickerController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)

View file

@ -0,0 +1,199 @@
package com.nextcloud.talk.controllers
import android.app.SearchManager
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.ListView
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat
import androidx.preference.PreferenceManager
import autodagger.AutoInjector
import butterknife.BindView
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.GeocodingAdapter
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.AppPreferences
import fr.dudie.nominatim.client.JsonNominatimClient
import fr.dudie.nominatim.model.Address
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.apache.http.client.HttpClient
import org.apache.http.conn.ClientConnectionManager
import org.apache.http.conn.scheme.Scheme
import org.apache.http.conn.scheme.SchemeRegistry
import org.apache.http.conn.ssl.SSLSocketFactory
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.impl.conn.SingleClientConnManager
import org.osmdroid.config.Configuration
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class GeocodingController(args: Bundle) : BaseController(args), SearchView.OnQueryTextListener {
@Inject
@JvmField
var context: Context? = null
@Inject
@JvmField
var appPreferences: AppPreferences? = null
@BindView(R.id.geocoding_results)
@JvmField
var geocodingResultListView: ListView? = null
var nominatimClient: JsonNominatimClient? = null
var searchItem: MenuItem? = null
var searchView: SearchView? = null
var query: String? = null
lateinit var adapter: GeocodingAdapter
private var geocodingResults: List<Address> = ArrayList()
init {
setHasOptionsMenu(true)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
query = args.getString(BundleKeys.KEY_GEOCODING_QUERY)
initAdapter(geocodingResults)
}
private fun initAdapter(addresses : List<Address>) {
adapter = GeocodingAdapter(context!!, addresses)
geocodingResultListView?.adapter = adapter
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.controller_geocoding, container, false)
}
override fun onAttach(view: View) {
super.onAttach(view)
initGeocoder()
if (!query.isNullOrEmpty()) {
searchLocation()
} else {
Log.e(TAG, "search string that was passed to GeocodingController was null or empty")
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_geocoding, menu)
searchItem = menu.findItem(R.id.geocoding_action_search)
initSearchView()
searchItem?.expandActionView()
searchView?.setQuery(query, false)
searchView?.clearFocus()
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
hideSearchBar()
actionBar.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent)))
actionBar.title = "Share location"
}
override fun onQueryTextSubmit(query: String?): Boolean {
this.query = query
searchLocation()
searchView?.clearFocus()
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
private fun initSearchView() {
if (activity != null) {
val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
searchView?.setMaxWidth(Int.MAX_VALUE)
searchView?.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER)
var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
}
searchView?.setImeOptions(imeOptions)
searchView?.setQueryHint(resources!!.getString(R.string.nc_search))
searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
searchView?.setOnQueryTextListener(this)
searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
router.popCurrentController()
return true
}
})
}
}
}
private fun initGeocoder() {
val registry = SchemeRegistry()
registry.register(Scheme("https", SSLSocketFactory.getSocketFactory(), 443))
val connexionManager: ClientConnectionManager = SingleClientConnManager(null, registry)
val httpClient: HttpClient = DefaultHttpClient(connexionManager, null)
val baseUrl = "https://nominatim.openstreetmap.org/"
val email = "android@nextcloud.com"
nominatimClient = JsonNominatimClient(baseUrl, httpClient, email)
}
private fun searchLocation(): Boolean {
CoroutineScope(IO).launch {
executeGeocodingRequest()
}
return true
}
private suspend fun executeGeocodingRequest() {
var results: ArrayList<Address> = ArrayList()
try {
results = nominatimClient!!.search(query) as ArrayList<Address>
for (address in results) {
Log.d(TAG, address.displayName)
Log.d(TAG, address.latitude.toString())
Log.d(TAG, address.longitude.toString())
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get geocoded addresses", e)
}
updateResultsOnMainThread(results)
}
private suspend fun updateResultsOnMainThread(results: ArrayList<Address>) {
withContext(Main) {
initAdapter(results)
}
}
companion object {
private val TAG = "GeocodingController"
}
}

View file

@ -1,31 +1,40 @@
package com.nextcloud.talk.controllers
import android.Manifest
import android.app.SearchManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.cardview.widget.CardView
import androidx.core.content.PermissionChecker
import androidx.core.view.MenuItemCompat
import androidx.preference.PreferenceManager
import autodagger.AutoInjector
import butterknife.BindView
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.database.user.UserUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
@ -47,7 +56,7 @@ import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class LocationController(args: Bundle) : BaseController(args) {
class LocationPickerController(args: Bundle) : BaseController(args), SearchView.OnQueryTextListener {
@Inject
lateinit var ncApi: NcApi
@ -87,6 +96,8 @@ class LocationController(args: Bundle) : BaseController(args) {
var moveToCurrentLocationWasClicked: Boolean = true
var readyToShareLocation: Boolean = false
var searchItem: MenuItem? = null
var searchView: SearchView? = null
init {
setHasOptionsMenu(true)
@ -107,31 +118,24 @@ class LocationController(args: Bundle) : BaseController(args) {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_conversation_plus_filter, menu)
// searchItem = menu.findItem(R.id.action_search)
// initSearchView()
inflater.inflate(R.menu.menu_locationpicker, menu)
searchItem = menu.findItem(R.id.location_action_search)
initSearchView()
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
Log.d(TAG, "onPrepareOptionsMenu")
hideSearchBar()
actionBar.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent)));
actionBar.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent)))
actionBar.title = "Share location"
// searchView = MenuItemCompat.getActionView(searchItem) as SearchView
// showShareToScreen = !shareToScreenWasShown && hasActivityActionSendIntent()
// if (showShareToScreen) {
// hideSearchBar()
// actionBar.setTitle(R.string.send_to_three_dots)
// }
}
override fun onViewBound(view: View) {
setCurrentLocationDescription()
shareLocation?.isClickable = false
shareLocation?.setOnClickListener {
if(readyToShareLocation){
if (readyToShareLocation) {
shareLocation()
} else {
Log.d(TAG, "readyToShareLocation was false while user tried to share location.")
@ -139,6 +143,44 @@ class LocationController(args: Bundle) : BaseController(args) {
}
}
private fun initSearchView() {
if (activity != null) {
val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
searchView?.setMaxWidth(Int.MAX_VALUE)
searchView?.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER)
var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
}
searchView?.setImeOptions(imeOptions)
searchView?.setQueryHint(resources!!.getString(R.string.nc_search))
if (searchManager != null) {
searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
}
searchView?.setOnQueryTextListener(this)
}
}
}
override fun onQueryTextSubmit(query: String?): Boolean {
if (!query.isNullOrEmpty()) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_GEOCODING_QUERY, query)
router.pushController(
RouterTransaction.with(GeocodingController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
private fun initMap() {
if (!isFineLocationPermissionGranted()) {
requestFineLocationPermission()
@ -279,7 +321,7 @@ class LocationController(args: Bundle) : BaseController(args) {
}
companion object {
private val TAG = "LocationController"
private val TAG = "LocationPickerController"
private val REQUEST_PERMISSIONS_REQUEST_CODE = 1;
}
}

View file

@ -66,4 +66,5 @@ object BundleKeys {
val KEY_FILE_ID = "KEY_FILE_ID"
val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"
val KEY_SHARED_TEXT = "KEY_SHARED_TEXT"
val KEY_GEOCODING_QUERY = "KEY_GEOCODING_QUERY"
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parent_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/geocoding_results"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/roundedImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_baseline_location_on_24"
android:layout_gravity="center"/>
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/geocoding_result_text_size"
android:layout_gravity="center"
android:text="bbb"/>
</LinearLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017 Mario Danic
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Search, should appear as action button -->
<item android:id="@+id/geocoding_action_search"
android:title="@string/nc_search"
android:icon="@drawable/ic_search_white_24dp"
app:showAsAction="collapseActionView|always"
android:animateLayoutChanges="true"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017 Mario Danic
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Search, should appear as action button -->
<item android:id="@+id/location_action_search"
android:title="@string/nc_search"
android:icon="@drawable/ic_search_white_24dp"
app:showAsAction="collapseActionView|always"
android:animateLayoutChanges="true"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View file

@ -38,6 +38,8 @@
<dimen name="message_bubble_corners_radius">6dp</dimen>
<dimen name="message_bubble_corners_padding">8dp</dimen>
<dimen name="geocoding_result_text_size">18sp</dimen>
<dimen name="maximum_file_preview_size">192dp</dimen>
<dimen name="large_preview_dimension">80dp</dimen>