Merge pull request #3349 from nextcloud/LIveData

use MVVM for Geocoding
This commit is contained in:
Andy Scherzinger 2023-10-03 16:24:57 +02:00 committed by GitHub
commit f85aea157c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 141 additions and 64 deletions

View File

@ -20,6 +20,7 @@
package com.nextcloud.talk.adapters package com.nextcloud.talk.adapters
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -29,13 +30,19 @@ import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.R import com.nextcloud.talk.R
import fr.dudie.nominatim.model.Address import fr.dudie.nominatim.model.Address
class GeocodingAdapter(private val context: Context, private val dataSource: List<Address>) : class GeocodingAdapter(private val context: Context, private var dataSource: List<Address>) :
RecyclerView.Adapter<GeocodingAdapter.ViewHolder>() { RecyclerView.Adapter<GeocodingAdapter.ViewHolder>() {
interface OnItemClickListener { interface OnItemClickListener {
fun onItemClick(position: Int) fun onItemClick(position: Int)
} }
@SuppressLint("NotifyDataSetChanged")
fun updateData(data: List<Address>) {
this.dataSource = data
notifyDataSetChanged()
}
private var listener: OnItemClickListener? = null private var listener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) { fun setOnItemClickListener(listener: OnItemClickListener) {
this.listener = listener this.listener = listener

View File

@ -32,12 +32,11 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import autodagger.AutoInjector import autodagger.AutoInjector
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.adapters.GeocodingAdapter import com.nextcloud.talk.adapters.GeocodingAdapter
@ -45,21 +44,16 @@ import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.ActivityGeocodingBinding import com.nextcloud.talk.databinding.ActivityGeocodingBinding
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.viewmodels.GeoCodingViewModel
import fr.dudie.nominatim.client.TalkJsonNominatimClient import fr.dudie.nominatim.client.TalkJsonNominatimClient
import fr.dudie.nominatim.model.Address 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 okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import javax.inject.Inject import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
class GeocodingActivity : class GeocodingActivity :
BaseActivity(), BaseActivity() {
SearchView.OnQueryTextListener {
private lateinit var binding: ActivityGeocodingBinding private lateinit var binding: ActivityGeocodingBinding
@ -70,15 +64,15 @@ class GeocodingActivity :
lateinit var okHttpClient: OkHttpClient lateinit var okHttpClient: OkHttpClient
lateinit var roomToken: String lateinit var roomToken: String
var nominatimClient: TalkJsonNominatimClient? = null private var nominatimClient: TalkJsonNominatimClient? = null
var searchItem: MenuItem? = null private var searchItem: MenuItem? = null
var searchView: SearchView? = null var searchView: SearchView? = null
var query: String? = null
lateinit var adapter: GeocodingAdapter lateinit var adapter: GeocodingAdapter
private var geocodingResults: List<Address> = ArrayList() private var geocodingResults: List<Address> = ArrayList()
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var viewModel: GeoCodingViewModel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -92,11 +86,27 @@ class GeocodingActivity :
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!! roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
query = intent.getStringExtra(BundleKeys.KEY_GEOCODING_QUERY)
recyclerView = findViewById(R.id.geocoding_results) recyclerView = findViewById(R.id.geocoding_results)
recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.layoutManager = LinearLayoutManager(this)
adapter = GeocodingAdapter(this, geocodingResults) adapter = GeocodingAdapter(this, geocodingResults)
recyclerView.adapter = adapter recyclerView.adapter = adapter
viewModel = ViewModelProvider(this)[GeoCodingViewModel::class.java]
var query = viewModel.getQuery()
if (query.isEmpty() && intent.hasExtra(BundleKeys.KEY_GEOCODING_QUERY)) {
query = intent.getStringExtra(BundleKeys.KEY_GEOCODING_QUERY).orEmpty()
viewModel.setQuery(query)
}
val savedResults = viewModel.getGeocodingResults()
initAdapter(savedResults)
viewModel.getGeocodingResultsLiveData().observe(this) { results ->
geocodingResults = results
adapter.updateData(results)
}
val baseUrl = getString(R.string.osm_geocoder_url)
val email = context.getString(R.string.osm_geocoder_contact)
nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email)
} }
override fun onStart() { override fun onStart() {
@ -108,12 +118,11 @@ class GeocodingActivity :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (!query.isNullOrEmpty()) { if (viewModel.getQuery().isNotEmpty() && adapter.itemCount == 0) {
searchLocation() viewModel.searchLocation()
} else { } else {
Log.e(TAG, "search string that was passed to GeocodingController was null or empty") Log.e(TAG, "search string that was passed to GeocodingController was null or empty")
} }
adapter.setOnItemClickListener(object : GeocodingAdapter.OnItemClickListener { adapter.setOnItemClickListener(object : GeocodingAdapter.OnItemClickListener {
override fun onItemClick(position: Int) { override fun onItemClick(position: Int) {
val address: Address = adapter.getItem(position) as Address val address: Address = adapter.getItem(position) as Address
@ -125,6 +134,7 @@ class GeocodingActivity :
startActivity(intent) startActivity(intent)
} }
}) })
searchView?.setQuery(viewModel.getQuery(), false)
} }
private fun setupActionBar() { private fun setupActionBar() {
@ -160,38 +170,43 @@ class GeocodingActivity :
menuInflater.inflate(R.menu.menu_geocoding, menu) menuInflater.inflate(R.menu.menu_geocoding, menu)
searchItem = menu.findItem(R.id.geocoding_action_search) searchItem = menu.findItem(R.id.geocoding_action_search)
initSearchView() initSearchView()
searchItem?.expandActionView() searchItem?.expandActionView()
searchView?.setQuery(query, false) searchView?.setQuery(viewModel.getQuery(), false)
searchView?.clearFocus() searchView?.clearFocus()
return true return true
} }
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() { private fun initSearchView() {
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
if (searchItem != null) { if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView searchView = searchItem!!.actionView as SearchView?
searchView?.maxWidth = Int.MAX_VALUE searchView?.maxWidth = Int.MAX_VALUE
searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.isKeyboardIncognito) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
} }
searchView?.imeOptions = imeOptions searchView?.imeOptions = imeOptions
searchView?.queryHint = resources!!.getString(R.string.nc_search) searchView?.queryHint = resources!!.getString(R.string.nc_search)
searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName)) searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView?.setOnQueryTextListener(this) searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
viewModel.setQuery(query)
viewModel.searchLocation()
searchView?.clearFocus()
return true
}
override fun onQueryTextChange(query: String): Boolean {
// This is a workaround to not set viewModel data when onQueryTextChange is triggered on startup
// Otherwise it would be set to an empty string.
if (searchView?.width!! > 0) {
viewModel.setQuery(query)
}
return true
}
})
searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean { override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
@ -215,37 +230,7 @@ class GeocodingActivity :
nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email)
} }
private fun searchLocation(): Boolean {
CoroutineScope(IO).launch {
executeGeocodingRequest()
}
return true
}
@Suppress("Detekt.TooGenericExceptionCaught")
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)
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
updateResultsOnMainThread(results)
}
private suspend fun updateResultsOnMainThread(results: ArrayList<Address>) {
withContext(Main) {
initAdapter(results)
}
}
companion object { companion object {
private val TAG = GeocodingActivity::class.java.simpleName val TAG = GeocodingActivity::class.java.simpleName
} }
} }

View File

@ -0,0 +1,85 @@
/*
* Nextcloud Talk application
*
* @author Samanwith KSN
* Copyright (C) 2023 Samanwith KSN <samanwith21@gmail.com>
*
* 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/>.
*/
package com.nextcloud.talk.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.nextcloud.talk.activities.CallActivity.Companion.TAG
import com.nextcloud.talk.location.GeocodingActivity
import fr.dudie.nominatim.client.TalkJsonNominatimClient
import fr.dudie.nominatim.model.Address
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import java.io.IOException
class GeoCodingViewModel : ViewModel() {
private val geocodingResultsLiveData = MutableLiveData<List<Address>>()
private val nominatimClient: TalkJsonNominatimClient
private val okHttpClient: OkHttpClient = OkHttpClient.Builder().build()
private var geocodingResults: List<Address> = ArrayList()
private var query: String = ""
fun getGeocodingResultsLiveData(): LiveData<List<Address>> {
return geocodingResultsLiveData
}
fun getQuery(): String {
return query
}
fun setQuery(query: String) {
this.query = query
}
fun getGeocodingResults(): List<Address> {
return geocodingResults
}
init {
nominatimClient = TalkJsonNominatimClient(
"https://nominatim.openstreetmap.org/",
okHttpClient,
" android@nextcloud.com"
)
}
fun searchLocation() {
if (query.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
try {
val results = nominatimClient.search(query) as ArrayList<Address>
for (address in results) {
Log.d(GeocodingActivity.TAG, address.displayName)
Log.d(GeocodingActivity.TAG, address.latitude.toString())
Log.d(GeocodingActivity.TAG, address.longitude.toString())
}
geocodingResults = results
geocodingResultsLiveData.postValue(results)
} catch (e: IOException) {
Log.e(TAG, "Failed to get geocoded addresses", e)
}
}
}
}
}