Compare commits

...

15 Commits
1.6.0 ... 1.6.3

Author SHA1 Message Date
johan12345
1be519b1ee Release 1.6.3 2023-06-14 22:02:07 +02:00
Hosted Weblate
01737f21d2 Translated using Weblate (Portuguese)
Currently translated at 100.0% (313 of 313 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-06-14 21:07:41 +02:00
johan12345
17ce9f420b Tesla: CongestionPriceHistogram is nullable 2023-06-13 22:57:10 +02:00
johan12345
6eb90498eb GoingElectric: fix SQL implementation of network/barrierFree/chargeCards filters 2023-06-13 20:37:30 +02:00
johan12345
074e0bf904 Release 1.6.2 2023-06-12 08:35:02 +02:00
johan12345
41ac223e97 properly escape strings in SQL queries 2023-06-12 08:33:33 +02:00
Hosted Weblate
f7196bcce0 Translated using Weblate (Portuguese)
Currently translated at 100.0% (313 of 313 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-06-11 21:56:23 +02:00
johan12345
4f6092e5dc remove extra spatialite library 2023-06-11 21:20:38 +02:00
johan12345
dfd42e1ffd upgrade spatia-room 2023-06-11 21:17:31 +02:00
johan12345
895b24d406 Release 1.6.1 2023-06-11 20:16:26 +02:00
johan12345
3dea7993f3 clear cache with next update 2023-06-11 19:54:36 +02:00
johan12345
ca90f1b37f GoingElectricApi: infer some details based on applied filters 2023-06-11 19:34:19 +02:00
johan12345
fe0843e653 fix bug in caching algorithm that caused chargers to disappear
some filters require details that we do not get in normal queries
2023-06-11 19:19:37 +02:00
johan12345
0f42ae84de fix NPE 2023-06-11 19:17:53 +02:00
johan12345
2748b0a3db make faultReport: true result in non-null value 2023-06-11 19:05:33 +02:00
18 changed files with 171 additions and 66 deletions

View File

@@ -21,8 +21,8 @@ android {
minSdkVersion 21
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 180
versionName "1.6.0"
versionCode 186
versionName "1.6.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(',')
@@ -246,10 +246,7 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation('com.github.anboralabs:spatia-room:0.2.6') {
exclude group: 'com.github.dalgarins', module: 'android-spatialite'
}
implementation 'com.github.ev-map:android-spatialite:654dca2365'
implementation 'com.github.anboralabs:spatia-room:0.2.7'
// billing library
def billing_version = "6.0.0"

View File

@@ -49,6 +49,8 @@ interface ChargepointApi<out T : ReferenceData> {
fun convertFiltersToSQL(filters: FilterValues, referenceData: ReferenceData): FiltersSQLQuery
fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean
val name: String
val id: String

View File

@@ -281,7 +281,7 @@ interface TeslaGraphQlApi {
val siteDynamic: SiteDynamic,
val siteStatic: SiteStatic,
val pricing: Pricing,
val congestionPriceHistogram: CongestionPriceHistogram,
val congestionPriceHistogram: CongestionPriceHistogram?,
)
@JsonClass(generateAdapter = true)
@@ -574,12 +574,13 @@ class TeslaAvailabilityDetector(
i += connector.count
}
val indexOfMidnight =
details.congestionPriceHistogram.dataAttributes.indexOfFirst { it.label == "12AM" }
val congestionHistogram = indexOfMidnight.takeIf { it >= 0 }?.let { index ->
val data = details.congestionPriceHistogram.data.toMutableList()
Collections.rotate(data, -index)
data
val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
val data = cph.data.toMutableList()
Collections.rotate(data, -index)
data
}
}
return ChargeLocationStatus(

View File

@@ -96,7 +96,7 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
false -> null // Response was false
else -> {
if (this.clazz == GEFaultReport::class.java) {
GEFaultReport(null, null) as T
GEFaultReport(null, "") as T
} else {
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
}

View File

@@ -1,6 +1,7 @@
package net.vonforst.evmap.api.goingelectric
import android.content.Context
import android.database.DatabaseUtils
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.squareup.moshi.Moshi
@@ -217,7 +218,7 @@ class GoingElectricApiWrapper(
}
} while (startkey != null && startkey < 10000)
val result = postprocessResult(data, minPower, connectorsVal, minConnectors)
val result = postprocessResult(data, filters)
return Resource.success(ChargepointList(result, startkey == null))
}
@@ -308,18 +309,26 @@ class GoingElectricApiWrapper(
}
} while (startkey != null && startkey < 10000)
val result = postprocessResult(data, minPower, connectorsVal, minConnectors)
val result = postprocessResult(data, filters)
return Resource.success(ChargepointList(result, startkey == null))
}
private fun postprocessResult(
chargers: List<GEChargepointListItem>,
minPower: Int?,
connectorsVal: MultipleChoiceFilterValue?,
minConnectors: Int?
filters: FilterValues?
): List<ChargepointListItem> {
// apply filters which GoingElectric does not support natively
val minPower = filters?.getSliderValue("min_power")
val minConnectors = filters?.getSliderValue("min_connectors")
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
val freecharging = filters?.getBooleanValue("freecharging")
val freeparking = filters?.getBooleanValue("freeparking")
val open247 = filters?.getBooleanValue("open_247")
val barrierfree = filters?.getBooleanValue("barrierfree")
val networks = filters?.getMultipleChoiceValue("networks")
val chargecards = filters?.getMultipleChoiceValue("chargecards")
return chargers.filter { it ->
// apply filters which GoingElectric does not support natively
if (it is GEChargeLocation) {
it.chargepoints
.filter { it.power >= (minPower ?: 0) }
@@ -328,6 +337,40 @@ class GoingElectricApiWrapper(
} else {
true
}
}.map {
// infer some properties based on applied filters
if (it is GEChargeLocation) {
var inferred = it
if (freecharging == true) {
inferred = inferred.copy(
cost = inferred.cost?.copy(freecharging = true)
?: GECost(freecharging = true)
)
}
if (freeparking == true) {
inferred = inferred.copy(
cost = inferred.cost?.copy(freeparking = true) ?: GECost(freeparking = true)
)
}
if (open247 == true) {
inferred = inferred.copy(
openinghours = inferred.openinghours?.copy(twentyfourSeven = true)
?: GEOpeningHours(twentyfourSeven = true)
)
}
if (barrierfree == true
&& (networks == null || networks.all || it.network !in networks.values)
&& (chargecards == null || chargecards.all)
) {
/* barrierfree, networks and chargecards are combined with OR - so we can only
* be sure that the charger is barrierFree if the other filters are not active
* or the charger does not match the other filters */
inferred = inferred.copy(barrierFree = true)
}
inferred
} else {
it
}
}.map { it.convert(apikey, false) }
}
@@ -487,9 +530,6 @@ class GoingElectricApiWrapper(
if (filters.getBooleanValue("open_247") == true) {
result.append(" AND twentyfourSeven IS 1")
}
if (filters.getBooleanValue("barrierfree") == true) {
result.append(" AND barrierFree IS 1")
}
if (filters.getBooleanValue("exclude_faults") == true) {
result.append(" AND fault_report_description IS NULL AND fault_report_created IS NULL")
}
@@ -505,31 +545,46 @@ class GoingElectricApiWrapper(
val connectorsList = if (connectors.values.size == 0) {
""
} else {
"'" + connectors.values.joinToString("', '") { GEChargepoint.convertTypeFromGE(it) } + "'"
connectors.values.joinToString(",") {
DatabaseUtils.sqlEscapeString(
GEChargepoint.convertTypeFromGE(
it
)
)
}
}
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
requiresChargepointQuery = true
}
// networks, chargecards and barrierFree filters are combined with OR in the GE API
val networks = filters.getMultipleChoiceValue("networks")
if (networks != null && !networks.all) {
val networksList = if (networks.values.size == 0) {
""
} else {
"'" + networks.values.joinToString("', '") + "'"
}
result.append(" AND network IN (${networksList})")
}
val chargecards = filters.getMultipleChoiceValue("chargecards")
if (chargecards != null && !chargecards.all) {
val chargecardsList = if (chargecards.values.size == 0) {
""
} else {
chargecards.values.joinToString(",")
val barrierFree = filters.getBooleanValue("barrierfree")
if ((networks != null && !networks.all) || barrierFree == true || (chargecards != null && !chargecards.all)) {
val queries = mutableListOf<String>()
if (networks != null && !networks.all) {
val networksList = if (networks.values.size == 0) {
""
} else {
networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) }
}
queries.add("network IN (${networksList})")
}
result.append(" AND json_extract(cc.value, '$.id') IN (${chargecardsList})")
requiresChargeCardQuery = true
if (barrierFree == true) {
queries.add("barrierFree IS 1")
}
if (chargecards != null && !chargecards.all) {
val chargecardsList = if (chargecards.values.size == 0) {
""
} else {
chargecards.values.joinToString(",")
}
queries.add("json_extract(cc.value, '$.id') IN (${chargecardsList})")
requiresChargeCardQuery = true
}
result.append(" AND (${queries.joinToString(" OR ")})")
}
val categories = filters.getMultipleChoiceValue("categories")
@@ -546,5 +601,14 @@ class GoingElectricApiWrapper(
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, requiresChargeCardQuery)
}
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
val chargecards = filters.getMultipleChoiceValue("chargecards")
return filters.getBooleanValue("freecharging") == true
|| filters.getBooleanValue("freeparking") == true
|| filters.getBooleanValue("open_247") == true
|| filters.getBooleanValue("barrierfree") == true
|| (chargecards != null && !chargecards.all)
}
}

View File

@@ -86,10 +86,10 @@ data class GEChargeLocation(
@JsonClass(generateAdapter = true)
data class GECost(
val freecharging: Boolean,
val freeparking: Boolean,
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
val freecharging: Boolean = false,
val freeparking: Boolean = false,
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String? = null,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String? = null
) {
fun convert() = Cost(
// In GE, freecharging = false can either mean "paid charging" or "no information
@@ -104,8 +104,8 @@ data class GECost(
@JsonClass(generateAdapter = true)
data class GEOpeningHours(
@Json(name = "24/7") val twentyfourSeven: Boolean,
@JsonObjectOrFalse val description: String?,
val days: GEOpeningHoursDays?
@JsonObjectOrFalse val description: String? = null,
val days: GEOpeningHoursDays? = null
) {
fun convert() = OpeningHours(twentyfourSeven, description, days?.convert())
}

View File

@@ -1,6 +1,7 @@
package net.vonforst.evmap.api.openchargemap
import android.content.Context
import android.database.DatabaseUtils
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.squareup.moshi.Moshi
@@ -347,12 +348,14 @@ class OpenChargeMapApiWrapper(
val connectorsList = if (connectors.values.size == 0) {
""
} else {
"'" + connectors.values.joinToString("', '") {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
refData
connectors.values.joinToString(",") {
DatabaseUtils.sqlEscapeString(
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
refData
)
)
} + "'"
}
}
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
requiresChargepointQuery = true
@@ -363,9 +366,9 @@ class OpenChargeMapApiWrapper(
val networksList = if (operators.values.size == 0) {
""
} else {
"'" + operators.values.joinToString("', '") { opId ->
refData.operators.find { it.id == opId.toLong() }?.title.orEmpty()
} + "'"
operators.values.joinToString(",") { opId ->
DatabaseUtils.sqlEscapeString(refData.operators.find { it.id == opId.toLong() }?.title.orEmpty())
}
}
result.append(" AND network IN (${networksList})")
}
@@ -379,4 +382,10 @@ class OpenChargeMapApiWrapper(
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false)
}
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
val operators = filters.getMultipleChoiceValue("operators")
return (operators != null && !operators.all)
// TODO: it would be possible to implement this without requiring details if we extended the data structure to also save the operator ID in the DB
}
}

View File

@@ -101,7 +101,7 @@ data class OCMChargepoint(
connections.first { it.statusType != null && it.statusTypeId in faultStatuses }.statusType!!.title
)
}
return FaultReport(null, null)
return FaultReport(null, "")
} else {
return null
}

View File

@@ -161,6 +161,7 @@ class ChargeLocationsRepository(
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
val requiresDetail = filters?.let { api.filteringInSQLRequiresDetails(it) } ?: false
val savedRegionResult = savedRegionDao.savedRegionCovers(
bounds.southwest.latitude,
bounds.northeast.latitude,
@@ -168,7 +169,8 @@ class ChargeLocationsRepository(
bounds.northeast.longitude,
api.id,
cacheSoftLimitDate(api),
filtersSerialized
filtersSerialized,
requiresDetail
)
val useClustering = shouldUseServerSideClustering(zoom)
val apiResult = liveData {
@@ -224,13 +226,15 @@ class ChargeLocationsRepository(
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
val requiresDetail = filters?.let { api.filteringInSQLRequiresDetails(it) } ?: false
val savedRegionResult = savedRegionDao.savedRegionCoversRadius(
location.latitude,
location.longitude,
radiusMeters * 0.999, // to account for float rounding errors
api.id,
cacheSoftLimitDate(api),
filtersSerialized
filtersSerialized,
requiresDetail
)
val useClustering = shouldUseServerSideClustering(zoom)
val apiResult = liveData {

View File

@@ -4,22 +4,20 @@ import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.lifecycle.map
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import co.anbora.labs.spatia.builder.SpatiaBuilder
import co.anbora.labs.spatia.builder.SpatiaRoom
import co.anbora.labs.spatia.geometry.GeometryConverters
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.api.goingelectric.GEChargeCard
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
import net.vonforst.evmap.api.openchargemap.OCMCountry
import net.vonforst.evmap.api.openchargemap.OCMOperator
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.await
@Database(
entities = [
@@ -37,7 +35,7 @@ import net.vonforst.evmap.viewmodel.await
OCMCountry::class,
OCMOperator::class,
SavedRegion::class
], version = 20
], version = 21
)
@TypeConverters(Converters::class, GeometryConverters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -77,7 +75,7 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -447,6 +445,13 @@ abstract class AppDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_21 = object : Migration(20, 21) {
override fun migrate(db: SupportSQLiteDatabase) {
// clear cache with this update
db.execSQL("DELETE FROM savedregion")
}
}
}
/**

View File

@@ -548,7 +548,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val apiId = apiId.value
when (apiId) {
"goingelectric" -> {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
val chargeCardsVal =
filters.getMultipleChoiceValue("chargecards") ?: return@addSource
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()

View File

@@ -58,7 +58,7 @@
<string name="fault_report_date">Com problemas (atualizado: %s)</string>
<string name="filter_chargecards">Formas de pagamento</string>
<string name="pref_language">Língua da app</string>
<string name="all_selected">Todos selecionados</string>
<string name="all_selected">Todas selecionadas</string>
<string name="edit">editar</string>
<string name="pref_darkmode">Modo escuro</string>
<string name="connection_error">Não foi possível carregar a lista de carregadores</string>
@@ -97,7 +97,7 @@
<string name="save_as_profile">Guardar como perfil</string>
<string name="filterprofiles_empty_state">Não existem filtros guardados</string>
<string name="welcome_2">Cada cor corresponde a potência máxima do carregador</string>
<string name="welcome_to_evmap">Bem-vindo ao EVMap</string>
<string name="welcome_to_evmap">Bem-vindo(a) ao EVMap</string>
<string name="pref_darkmode_always_off">Sempre desligado</string>
<string name="welcome_2_title">Escolha a potência</string>
<string name="navigate">Navegar</string>
@@ -302,7 +302,7 @@
<string name="charger_website">Website</string>
<string name="realtime_data_login_needed">Conta Tesla necessária para informação em tempo real</string>
<string name="charge_price_minute_format">%2$s%1$.2f/min</string>
<string name="pref_tesla_account_disabled">Faça o login para ver os dados em tempo real dos Superchargers da Tesla. Não é necessário possuir um veículo da Tesla</string>
<string name="pref_tesla_account_disabled">Faça o login para ver informação em tempo real sobre os Tesla Superchargers. Não é necessário possuir um veículo Tesla</string>
<string name="login">Login</string>
<string name="login_error">Falha no login</string>
<string name="pricing_up_to">até %s</string>
@@ -324,4 +324,10 @@
<string name="pref_map_scale_meters">metros</string>
<string name="pref_map_scale_miles">milhas</string>
<string name="pref_map_scale">Barra de escala do mapa</string>
<string name="data_retrieved_at">Informação atualizada %s</string>
<string name="settings_cache_count">Tamanho da cache</string>
<string name="settings_cache_clear">Limpar cache</string>
<string name="settings_cache_count_summary">%d carregadores na base de dados, %.1f MB</string>
<string name="settings_caching">Caching (base de dados local)</string>
<string name="settings_cache_clear_summary">Elimina todos os carregadores guardados na base de dados local, com a exceção dos seus favoritos</string>
</resources>

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Abstürze behoben
- Fehler im Caching-Algorithmus im Zusammenspiel mit bestimmten Filtern behoben

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Abstürze im Zusammenspiel mit bestimmten Filtern behoben

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Fehler im Caching-Algorithmus im Zusammenspiel mit bestimmten Filtern behoben
- Abstürze behoben

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed crashes
- Fixed error in caching algorithm when some filters are active

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed crashes when some filters are active

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed error in caching algorithm when some filters are active
- Fixed crashes