diff --git a/app/src/googleplay/java/org/tasks/location/GooglePlacesSearchProvider.kt b/app/src/googleplay/java/org/tasks/location/GooglePlacesSearchProvider.kt index 0b5c370fc..3ab9ae289 100644 --- a/app/src/googleplay/java/org/tasks/location/GooglePlacesSearchProvider.kt +++ b/app/src/googleplay/java/org/tasks/location/GooglePlacesSearchProvider.kt @@ -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.RectangularBounds 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.data.Place import org.tasks.data.Place.Companion.newPlace +import kotlin.coroutines.suspendCoroutine class GooglePlacesSearchProvider(private val context: Context) : PlaceSearchProvider { private var token: AutocompleteSessionToken? = null private var placesClient: PlacesClient? = null - override fun restoreState(savedInstanceState: Bundle) { - token = savedInstanceState.getParcelable(EXTRA_SESSION_TOKEN) + override fun restoreState(savedInstanceState: Bundle?) { + token = savedInstanceState?.getParcelable(EXTRA_SESSION_TOKEN) } 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 } - override fun search( - query: String, - bias: MapPosition?, - onSuccess: Callback>, - onError: Callback) { - if (!Places.isInitialized()) { - Places.initialize(context, context.getString(R.string.google_key)) - } - if (placesClient == null) { - placesClient = Places.createClient(context) - } - if (token == null) { - token = AutocompleteSessionToken.newInstance() - } - val request = FindAutocompletePredictionsRequest.builder().setSessionToken(token).setQuery(query) - if (bias != null) { - request.setLocationBias( - RectangularBounds.newInstance( - LatLngBounds.builder() - .include(LatLng(bias.latitude, bias.longitude)) - .build())) - } - placesClient!! - .findAutocompletePredictions(request.build()) - .addOnSuccessListener { response: FindAutocompletePredictionsResponse -> onSuccess.call(toSearchResults(response.autocompletePredictions)) } - .addOnFailureListener { e: Exception -> onError.call(e.message) } - } + override suspend fun search(query: String, bias: MapPosition?): List = + withContext(Dispatchers.IO) { + suspendCoroutine { cont -> + if (!Places.isInitialized()) { + Places.initialize(context, context.getString(R.string.google_key)) + } + if (placesClient == null) { + placesClient = Places.createClient(context) + } + if (token == null) { + token = AutocompleteSessionToken.newInstance() + } + val request = FindAutocompletePredictionsRequest.builder().setSessionToken(token).setQuery(query) + if (bias != null) { + request.locationBias = + RectangularBounds.newInstance( + LatLngBounds.builder() + .include(LatLng(bias.latitude, bias.longitude)) + .build()) + } + placesClient!! + .findAutocompletePredictions(request.build()) + .addOnSuccessListener { response: FindAutocompletePredictionsResponse -> + val places = toSearchResults(response.autocompletePredictions) + cont.resumeWith(Result.success(places)) + } + .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, onError: Callback) { - 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): List { return predictions.map { diff --git a/app/src/main/java/org/tasks/location/MapboxSearchProvider.java b/app/src/main/java/org/tasks/location/MapboxSearchProvider.java deleted file mode 100644 index d2e5a770b..000000000 --- a/app/src/main/java/org/tasks/location/MapboxSearchProvider.java +++ /dev/null @@ -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> onSuccess, - Callback 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() { - @Override - public void onResponse( - Call call, Response response) { - List results = new ArrayList<>(); - results.clear(); - for (CarmenFeature feature : response.body().features()) { - results.add(toSearchResult(feature)); - } - onSuccess.call(results); - } - - @Override - public void onFailure(Call call, Throwable t) { - onError.call(t.getMessage()); - } - }); - } - - @Override - public void fetch( - PlaceSearchResult placeSearchResult, Callback onSuccess, Callback onError) { - onSuccess.call(placeSearchResult.getPlace()); - } - - private PlaceSearchResult toSearchResult(CarmenFeature feature) { - Place place = newPlace(feature); - return new PlaceSearchResult(feature.id(), place.getName(), place.getDisplayAddress(), place); - } -} diff --git a/app/src/main/java/org/tasks/location/MapboxSearchProvider.kt b/app/src/main/java/org/tasks/location/MapboxSearchProvider.kt new file mode 100644 index 000000000..340462efd --- /dev/null +++ b/app/src/main/java/org/tasks/location/MapboxSearchProvider.kt @@ -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 = + 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 { + override fun onResponse( + call: Call, response: Response) { + val results: MutableList = ArrayList() + results.clear() + for (feature in response.body()!!.features()) { + results.add(toSearchResult(feature)) + } + cont.resumeWith(Result.success(results)) + } + + override fun onFailure(call: Call, 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/location/PlaceSearchProvider.java b/app/src/main/java/org/tasks/location/PlaceSearchProvider.java deleted file mode 100644 index ca2c6b73b..000000000 --- a/app/src/main/java/org/tasks/location/PlaceSearchProvider.java +++ /dev/null @@ -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> onSuccess, - Callback onError); - - void fetch( - PlaceSearchResult placeSearchResult, Callback onSuccess, Callback onError); -} diff --git a/app/src/main/java/org/tasks/location/PlaceSearchProvider.kt b/app/src/main/java/org/tasks/location/PlaceSearchProvider.kt new file mode 100644 index 000000000..b4439c15b --- /dev/null +++ b/app/src/main/java/org/tasks/location/PlaceSearchProvider.kt @@ -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 + + suspend fun fetch(placeSearchResult: PlaceSearchResult): Place +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/location/PlaceSearchViewModel.kt b/app/src/main/java/org/tasks/location/PlaceSearchViewModel.kt index ba99a6465..3c2e5c6a9 100644 --- a/app/src/main/java/org/tasks/location/PlaceSearchViewModel.kt +++ b/app/src/main/java/org/tasks/location/PlaceSearchViewModel.kt @@ -1,14 +1,10 @@ package org.tasks.location import android.os.Bundle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel -import com.todoroo.andlib.utility.AndroidUtilities +import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import org.tasks.Event -import org.tasks.Strings.isNullOrEmpty import org.tasks.data.Place import javax.inject.Inject @@ -30,7 +26,7 @@ class PlaceSearchViewModel @Inject constructor( error.observe(owner, onError!!) } - fun saveState(outState: Bundle?) { + fun saveState(outState: Bundle) { searchProvider.saveState(outState) } @@ -38,17 +34,24 @@ class PlaceSearchViewModel @Inject constructor( searchProvider.restoreState(savedInstanceState) } - fun query(query: String?, bias: MapPosition?) { - AndroidUtilities.assertMainThread() - if (isNullOrEmpty(query)) { + fun query(query: String?, bias: MapPosition?) = viewModelScope.launch { + if (query.isNullOrBlank()) { searchResults.postValue(emptyList()) } else { - searchProvider.search(query, bias, { value: List -> 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?) { - searchProvider.fetch(result, { value: Place -> selection.setValue(value) }) { message: String -> setError(message) } + fun fetch(result: PlaceSearchResult) = viewModelScope.launch { + try { + selection.value = searchProvider.fetch(result) + } catch (e: Exception) { + e.message?.let { setError(it) } + } } private fun setError(message: String) {