Browse Source

add location search with nominatim

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 4 years ago
parent
commit
2f0105abd3

+ 6 - 1
app/build.gradle

@@ -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'

+ 40 - 0
app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt

@@ -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
+    }
+}
+
+

+ 1 - 1
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -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())
         )

+ 199 - 0
app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt

@@ -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"
+    }
+}

+ 56 - 14
app/src/main/java/com/nextcloud/talk/controllers/LocationController.kt → app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt

@@ -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;
     }
 }

+ 1 - 0
app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt

@@ -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"
 }

+ 14 - 0
app/src/main/res/layout/controller_geocoding.xml

@@ -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>

+ 22 - 0
app/src/main/res/layout/geocoding_item.xml

@@ -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>

+ 33 - 0
app/src/main/res/menu/menu_geocoding.xml

@@ -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>

+ 33 - 0
app/src/main/res/menu/menu_locationpicker.xml

@@ -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>

+ 2 - 0
app/src/main/res/values/dimens.xml

@@ -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>