Replace Places SDK with HTTP API

pull/1376/head
Alex Baker 3 years ago
parent 4f9d229c4b
commit 0e728152c9

@ -96,6 +96,7 @@ android {
resValue("string", "google_key", tasks_google_key_debug ?: "")
resValue("string", "tasks_caldav_url", tasks_caldav_url ?: "https://caldav.tasks.org")
resValue("string", "tasks_nominatim_url", tasks_caldav_url ?: "https://nominatim.tasks.org")
resValue("string", "tasks_places_url", tasks_caldav_url ?: "https://places.tasks.org")
isTestCoverageEnabled = project.hasProperty("coverage")
}
getByName("release") {
@ -215,7 +216,6 @@ dependencies {
googleplayImplementation("com.google.firebase:firebase-config-ktx:${Versions.remote_config}")
googleplayImplementation("com.google.android.gms:play-services-location:17.1.0")
googleplayImplementation("com.google.android.gms:play-services-maps:17.0.0")
googleplayImplementation("com.google.android.libraries.places:places:2.4.0")
googleplayImplementation("com.android.billingclient:billing:1.2.2")
androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.hilt}")

@ -5,20 +5,16 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import dagger.hilt.android.scopes.ViewModelScoped
import org.tasks.location.*
import org.tasks.location.AndroidLocationProvider
import org.tasks.location.LocationProvider
import org.tasks.location.MapFragment
import org.tasks.location.OsmMapFragment
@Module
@InstallIn(ActivityComponent::class, ViewModelComponent::class)
@InstallIn(ActivityComponent::class)
class LocationModule {
@Provides
@ViewModelScoped
fun getPlaceSearchProvider(mapboxSearchProvider: PlaceSearchMapbox): PlaceSearch =
mapboxSearchProvider
@Provides
@ActivityScoped
fun getLocationProvider(provider: AndroidLocationProvider): LocationProvider = provider

@ -5,37 +5,15 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import dagger.hilt.android.scopes.ViewModelScoped
import org.tasks.R
import org.tasks.billing.Inventory
import org.tasks.gtasks.PlayServices
import org.tasks.location.*
import org.tasks.preferences.Preferences
@Module
@InstallIn(ActivityComponent::class, ViewModelComponent::class)
@InstallIn(ActivityComponent::class)
internal class LocationModule {
@Provides
@ViewModelScoped
fun getPlaceSearchProvider(
@ApplicationContext context: Context,
preferences: Preferences,
playServices: PlayServices,
inventory: Inventory,
mapboxSearchProvider: MapboxSearchProvider
): PlaceSearchProvider {
return if (preferences.useGooglePlaces()
&& playServices.isPlayServicesAvailable
&& inventory.hasPro) {
PlaceSearchGoogle(context)
} else {
mapboxSearchProvider
}
}
@Provides
@ActivityScoped
fun getLocationProvider(@ApplicationContext context: Context): LocationProvider =

@ -1,128 +0,0 @@
package org.tasks.location
import android.content.Context
import android.os.Bundle
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.AutocompletePrediction
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 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 PlaceSearchGoogle(private val context: Context) : PlaceSearch {
private var token: AutocompleteSessionToken? = null
private var placesClient: PlacesClient? = null
override fun restoreState(savedInstanceState: Bundle?) {
token = savedInstanceState?.getParcelable(EXTRA_SESSION_TOKEN)
}
override fun saveState(outState: Bundle) {
outState.putParcelable(EXTRA_SESSION_TOKEN, token)
}
override fun getAttributionRes(dark: Boolean): Int {
return if (dark) R.drawable.places_powered_by_google_dark else R.drawable.places_powered_by_google_light
}
override suspend fun search(query: String, bias: MapPosition?): List<PlaceSearchResult> =
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))
}
}
}
private fun toSearchResults(predictions: List<AutocompletePrediction>): List<PlaceSearchResult> {
return predictions.map {
PlaceSearchResult(
it.placeId,
it.getPrimaryText(null).toString(),
it.getSecondaryText(null).toString())
}
}
private fun toPlace(fetchPlaceResponse: FetchPlaceResponse): Place {
val place = fetchPlaceResponse.place
val result = newPlace()
result.name = place.name
val address: CharSequence? = place.address
if (address != null) {
result.address = place.address
}
val phoneNumber: CharSequence? = place.phoneNumber
if (phoneNumber != null) {
result.phone = phoneNumber.toString()
}
val uri = place.websiteUri
if (uri != null) {
result.url = uri.toString()
}
val latLng = place.latLng
result.latitude = latLng!!.latitude
result.longitude = latLng.longitude
return result
}
companion object {
private const val EXTRA_SESSION_TOKEN = "extra_session_token"
}
}

@ -140,6 +140,7 @@ _${getString(R.string.account_not_included)}_
#### ${getString(R.string.tasks_org_account)}
* ${getString(R.string.tasks_org_description)}
* [${getString(R.string.upgrade_third_party_apps)}](${getString(R.string.url_app_passwords)})
* ${getString(R.string.upgrade_google_places)}
* [${getString(R.string.upgrade_coming_soon)}](${getString(R.string.help_url_sync)})
"""
}
@ -155,7 +156,6 @@ _${getString(R.string.account_not_included)}_
---
#### ${getString(R.string.upgrade_additional_features)}
* ${getString(R.string.upgrade_themes)}
* ${getString(R.string.upgrade_google_places)}
* [${getString(R.string.upgrade_tasker)}](${getString(R.string.url_tasker)})
---
* ${getString(R.string.upgrade_free_trial)}

@ -2,6 +2,7 @@ package org.tasks.http
import android.content.Context
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4jvm.BasicDigestAuthHandler
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -9,6 +10,7 @@ import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.DebugNetworkInterceptor
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import javax.inject.Inject
import javax.net.ssl.SSLContext
@ -16,16 +18,30 @@ class HttpClientFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val preferences: Preferences,
private val interceptor: DebugNetworkInterceptor,
private val encryption: KeyStoreEncryption,
) {
suspend fun newCertManager() = withContext(Dispatchers.Default) {
CustomCertManager(context)
}
suspend fun newBuilder(foreground: Boolean = false): OkHttpClient.Builder =
newBuilder(newCertManager(), foreground)
suspend fun newBuilder(
foreground: Boolean = false,
username: String? = null,
encryptedPassword: String? = null
): OkHttpClient.Builder = newBuilder(
newCertManager(),
foreground = foreground,
username = username,
password = encryptedPassword?.let { encryption.decrypt(it) }
)
fun newBuilder(customCertManager: CustomCertManager, foreground: Boolean = false): OkHttpClient.Builder {
fun newBuilder(
customCertManager: CustomCertManager,
foreground: Boolean = false,
username: String? = null,
password: String? = null
): OkHttpClient.Builder {
customCertManager.appInForeground = foreground
val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier)
val sslContext = SSLContext.getInstance("TLS")
@ -37,6 +53,11 @@ class HttpClientFactory @Inject constructor(
if (preferences.isFlipperEnabled) {
interceptor.apply(builder)
}
if (!username.isNullOrBlank() && !password.isNullOrBlank()) {
val auth = BasicDigestAuthHandler(null, username, password)
builder.addNetworkInterceptor(auth)
builder.authenticator(auth)
}
return builder
}
}

@ -0,0 +1,24 @@
package org.tasks.injection
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
import org.tasks.billing.Inventory
import org.tasks.location.PlaceSearch
import org.tasks.location.PlaceSearchGoogle
import org.tasks.location.PlaceSearchMapbox
@Module
@InstallIn(ViewModelComponent::class)
class ViewModelModule {
@Provides
@ViewModelScoped
fun getPlaceSearchProvider(
inventory: Inventory,
google: Lazy<PlaceSearchGoogle>,
mapbox: Lazy<PlaceSearchMapbox>
): PlaceSearch = if (inventory.hasTasksSubscription) google.get() else mapbox.get()
}

@ -23,7 +23,7 @@ class GeocoderNominatim @Inject constructor(
override suspend fun reverseGeocode(mapPosition: MapPosition): Place? =
withContext(Dispatchers.IO) {
val client = httpClientFactory.newBuilder(true).build()
val client = httpClientFactory.newBuilder(foreground = true).build()
val url = "$url/reverse?format=geocodejson&lat=${mapPosition.latitude}&lon=${mapPosition.longitude}"
val response = client.newCall(
Request.Builder().get().url(url).addHeader(USER_AGENT, UA_VALUE).build()

@ -303,7 +303,7 @@ class LocationPickerActivity : InjectingAppCompatActivity(), Toolbar.OnMenuItemC
searchSubject
.debounce(SEARCH_DEBOUNCE_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { query: String? -> viewModel.query(query, mapPosition) })
.subscribe { query: String? -> viewModel.query(query, map.mapPosition) })
}
override fun onDestroy() {

@ -0,0 +1,146 @@
package org.tasks.location
import android.content.Context
import android.os.Bundle
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
import org.tasks.R
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.data.CaldavDao
import org.tasks.data.Place
import org.tasks.data.Place.Companion.newPlace
import org.tasks.http.HttpClientFactory
import org.tasks.http.HttpException
import timber.log.Timber
import java.util.*
import javax.inject.Inject
class PlaceSearchGoogle @Inject constructor(
@ApplicationContext private val context: Context,
private val httpClientFactory: HttpClientFactory,
private val caldavDao: CaldavDao
) : PlaceSearch {
private val url = context.getString(R.string.tasks_places_url)
private var token: String? = null
override fun restoreState(savedInstanceState: Bundle?) {
token = savedInstanceState?.getParcelable(EXTRA_SESSION_TOKEN)
}
override fun saveState(outState: Bundle) {
outState.putString(EXTRA_SESSION_TOKEN, token)
}
override fun getAttributionRes(dark: Boolean) = if (dark) {
R.drawable.powered_by_google_on_non_white
} else {
R.drawable.powered_by_google_on_white
}
override suspend fun search(query: String, bias: MapPosition?): List<PlaceSearchResult> {
if (token == null) {
token = UUID.randomUUID().toString()
}
val proximity = bias?.let {
"&location=${bias.latitude},${bias.longitude}&radius=25000"
}
val jsonObject = execute(
"${this.url}/maps/api/place/queryautocomplete/json?input=$query&sessiontoken=$token$proximity"
)
return toSearchResults(jsonObject)
}
override suspend fun fetch(placeSearchResult: PlaceSearchResult): Place {
val jsonObject = execute(
"${this.url}/maps/api/place/details/json?place_id=${placeSearchResult.id}&fields=$FIELDS&sessiontoken=$token"
)
return toPlace(jsonObject)
}
private suspend fun execute(url: String): JsonObject = withContext(Dispatchers.IO) {
Timber.d(url)
val account = caldavDao.getAccounts(TYPE_TASKS).firstOrNull()
?: throw IllegalStateException(
context.getString(R.string.tasks_org_account_required)
)
val client = httpClientFactory
.newBuilder(
foreground = true,
username = account.username,
encryptedPassword = account.password
)
.build()
val response = client.newCall(Request.Builder().get().url(url).build()).execute()
if (response.isSuccessful) {
response.body?.string()?.toJson()?.apply { checkResult(this) }
?: throw IllegalStateException("Request failed")
} else {
throw HttpException(response.code, response.message)
}
}
companion object {
private const val EXTRA_SESSION_TOKEN = "extra_session_token"
private val FIELDS =
listOf(
"place_id",
"geometry/location",
"formatted_address",
"website",
"name",
"international_phone_number"
).joinToString(",")
internal fun String.toJson(): JsonObject = JsonParser.parseString(this).asJsonObject
private fun checkResult(json: JsonObject) {
val status = json.get("status").asString
when {
status == "OK" -> return
json.has("error_message") ->
throw IllegalStateException(json.get("error_message").asString)
else ->
throw IllegalStateException(status)
}
}
internal fun toSearchResults(json: JsonObject): List<PlaceSearchResult> =
json.get("predictions")
.asJsonArray
.map { it.asJsonObject }
.filter { it.has("place_id") }
.map { toSearchEntry(it) }
private fun toSearchEntry(json: JsonObject): PlaceSearchResult {
val place = json.get("structured_formatting").asJsonObject
return PlaceSearchResult(
json.get("place_id").asString,
place.get("main_text").asString,
place.get("secondary_text").asString
)
}
internal fun toPlace(json: JsonObject): Place {
val result = json.get("result").asJsonObject
val location = result.get("geometry").asJsonObject.get("location").asJsonObject
return newPlace().apply {
name = result.get("name").asString
address = result.getString("formatted_address")
phone = result.getString("international_phone_number")
url = result.getString("website")
latitude = location.get("lat").asDouble
longitude = location.get("lng").asDouble
}
}
private fun JsonObject.getString(field: String): String? = if (has(field)) {
get(field).asString
} else {
null
}
}
}

@ -423,8 +423,6 @@ class Preferences @JvmOverloads constructor(
syncFlags.forEach { setBoolean(it, value) }
}
fun useGooglePlaces(): Boolean = getIntegerFromString(R.string.p_place_provider, 0) == 1
fun <T> getPrefs(c: Class<T>): Map<String, T> {
val result: MutableMap<String, T> = HashMap()
val entries: Iterable<Map.Entry<String, *>> = prefs.all.entries.filter { e: Map.Entry<String?, Any?> -> c.isInstance(e.value) }

@ -35,16 +35,10 @@ class LocationPreferences : InjectingPreferenceFragment() {
override suspend fun setupPreferences(savedInstanceState: Bundle?) {
if (IS_GOOGLE_PLAY) {
findPreference(R.string.p_place_provider)
.setOnPreferenceChangeListener(this::onPlaceSearchChanged)
findPreference(R.string.p_geofence_service)
.setOnPreferenceChangeListener(this::onGeofenceServiceChanged)
} else {
disable(
R.string.p_map_tiles,
R.string.p_place_provider,
R.string.p_geofence_service
)
disable(R.string.p_map_tiles, R.string.p_geofence_service)
}
}
@ -71,21 +65,6 @@ class LocationPreferences : InjectingPreferenceFragment() {
findPreference(R.string.p_geofence_service).isEnabled = hasPermissions && IS_GOOGLE_PLAY
}
private fun onPlaceSearchChanged(preference: Preference, newValue: Any): Boolean =
if (newValue.toString().toIntOrNull() ?: 0 == 1) {
if (!playServices.refreshAndCheck()) {
playServices.resolve(activity)
false
} else if (!inventory.hasPro) {
toaster.longToast(R.string.requires_pro_subscription)
false
} else {
true
}
} else {
true
}
private fun onGeofenceServiceChanged(preference: Preference, newValue: Any): Boolean =
if (newValue.toString().toIntOrNull() ?: 0 == 1) {
if (!playServices.refreshAndCheck()) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

@ -188,7 +188,6 @@
<string name="building_notifications">توليد الإخطارات</string>
<string name="bundle_notifications_summary">ادمج عدة إشعارات في شعار واحد</string>
<string name="notification_troubleshooting_summary">انقر هنا إذا كنت تواجه مشاكل في الإشعارات</string>
<string name="map_search_provider">مزود الخرائط</string>
<string name="TEA_control_location">الموقع</string>
<string name="EPr_show_task_edit_comments">إظهار التعليقات في شاشة تحرير المهمة</string>
<string name="back_button_saves_task">الضغط على زر الرجوع يحفظ المهمة</string>

@ -438,7 +438,6 @@
<string name="choose_a_location">Изберете местоположение</string>
<string name="pick_this_location">Избери това местоположение</string>
<string name="or_choose_a_location">Или избери местоположение</string>
<string name="map_search_provider">Намери доставчик</string>
<string name="missing_permissions">Липсващи разрешения</string>
<string name="location_permission_required_location">Необходими са разрешения за местоположение, за да откриете текущото ви местоположение</string>
<string name="open_map">Отворете картата</string>

@ -464,7 +464,6 @@
<string name="theme_system_default">Dle systému</string>
<string name="repeat_monthly_on_every_day_of_nth_week">každý %1$s %2$s</string>
<string name="or_choose_a_location">Nebo vyberte místo</string>
<string name="map_search_provider">Poskytovatel vyhledávání</string>
<string name="missing_permissions">Chybí oprávnění</string>
<string name="location_permission_required_location">Oprávnění k přístupu k poloze je nutné pro zjištění Vaší aktuální polohy</string>
<string name="open_map">Otevřít mapu</string>

@ -211,7 +211,6 @@
<string name="open_map">Åbn kort</string>
<string name="location_permission_required_location">Placeringstilladelse kræves for at finde din nuværende placering</string>
<string name="missing_permissions">Manglende tilladelser</string>
<string name="map_search_provider">Søgeudbyder</string>
<string name="or_choose_a_location">Eller vælg et sted</string>
<string name="pick_this_location">Vælg dette sted</string>
<string name="choose_a_location">Vælg sted</string>

@ -435,7 +435,6 @@
<string name="choose_a_location">Ort auswählen</string>
<string name="pick_this_location">Diesen Ort auswählen</string>
<string name="or_choose_a_location">Oder Ort auswählen</string>
<string name="map_search_provider">Suchanbieter</string>
<string name="missing_permissions">Fehlende Berechtigungen</string>
<string name="location_permission_required_location">Standortberechtigung wird zur Ermittlung des aktuellen Standortes benötigt</string>
<string name="open_map">Karte öffnen</string>

@ -438,7 +438,6 @@
<string name="choose_a_location">Escoger ubicación</string>
<string name="pick_this_location">Seleccionar esta ubicación</string>
<string name="or_choose_a_location">O elija una ubicación</string>
<string name="map_search_provider">Proveedor de búsquedas</string>
<string name="missing_permissions">Permisos ausentes</string>
<string name="location_permission_required_location">Son necesarios permisos de ubicación para encontrar tu ubicación actual</string>
<string name="open_map">Abrir mapa</string>

@ -444,7 +444,6 @@
<string name="choose_a_location">Hautatu kokaleku bat</string>
<string name="pick_this_location">Hautatu kokaleku hau</string>
<string name="or_choose_a_location">Edo hautatu kokaleku bat</string>
<string name="map_search_provider">Bilatu hornitzailea</string>
<string name="missing_permissions">Baimenak falta dira</string>
<string name="location_permission_required_location">Kokapen baimena behar da zure uneko kokalekua aurkitzeko</string>
<string name="open_map">Ireki mapa</string>

@ -441,7 +441,6 @@
<string name="open_map">Avaa kartta</string>
<string name="location_permission_required_location">Sijaintioikeudet tarvitaan nykysijainnin paikannukseen</string>
<string name="missing_permissions">Oikeuksia puuttuu</string>
<string name="map_search_provider">Hauntarjoaja</string>
<string name="or_choose_a_location">Tai valitse muu sijainti</string>
<string name="pick_this_location">Valitse tämä sijainti</string>
<string name="choose_a_location">Valitse sijainti</string>

@ -420,7 +420,6 @@
<string name="choose_a_location">Choisir une localisation</string>
<string name="pick_this_location">Sélectionner cette localisation</string>
<string name="or_choose_a_location">Ou choisir une localisation</string>
<string name="map_search_provider">Moteurs de recherche</string>
<string name="missing_permissions">Permissions manquantes</string>
<string name="location_permission_required_location">La permission de la localisation est nécessaire pour trouver votre localisation actuelle</string>
<string name="open_map">Ouvrir la carte</string>

@ -439,7 +439,6 @@
<string name="choose_a_location">Hely kiválasztása</string>
<string name="pick_this_location">Hely kiválasztása</string>
<string name="or_choose_a_location">Vagy másik hely keresése</string>
<string name="map_search_provider">Keresési szolgáltató</string>
<string name="missing_permissions">Hiányzó jogosultságok</string>
<string name="location_permission_required_location">A jelenlegi pozíció meghatározásához helymeghatározási jogosultság szükséges</string>
<string name="open_map">Térkép megnyitása</string>

@ -368,7 +368,6 @@
<string name="choose_a_location">Pilih sebuah lokasi</string>
<string name="pick_this_location">Pilih lokasi ini</string>
<string name="or_choose_a_location">Atau pilih sebuah lokasi</string>
<string name="map_search_provider">Penyedia pencarian</string>
<string name="location_permission_required_location">Izin akses lokasi diperlukan untuk mencari lokasi Anda saat ini</string>
<string name="open_map">Buka peta</string>
<string name="choose_new_location">Pilih lokasi baru</string>

@ -439,7 +439,6 @@
<string name="choose_a_location">Scegli una posizione</string>
<string name="pick_this_location">Seleziona questa posizione</string>
<string name="or_choose_a_location">O scegli una posizione</string>
<string name="map_search_provider">Seleziona fornitore</string>
<string name="missing_permissions">Permessi mancanti</string>
<string name="location_permission_required_location">Per trovare la tua posizione sono richiesti i permessi di localizzazione</string>
<string name="open_map">Apri mappa</string>

@ -473,7 +473,6 @@
<string name="choose_a_location">בחרו מיקום</string>
<string name="pick_this_location">בחרו את המיקום הזה</string>
<string name="or_choose_a_location">או בחרו מיקום</string>
<string name="map_search_provider">ספק חיפוש</string>
<string name="missing_permissions">חסרות הרשאות</string>
<string name="location_permission_required_location">הרשאות מיקום נדרשות על מנת למצוא את מיקומך הנוכחי</string>
<string name="open_map">פתיחת מפה</string>

@ -436,7 +436,6 @@
<string name="choose_a_location">場所を選択</string>
<string name="pick_this_location">この場所を選択</string>
<string name="or_choose_a_location">または場所を選ぶ</string>
<string name="map_search_provider">プロバイダーを検索</string>
<string name="missing_permissions">アクセス許可がありません</string>
<string name="location_permission_required_location">現在の場所を見つけるには位置のアクセス許可が必要です</string>
<string name="open_map">地図を開く</string>

@ -441,7 +441,6 @@
<string name="choose_a_location">위치 선택</string>
<string name="pick_this_location">현위치 선택</string>
<string name="or_choose_a_location">위치 고르기</string>
<string name="map_search_provider">검색 제공업체</string>
<string name="missing_permissions">권한 누락</string>
<string name="location_permission_required_location">기기의 현재 위치를 확인하려면 위치 권한이 필요합니다</string>
<string name="open_map">지도 열기</string>

@ -435,7 +435,6 @@
<string name="choose_a_location">Pasirinkti vietą</string>
<string name="pick_this_location">Pasirinkti šią vietą</string>
<string name="or_choose_a_location">Arba pasirinkti vietą</string>
<string name="map_search_provider">Paieškos tiekėjas</string>
<string name="missing_permissions">Dingę leidimai</string>
<string name="location_permission_required_location">Vietos leidimai reikalingi surasti jūsų esamą vietą</string>
<string name="open_map">Atidaryti žemėlapį</string>

@ -444,7 +444,6 @@
<string name="choose_a_location">Velg et sted</string>
<string name="pick_this_location">Velg dette stedet</string>
<string name="or_choose_a_location">Eller velg et sted</string>
<string name="map_search_provider">Søketilbyder</string>
<string name="missing_permissions">Manglende tilganger</string>
<string name="location_permission_required_location">Plasseringstilganger trengs for å finne ditt nåværende sted</string>
<string name="open_map">Åpne kart</string>

@ -428,7 +428,6 @@
<string name="choose_a_location">Kies een locatie</string>
<string name="pick_this_location">Selecteer deze locatie</string>
<string name="or_choose_a_location">Of kies een locatie</string>
<string name="map_search_provider">Zoek aanbieder</string>
<string name="missing_permissions">Missende rechten</string>
<string name="location_permission_required_location">Locatie rechten zijn nodig om je huidige locatie te vinden</string>
<string name="open_map">Open kaart</string>

@ -448,7 +448,6 @@
<string name="choose_a_location">Wybierz lokalizację</string>
<string name="pick_this_location">Ustaw wybraną lokalizację</string>
<string name="or_choose_a_location">Lub wybierz lokalizację</string>
<string name="map_search_provider">Silnik wyszukiwania</string>
<string name="missing_permissions">Brakujące uprawnienia</string>
<string name="location_permission_required_location">Uprawnienia lokalizacji są wymagane do ustalenia twojej aktualnej lokalizacji</string>
<string name="open_map">Otwórz mapę</string>

@ -436,7 +436,6 @@
<string name="choose_a_location">Escolher uma localização</string>
<string name="pick_this_location">Selecionar essa localização</string>
<string name="or_choose_a_location">Ou escolha uma localização</string>
<string name="map_search_provider">Provedor de pesquisa</string>
<string name="missing_permissions">Permissões faltando</string>
<string name="location_permission_required_location">Permissão de Localização é necessária para encontrar sua localização atual</string>
<string name="open_map">Abrir mapa</string>

@ -399,7 +399,6 @@
<string name="open_map">Abrir mapa</string>
<string name="location_permission_required_location">Permissão de localização é necessária para encontrar a sua localização atual</string>
<string name="missing_permissions">Permissões faltando</string>
<string name="map_search_provider">Provedor de pesquisa</string>
<string name="or_choose_a_location">Ou escolha uma localização</string>
<string name="pick_this_location">Selecione esta localização</string>
<string name="choose_a_location">Escolher uma localização</string>

@ -455,7 +455,6 @@
<string name="choose_a_location">Выбрать местоположение</string>
<string name="pick_this_location">Выбрать это местоположение</string>
<string name="or_choose_a_location">Или выбрать местоположение</string>
<string name="map_search_provider">Поставщик системы поиска</string>
<string name="missing_permissions">Отсутствующие разрешения</string>
<string name="version_string">Версия %s</string>
<string name="gtasks_GPr_header">Google Tasks</string>

@ -435,7 +435,6 @@
<string name="choose_a_location">Vybrať polohu</string>
<string name="pick_this_location">Vybrať túto polohu</string>
<string name="or_choose_a_location">Alebo vybrať polohu ručne</string>
<string name="map_search_provider">Vyhľadávanie poskytuje</string>
<string name="missing_permissions">Chýbajúce oprávnenia</string>
<string name="location_permission_required_location">Pre učenie súčasnej polohy sú potrebné povolenia</string>
<string name="open_map">Otvoriť mapu</string>

@ -439,7 +439,6 @@
<string name="choose_a_location">Välj en plats</string>
<string name="pick_this_location">Välj den här platsen</string>
<string name="or_choose_a_location">Eller Välj en plats</string>
<string name="map_search_provider">Sökleverantör</string>
<string name="missing_permissions">Saknar behörigheter</string>
<string name="location_permission_required_location">Plats behörigheter behövs för att hitta din aktuella plats</string>
<string name="open_map">Öppna karta</string>

@ -273,7 +273,6 @@
<string name="open_map">வரைபடத்தைத் திறக்கவும்</string>
<string name="location_permission_required_location">உங்கள் தற்போதைய இருப்பிடத்தைக் கண்டுபிடிக்க இருப்பிட அனுமதிகள் தேவை</string>
<string name="missing_permissions">அனுமதிகள் இல்லை</string>
<string name="map_search_provider">தேடல் வழங்குநர்</string>
<string name="or_choose_a_location">அல்லது இருப்பிடத்தைத் தேர்வுசெய்க</string>
<string name="pick_this_location">இந்த இருப்பிடத்தைத் தேர்ந்தெடுக்கவும்</string>
<string name="choose_a_location">இருப்பிடத்தைத் தேர்வுசெய்க</string>

@ -441,7 +441,6 @@
<string name="choose_a_location">Konum seç</string>
<string name="pick_this_location">Bu konumu seç</string>
<string name="or_choose_a_location">Ya da konum seç</string>
<string name="map_search_provider">Arama sağlayıcı</string>
<string name="missing_permissions">Eksik izinler</string>
<string name="location_permission_required_location">Geçerli konumunuzu bulmak için konum izinleri gereklidir</string>
<string name="open_map">Haritayı</string>

@ -457,7 +457,6 @@
<string name="choose_a_location">Обрати місце</string>
<string name="pick_this_location">Обрати це місце</string>
<string name="or_choose_a_location">Або вказати інше місце</string>
<string name="map_search_provider">Знайти провайдера</string>
<string name="missing_permissions">Відсутні дозволи</string>
<string name="location_permission_required_location">Щоб визначити ваше місцезнаходження необхідний дозвіл на перегляд місцезнаходження </string>
<string name="open_map">Відкрити карту</string>

@ -434,7 +434,6 @@
<string name="choose_a_location">选择一个位置</string>
<string name="pick_this_location">选中这个位置</string>
<string name="or_choose_a_location">或选择一个位置</string>
<string name="map_search_provider">搜索引擎供应商</string>
<string name="missing_permissions">缺少权限</string>
<string name="location_permission_required_location">需要位置权限来发现您当前位置</string>
<string name="open_map">打开地图</string>

@ -444,7 +444,6 @@
<string name="open_map">開啟地圖</string>
<string name="location_permission_required_location">需要位置權限以找到您現在的位置</string>
<string name="missing_permissions">缺少權限</string>
<string name="map_search_provider">搜尋提供者</string>
<string name="or_choose_a_location">或選擇一個地點</string>
<string name="pick_this_location">選擇此地點</string>
<string name="choose_a_location">選擇一個地點</string>

@ -292,16 +292,6 @@
<item>2</item>
</string-array>
<string-array name="map_search_provider_names">
<item>@string/mapbox</item>
<item>@string/map_search_google_places</item>
</string-array>
<string-array name="map_search_provider_values">
<item>0</item>
<item>1</item>
</string-array>
<string-array name="geofence_service_names">
<item>@string/google_play_location_service</item>
<item>@string/android_location_services</item>

@ -396,12 +396,10 @@
<string name="p_linkify_task_list">linkify_task_list</string>
<string name="p_linkify_task_edit">linkify_task_edit</string>
<string name="mapbox">Mapbox</string>
<string name="map_search_google_places">Google Places</string>
<string name="nominatim">Nominatim</string>
<string name="google_maps">Google Maps</string>
<string name="openstreetmap">OpenStreetMap</string>
<string name="android">Android</string>
<string name="p_place_provider">place_provider_v2</string>
<string name="p_location_based_reminders">location_based_reminders</string>
<string name="p_geofence_service">geofence_service</string>
<string name="preference_screen">preference_screen</string>

@ -523,7 +523,6 @@ File %1$s contained %2$s.\n\n
<string name="choose_a_location">Choose a location</string>
<string name="pick_this_location">Select this location</string>
<string name="or_choose_a_location">Or choose a location</string>
<string name="map_search_provider">Search provider</string>
<string name="missing_permissions">Missing permissions</string>
<string name="background_location_permission_required">This app collects location data to enable location-based reminders even when the app is closed or not in use.</string>
<string name="location_permission_required_location">Location permissions are needed to find your current location</string>

@ -54,14 +54,6 @@
android:entryValues="@array/reverse_geocoder_values"
android:summary="%s"/>
<ListPreference
android:defaultValue="0"
android:key="@string/p_place_provider"
android:title="@string/map_search_provider"
android:entries="@array/map_search_provider_names"
android:entryValues="@array/map_search_provider_values"
android:summary="%s" />
</PreferenceCategory>
</PreferenceScreen>

@ -3,4 +3,5 @@
<string name="app_name">Tasks</string>
<string name="tasks_caldav_url">https://caldav.tasks.org</string>
<string name="tasks_nominatim_url">https://nominatim.tasks.org</string>
<string name="tasks_places_url">https://places.tasks.org</string>
</resources>

@ -0,0 +1,49 @@
package org.tasks.location
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.TestUtilities.readFile
import org.tasks.location.PlaceSearchGoogle.Companion.toJson
import org.tasks.location.PlaceSearchGoogle.Companion.toPlace
import org.tasks.location.PlaceSearchGoogle.Companion.toSearchResults
class PlaceSearchGoogleTest {
@Test
fun placeSearchWithMultipleResults() {
val results = toSearchResults(readFile("google_places/search.json").toJson())
assertEquals(
listOf(
"ChIJfQQVCCMzjoARce0POzONI8I",
"ChIJQXqgXsiAj4ARqtl6U4GD-Cw",
"ChIJCWNdVNgr3YAR4pLlOt8CfEk",
"ChIJhTEH6lev3IARDMKC_pGF6nI"
),
results.map { it.id }
)
}
@Test
fun validatePlace() {
val result = toSearchResults(readFile("google_places/search.json").toJson())[2]
assertEquals("ChIJCWNdVNgr3YAR4pLlOt8CfEk", result.id)
assertEquals("Portillo's Hot Dogs", result.name)
assertEquals("La Palma Avenue, Buena Park, CA, USA", result.address)
}
@Test
fun fetchPlace() {
val result = toPlace(readFile("google_places/fetch.json").toJson())
assertEquals("Magic Kingdom Park", result.name)
assertEquals("1180 Seven Seas Drive, Lake Buena Vista, FL 32836, USA", result.address)
assertEquals(28.417663, result.latitude, 0.0)
assertEquals(-81.581212, result.longitude, 0.0)
assertEquals("+1 407-939-5277", result.phone)
assertEquals(
"https://disneyworld.disney.go.com/destinations/magic-kingdom/?CMP=OKC-80007944_GM_WDW_destination_magickingdompark_NA",
result.url
)
}
}

@ -0,0 +1,17 @@
{
"html_attributions": [],
"result": {
"formatted_address": "1180 Seven Seas Drive, Lake Buena Vista, FL 32836, USA",
"geometry": {
"location": {
"lat": 28.417663,
"lng": -81.581212
}
},
"international_phone_number": "+1 407-939-5277",
"name": "Magic Kingdom Park",
"place_id": "ChIJgUulalN-3YgRGoTaWM2LawY",
"website": "https://disneyworld.disney.go.com/destinations/magic-kingdom/?CMP=OKC-80007944_GM_WDW_destination_magickingdompark_NA"
},
"status": "OK"
}

@ -0,0 +1,225 @@
{
"predictions": [
{
"description": "portillo's hot dogs",
"matched_substrings": [
{
"length": 19,
"offset": 0
}
],
"structured_formatting": {
"main_text": "portillo's hot dogs",
"main_text_matched_substrings": [
{
"length": 19,
"offset": 0
}
]
},
"terms": [
{
"offset": 0,
"value": "portillo's hot dogs"
}
]
},
{
"description": "Portillo Diesel, Leo Avenue, San Jose, CA, USA",
"matched_substrings": [
{
"length": 15,
"offset": 0
}
],
"place_id": "ChIJfQQVCCMzjoARce0POzONI8I",
"reference": "ChIJfQQVCCMzjoARce0POzONI8I",
"structured_formatting": {
"main_text": "Portillo Diesel",
"main_text_matched_substrings": [
{
"length": 15,
"offset": 0
}
],
"secondary_text": "Leo Avenue, San Jose, CA, USA"
},
"terms": [
{
"offset": 0,
"value": "Portillo Diesel"
},
{
"offset": 17,
"value": "Leo Avenue"
},
{
"offset": 29,
"value": "San Jose"
},
{
"offset": 39,
"value": "CA"
},
{
"offset": 43,
"value": "USA"
}
],
"types": [
"car_repair",
"point_of_interest",
"establishment"
]
},
{
"description": "Portillo's Trucking, Franklin Street, Oakland, CA, USA",
"matched_substrings": [
{
"length": 19,
"offset": 0
}
],
"place_id": "ChIJQXqgXsiAj4ARqtl6U4GD-Cw",
"reference": "ChIJQXqgXsiAj4ARqtl6U4GD-Cw",
"structured_formatting": {
"main_text": "Portillo's Trucking",
"main_text_matched_substrings": [
{
"length": 19,
"offset": 0
}
],
"secondary_text": "Franklin Street, Oakland, CA, USA"
},
"terms": [
{
"offset": 0,
"value": "Portillo's Trucking"
},
{
"offset": 21,
"value": "Franklin Street"
},
{
"offset": 38,
"value": "Oakland"
},
{
"offset": 47,
"value": "CA"
},
{
"offset": 51,
"value": "USA"
}
],
"types": [
"moving_company",
"point_of_interest",
"establishment"
]
},
{
"description": "Portillo's Hot Dogs, La Palma Avenue, Buena Park, CA, USA",
"matched_substrings": [
{
"length": 19,
"offset": 0
}
],
"place_id": "ChIJCWNdVNgr3YAR4pLlOt8CfEk",
"reference": "ChIJCWNdVNgr3YAR4pLlOt8CfEk",
"structured_formatting": {
"main_text": "Portillo's Hot Dogs",
"main_text_matched_substrings": [
{
"length": 19,
"offset": 0
}
],
"secondary_text": "La Palma Avenue, Buena Park, CA, USA"
},
"terms": [
{
"offset": 0,
"value": "Portillo's Hot Dogs"
},
{
"offset": 21,
"value": "La Palma Avenue"
},
{
"offset": 38,
"value": "Buena Park"
},
{
"offset": 50,
"value": "CA"
},
{
"offset": 54,
"value": "USA"
}
],
"types": [
"meal_takeaway",
"restaurant",
"food",
"point_of_interest",
"establishment"
]
},
{
"description": "Portillo's Hot Dogs, Day Street, Moreno Valley, CA, USA",
"matched_substrings": [
{
"length": 19,
"offset": 0
}
],
"place_id": "ChIJhTEH6lev3IARDMKC_pGF6nI",
"reference": "ChIJhTEH6lev3IARDMKC_pGF6nI",
"structured_formatting": {
"main_text": "Portillo's Hot Dogs",
"main_text_matched_substrings": [
{
"length": 19,
"offset": 0
}
],
"secondary_text": "Day Street, Moreno Valley, CA, USA"
},
"terms": [
{
"offset": 0,
"value": "Portillo's Hot Dogs"
},
{
"offset": 21,
"value": "Day Street"
},
{
"offset": 33,
"value": "Moreno Valley"
},
{
"offset": 48,
"value": "CA"
},
{
"offset": 52,
"value": "USA"
}
],
"types": [
"meal_takeaway",
"restaurant",
"food",
"point_of_interest",
"establishment"
]
}
],
"status": "OK"
}

@ -186,8 +186,32 @@
+| +--- androidx.fragment:fragment:1.0.0 -> 1.2.5 (*)
+| +--- com.google.android.gms:play-services-base:17.0.0 -> 17.3.0 (*)
+| \--- com.google.android.gms:play-services-basement:17.0.0 -> 17.3.0 (*)
++--- com.google.android.libraries.places:places:2.4.0
+| +--- androidx.appcompat:appcompat:1.0.0 -> 1.2.0
++--- com.android.billingclient:billing:1.2.2
++--- com.gitlab.bitfireAT:dav4jvm:2.1.1
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.30 (*)
+| \--- org.apache.commons:commons-lang3:3.9
++--- com.gitlab.abaker:ical4android:0e928b567c
+| +--- org.mnode.ical4j:ical4j:3.0.21
+| | +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.30
+| | +--- commons-codec:commons-codec:1.11
+| | +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9
+| | \--- org.apache.commons:commons-collections4:4.1
+| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 (*)
+| +--- com.sun.mail:android-mail:1.6.5
+| | \--- com.sun.mail:android-activation:1.6.5
+| +--- commons-io:commons-io:2.6
+| +--- org.slf4j:slf4j-jdk14:1.7.30
+| | \--- org.slf4j:slf4j-api:1.7.30
+| \--- androidx.core:core-ktx:1.3.2
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.71 -> 1.4.30 (*)
+| +--- androidx.annotation:annotation:1.1.0
+| \--- androidx.core:core:1.3.2 (*)
++--- com.gitlab.bitfireAT:cert4android:26a91a729f
+| +--- androidx.databinding:databinding-common:4.1.1 -> 4.1.2
+| +--- androidx.databinding:databinding-runtime:4.1.1 -> 4.1.2 (*)
+| +--- androidx.databinding:databinding-adapters:4.1.1 -> 4.1.2 (*)
+| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 (*)
+| +--- androidx.appcompat:appcompat:1.2.0
+| | +--- androidx.annotation:annotation:1.1.0
+| | +--- androidx.core:core:1.3.0 -> 1.3.2 (*)
+| | +--- androidx.cursoradapter:cursoradapter:1.0.0
@ -213,8 +237,7 @@
+| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
+| +--- androidx.cardview:cardview:1.0.0
+| | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+| +--- androidx.fragment:fragment:1.1.0 -> 1.2.5 (*)
+| +--- androidx.lifecycle:lifecycle-extensions:2.1.0 -> 2.2.0
+| +--- androidx.lifecycle:lifecycle-extensions:2.2.0
+| | +--- androidx.lifecycle:lifecycle-runtime:2.2.0 (*)
+| | +--- androidx.arch.core:core-common:2.1.0 (*)
+| | +--- androidx.arch.core:core-runtime:2.1.0 (*)
@ -226,50 +249,6 @@
+| | +--- androidx.lifecycle:lifecycle-service:2.2.0
+| | | \--- androidx.lifecycle:lifecycle-runtime:2.2.0 (*)
+| | \--- androidx.lifecycle:lifecycle-viewmodel:2.2.0 (*)
+| +--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0
+| | +--- androidx.annotation:annotation:1.1.0
+| | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
+| | +--- androidx.customview:customview:1.0.0 (*)
+| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
+| +--- com.android.volley:volley:1.1.1
+| +--- com.google.android.datatransport:transport-api:2.2.0 -> 2.2.1 (*)
+| +--- com.google.android.datatransport:transport-backend-cct:2.3.0 -> 2.3.3 (*)
+| +--- com.google.android.datatransport:transport-runtime:2.2.3 -> 2.2.5 (*)
+| +--- com.google.android.gms:play-services-base:17.2.1 -> 17.3.0 (*)
+| +--- com.google.android.gms:play-services-basement:17.0.0 -> 17.3.0 (*)
+| +--- com.google.android.gms:play-services-location:17.0.0 -> 17.1.0 (*)
+| +--- com.google.android.gms:play-services-maps:17.0.0 (*)
+| +--- com.google.android.gms:play-services-tasks:17.0.0 -> 17.1.0 (*)
+| +--- com.google.auto.value:auto-value-annotations:1.6.2 -> 1.7.4
+| \--- com.google.code.gson:gson:2.8.5 -> 2.8.6
++--- com.android.billingclient:billing:1.2.2
++--- com.gitlab.bitfireAT:dav4jvm:2.1.1
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.30 (*)
+| \--- org.apache.commons:commons-lang3:3.9
++--- com.gitlab.abaker:ical4android:0e928b567c
+| +--- org.mnode.ical4j:ical4j:3.0.21
+| | +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.30
+| | +--- commons-codec:commons-codec:1.11
+| | +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9
+| | \--- org.apache.commons:commons-collections4:4.1
+| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 (*)
+| +--- com.sun.mail:android-mail:1.6.5
+| | \--- com.sun.mail:android-activation:1.6.5
+| +--- commons-io:commons-io:2.6
+| +--- org.slf4j:slf4j-jdk14:1.7.30
+| | \--- org.slf4j:slf4j-api:1.7.30
+| \--- androidx.core:core-ktx:1.3.2
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.71 -> 1.4.30 (*)
+| +--- androidx.annotation:annotation:1.1.0
+| \--- androidx.core:core:1.3.2 (*)
++--- com.gitlab.bitfireAT:cert4android:26a91a729f
+| +--- androidx.databinding:databinding-common:4.1.1 -> 4.1.2
+| +--- androidx.databinding:databinding-runtime:4.1.1 -> 4.1.2 (*)
+| +--- androidx.databinding:databinding-adapters:4.1.1 -> 4.1.2 (*)
+| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 (*)
+| +--- androidx.appcompat:appcompat:1.2.0 (*)
+| +--- androidx.cardview:cardview:1.0.0 (*)
+| +--- androidx.lifecycle:lifecycle-extensions:2.2.0 (*)
+| +--- androidx.lifecycle:lifecycle-livedata-ktx:2.2.0
+| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50 -> 1.4.30 (*)
+| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0 -> 1.4.1
@ -307,7 +286,11 @@
+| | +--- androidx.annotation:annotation-experimental:1.0.0
+| | +--- androidx.fragment:fragment:1.0.0 -> 1.2.5 (*)
+| | +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.2.0 (*)
+| | +--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0 (*)
+| | +--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0
+| | | +--- androidx.annotation:annotation:1.1.0
+| | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
+| | | +--- androidx.customview:customview:1.0.0 (*)
+| | | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
+| | +--- androidx.transition:transition:1.2.0
+| | | +--- androidx.annotation:annotation:1.1.0
+| | | +--- androidx.core:core:1.0.1 -> 1.3.2 (*)

Loading…
Cancel
Save