Convert place search to coroutines

pull/1369/head
Alex Baker 5 years ago
parent f0f6478dab
commit 14f5015fac

@ -10,17 +10,19 @@ import com.google.android.libraries.places.api.model.AutocompleteSessionToken
import com.google.android.libraries.places.api.model.Place.Field import com.google.android.libraries.places.api.model.Place.Field
import com.google.android.libraries.places.api.model.RectangularBounds import com.google.android.libraries.places.api.model.RectangularBounds
import com.google.android.libraries.places.api.net.* import com.google.android.libraries.places.api.net.*
import org.tasks.Callback import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.R import org.tasks.R
import org.tasks.data.Place import org.tasks.data.Place
import org.tasks.data.Place.Companion.newPlace import org.tasks.data.Place.Companion.newPlace
import kotlin.coroutines.suspendCoroutine
class GooglePlacesSearchProvider(private val context: Context) : PlaceSearchProvider { class GooglePlacesSearchProvider(private val context: Context) : PlaceSearchProvider {
private var token: AutocompleteSessionToken? = null private var token: AutocompleteSessionToken? = null
private var placesClient: PlacesClient? = null private var placesClient: PlacesClient? = null
override fun restoreState(savedInstanceState: Bundle) { override fun restoreState(savedInstanceState: Bundle?) {
token = savedInstanceState.getParcelable(EXTRA_SESSION_TOKEN) token = savedInstanceState?.getParcelable(EXTRA_SESSION_TOKEN)
} }
override fun saveState(outState: Bundle) { override fun saveState(outState: Bundle) {
@ -31,52 +33,63 @@ class GooglePlacesSearchProvider(private val context: Context) : PlaceSearchProv
return if (dark) R.drawable.places_powered_by_google_dark else R.drawable.places_powered_by_google_light return if (dark) R.drawable.places_powered_by_google_dark else R.drawable.places_powered_by_google_light
} }
override fun search( override suspend fun search(query: String, bias: MapPosition?): List<PlaceSearchResult> =
query: String, withContext(Dispatchers.IO) {
bias: MapPosition?, suspendCoroutine { cont ->
onSuccess: Callback<List<PlaceSearchResult>>, if (!Places.isInitialized()) {
onError: Callback<String>) { Places.initialize(context, context.getString(R.string.google_key))
if (!Places.isInitialized()) { }
Places.initialize(context, context.getString(R.string.google_key)) if (placesClient == null) {
} placesClient = Places.createClient(context)
if (placesClient == null) { }
placesClient = Places.createClient(context) if (token == null) {
} token = AutocompleteSessionToken.newInstance()
if (token == null) { }
token = AutocompleteSessionToken.newInstance() val request = FindAutocompletePredictionsRequest.builder().setSessionToken(token).setQuery(query)
} if (bias != null) {
val request = FindAutocompletePredictionsRequest.builder().setSessionToken(token).setQuery(query) request.locationBias =
if (bias != null) { RectangularBounds.newInstance(
request.setLocationBias( LatLngBounds.builder()
RectangularBounds.newInstance( .include(LatLng(bias.latitude, bias.longitude))
LatLngBounds.builder() .build())
.include(LatLng(bias.latitude, bias.longitude)) }
.build())) placesClient!!
} .findAutocompletePredictions(request.build())
placesClient!! .addOnSuccessListener { response: FindAutocompletePredictionsResponse ->
.findAutocompletePredictions(request.build()) val places = toSearchResults(response.autocompletePredictions)
.addOnSuccessListener { response: FindAutocompletePredictionsResponse -> onSuccess.call(toSearchResults(response.autocompletePredictions)) } cont.resumeWith(Result.success(places))
.addOnFailureListener { e: Exception -> onError.call(e.message) } }
} .addOnFailureListener { e: Exception ->
cont.resumeWith(Result.failure(e))
}
}
}
override suspend fun fetch(placeSearchResult: PlaceSearchResult): Place =
withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
placesClient!!
.fetchPlace(
FetchPlaceRequest.builder(
placeSearchResult.id,
listOf(
Field.ID,
Field.LAT_LNG,
Field.ADDRESS,
Field.WEBSITE_URI,
Field.NAME,
Field.PHONE_NUMBER))
.setSessionToken(token)
.build())
.addOnSuccessListener { result: FetchPlaceResponse ->
cont.resumeWith(Result.success(toPlace(result)))
}
.addOnFailureListener { e: Exception ->
cont.resumeWith(Result.failure(e))
}
}
}
override fun fetch(
placeSearchResult: PlaceSearchResult, onSuccess: Callback<Place>, onError: Callback<String>) {
placesClient!!
.fetchPlace(
FetchPlaceRequest.builder(
placeSearchResult.id,
listOf(
Field.ID,
Field.LAT_LNG,
Field.ADDRESS,
Field.WEBSITE_URI,
Field.NAME,
Field.PHONE_NUMBER))
.setSessionToken(token)
.build())
.addOnSuccessListener { result: FetchPlaceResponse -> onSuccess.call(toPlace(result)) }
.addOnFailureListener { e: Exception -> onError.call(e.message) }
}
private fun toSearchResults(predictions: List<AutocompletePrediction>): List<PlaceSearchResult> { private fun toSearchResults(predictions: List<AutocompletePrediction>): List<PlaceSearchResult> {
return predictions.map { return predictions.map {

@ -1,87 +0,0 @@
package org.tasks.location;
import static org.tasks.data.Place.newPlace;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.mapbox.api.geocoding.v5.MapboxGeocoding;
import com.mapbox.api.geocoding.v5.models.CarmenFeature;
import com.mapbox.api.geocoding.v5.models.GeocodingResponse;
import com.mapbox.geojson.Point;
import java.util.ArrayList;
import java.util.List;
import org.tasks.Callback;
import org.tasks.R;
import org.tasks.data.Place;
import retrofit2.Call;
import retrofit2.Response;
public class MapboxSearchProvider implements PlaceSearchProvider {
private final Context context;
private MapboxGeocoding.Builder builder;
public MapboxSearchProvider(Context context) {
this.context = context;
}
@Override
public void restoreState(Bundle savedInstanceState) {}
@Override
public void saveState(Bundle outState) {}
@Override
public int getAttributionRes(boolean dark) {
return R.drawable.mapbox_logo_icon;
}
@Override
public void search(
String query,
@Nullable MapPosition bias,
Callback<List<PlaceSearchResult>> onSuccess,
Callback<String> onError) {
if (builder == null) {
String token = context.getString(R.string.mapbox_key);
builder = MapboxGeocoding.builder().autocomplete(true).accessToken(token);
if (bias != null) {
builder.proximity(Point.fromLngLat(bias.getLongitude(), bias.getLatitude()));
}
}
builder
.query(query)
.build()
.enqueueCall(
new retrofit2.Callback<GeocodingResponse>() {
@Override
public void onResponse(
Call<GeocodingResponse> call, Response<GeocodingResponse> response) {
List<PlaceSearchResult> results = new ArrayList<>();
results.clear();
for (CarmenFeature feature : response.body().features()) {
results.add(toSearchResult(feature));
}
onSuccess.call(results);
}
@Override
public void onFailure(Call<GeocodingResponse> call, Throwable t) {
onError.call(t.getMessage());
}
});
}
@Override
public void fetch(
PlaceSearchResult placeSearchResult, Callback<Place> onSuccess, Callback<String> onError) {
onSuccess.call(placeSearchResult.getPlace());
}
private PlaceSearchResult toSearchResult(CarmenFeature feature) {
Place place = newPlace(feature);
return new PlaceSearchResult(feature.id(), place.getName(), place.getDisplayAddress(), place);
}
}

@ -0,0 +1,67 @@
package org.tasks.location
import android.content.Context
import android.os.Bundle
import com.mapbox.api.geocoding.v5.MapboxGeocoding
import com.mapbox.api.geocoding.v5.models.CarmenFeature
import com.mapbox.api.geocoding.v5.models.GeocodingResponse
import com.mapbox.geojson.Point
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.R
import org.tasks.data.Place
import org.tasks.data.Place.Companion.newPlace
import retrofit2.Call
import retrofit2.Response
import java.util.*
import kotlin.coroutines.suspendCoroutine
class MapboxSearchProvider(private val context: Context) : PlaceSearchProvider {
private var builder: MapboxGeocoding.Builder? = null
override fun restoreState(savedInstanceState: Bundle?) {}
override fun saveState(outState: Bundle) {}
override fun getAttributionRes(dark: Boolean) = R.drawable.mapbox_logo_icon
override suspend fun search(query: String, bias: MapPosition?): List<PlaceSearchResult> =
withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
if (builder == null) {
val token = context.getString(R.string.mapbox_key)
builder = MapboxGeocoding.builder().autocomplete(true).accessToken(token)
if (bias != null) {
builder?.proximity(Point.fromLngLat(bias.longitude, bias.latitude))
}
}
builder
?.query(query)
?.build()
?.enqueueCall(
object : retrofit2.Callback<GeocodingResponse> {
override fun onResponse(
call: Call<GeocodingResponse>, response: Response<GeocodingResponse>) {
val results: MutableList<PlaceSearchResult> = ArrayList()
results.clear()
for (feature in response.body()!!.features()) {
results.add(toSearchResult(feature))
}
cont.resumeWith(Result.success(results))
}
override fun onFailure(call: Call<GeocodingResponse>, t: Throwable) {
cont.resumeWith(Result.failure(t))
}
})
}
}
override suspend fun fetch(placeSearchResult: PlaceSearchResult): Place =
placeSearchResult.place
private fun toSearchResult(feature: CarmenFeature): PlaceSearchResult {
val place = newPlace(feature)
return PlaceSearchResult(feature.id(), place.name, place.displayAddress, place)
}
}

@ -1,25 +0,0 @@
package org.tasks.location;
import android.os.Bundle;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import java.util.List;
import org.tasks.Callback;
import org.tasks.data.Place;
public interface PlaceSearchProvider {
void restoreState(Bundle savedInstanceState);
void saveState(Bundle outState);
@DrawableRes int getAttributionRes(boolean dark);
void search(
String query,
@Nullable MapPosition bias,
Callback<List<PlaceSearchResult>> onSuccess,
Callback<String> onError);
void fetch(
PlaceSearchResult placeSearchResult, Callback<Place> onSuccess, Callback<String> onError);
}

@ -0,0 +1,19 @@
package org.tasks.location
import android.os.Bundle
import androidx.annotation.DrawableRes
import org.tasks.Callback
import org.tasks.data.Place
interface PlaceSearchProvider {
fun restoreState(savedInstanceState: Bundle?)
fun saveState(outState: Bundle)
@DrawableRes
fun getAttributionRes(dark: Boolean): Int
suspend fun search(query: String, bias: MapPosition?): List<PlaceSearchResult>
suspend fun fetch(placeSearchResult: PlaceSearchResult): Place
}

@ -1,14 +1,10 @@
package org.tasks.location package org.tasks.location
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.*
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import com.todoroo.andlib.utility.AndroidUtilities
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.tasks.Event import org.tasks.Event
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Place import org.tasks.data.Place
import javax.inject.Inject import javax.inject.Inject
@ -30,7 +26,7 @@ class PlaceSearchViewModel @Inject constructor(
error.observe(owner, onError!!) error.observe(owner, onError!!)
} }
fun saveState(outState: Bundle?) { fun saveState(outState: Bundle) {
searchProvider.saveState(outState) searchProvider.saveState(outState)
} }
@ -38,17 +34,24 @@ class PlaceSearchViewModel @Inject constructor(
searchProvider.restoreState(savedInstanceState) searchProvider.restoreState(savedInstanceState)
} }
fun query(query: String?, bias: MapPosition?) { fun query(query: String?, bias: MapPosition?) = viewModelScope.launch {
AndroidUtilities.assertMainThread() if (query.isNullOrBlank()) {
if (isNullOrEmpty(query)) {
searchResults.postValue(emptyList()) searchResults.postValue(emptyList())
} else { } else {
searchProvider.search(query, bias, { value: List<PlaceSearchResult> -> searchResults.setValue(value) }) { message: String -> setError(message) } try {
searchResults.value = searchProvider.search(query, bias)
} catch (e: Exception) {
e.message?.let { setError(it) }
}
} }
} }
fun fetch(result: PlaceSearchResult?) { fun fetch(result: PlaceSearchResult) = viewModelScope.launch {
searchProvider.fetch(result, { value: Place -> selection.setValue(value) }) { message: String -> setError(message) } try {
selection.value = searchProvider.fetch(result)
} catch (e: Exception) {
e.message?.let { setError(it) }
}
} }
private fun setError(message: String) { private fun setError(message: String) {

Loading…
Cancel
Save