Compare commits

..

19 Commits

Author SHA1 Message Date
Johan von Forstner
60d54c989b update version code 2022-09-27 23:13:31 +02:00
Johan von Forstner
c0555e7965 Android Auto: show loading error as list text instead of toast 2022-09-27 23:11:18 +02:00
Johan von Forstner
49c2fb3494 Android Auto location fixes 2022-09-27 23:04:43 +02:00
Johan von Forstner
c1ec46917e add app_logo.svg 2022-09-27 19:33:33 +02:00
Johan von Forstner
ac11cddd42 Release 1.3.13 2022-09-27 19:02:05 +02:00
Johan von Forstner
6267e897d4 observe apiId liveData to fix bug 2022-09-27 18:59:38 +02:00
Johan von Forstner
8a0224707b Release 1.3.12 2022-09-26 16:49:16 +02:00
johan12345
09ded65b4e upgrade car app library to 1.3.0-beta01 2022-09-25 20:10:53 +02:00
johan12345
c8f333ce89 try to use plug icons in detail view
#199, #198
2022-09-25 20:10:53 +02:00
johan12345
4989aedd8b use CarIconSpan in row texts
fixes #199
2022-09-25 20:10:53 +02:00
johan12345
4beb1f92ad check car app API level before setting OnContentRefreshListener 2022-09-25 20:10:52 +02:00
johan12345
f24b7d1c2c Android Auto: move search button from filter screen back to map
This reverts commit 6b6c7da081.
2022-09-25 20:10:52 +02:00
johan12345
094a26980f Upgrade Car App library
This reverts commit 2e8cdb01fd.
2022-09-25 20:10:52 +02:00
Johan von Forstner
098f815ac9 fix german string 2022-09-25 20:10:40 +02:00
johan12345
6c51693b8d rebuild availability loading with switchMap 2022-09-21 20:15:47 +02:00
johan12345
a360c71ecb Upgrade dependencies 2022-09-20 19:54:51 +02:00
johan12345
b9da72e449 add supported countries for Chargeprice
fixes #234
2022-09-20 19:24:33 +02:00
johan12345
b0aeb8af98 Fix crash when changing data source
fixes #232
2022-09-20 19:15:45 +02:00
johan12345
1867d1bf7a Update source button text when API changes
fixes #233
2022-09-18 19:54:19 +02:00
27 changed files with 400 additions and 154 deletions

65
_img/app_logo.svg Normal file
View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 663.6 219.8" style="enable-background:new 0 0 663.6 219.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFB300;}
.st1{fill:#90A4AE;}
.st2{fill:#546E7A;}
.st3{fill:#00E676;}
.st4{fill:#FFFFFF;fill-opacity:0.2;}
.st5{fill:#3E2723;fill-opacity:0.2;}
.st6{opacity:0.45;enable-background:new ;}
.st7{enable-background:new ;}
.st8{fill:#1D1D1B;}
</style>
<g id="Ebene_2_1_">
<g>
<g>
<g>
<path class="st0"
d="M19.4,161.7l-4-35.1l-6.1,0.6l4,35.1L19.4,161.7z M41.2,159.1l-4-35.1l-6.1,0.6l4,35.1L41.2,159.1z" />
<path class="st1" d="M52.6,206.9c-1.9,2.3-3.4,3.8-3.6,4c-5.5,4.4-9.9,5.7-13.5,4c-6.3-3.2-5.9-15-5.7-16.3l4.4,0.2
c-0.2,3.4,0.4,10.6,3.4,12c1.7,0.8,4.6-0.2,8.5-3.4l0,0c0,0,12.3-12.3,9.7-22c-3-11.6,10.6-28.3,15-34l0.6-0.6l3.6,2.7l-0.6,0.8
c-13.7,16.9-15.2,25.6-14.2,30C62.3,192.9,56.6,202,52.6,206.9z" />
<path class="st1"
d="M5.9,161.2l1.7,14.4l13.3,8.9l18-1.9l11-11.6l-1.7-14.4L5.9,161.2z" />
<g>
<path class="st2" d="M38.6,182.6l-18,1.9l3.8,15.8l14.2-1.7V182.6L38.6,182.6z M51.5,144.5l1.5,13.1l-51.5,5.9L0,150.4
L51.5,144.5z" />
</g>
</g>
<g>
<g>
<path class="st3" d="M91.9,0c-37,0-67,30-67,67c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6
c6.8-72,63.2-98.4,63.2-148.9C158.8,29.8,128.8,0,91.9,0z" />
<path class="st4" d="M91.9,1.5c36.8,0,66.5,29.6,67,66.1c0-0.2,0-0.4,0-0.6c0-37-30-67-67-67s-67,29.8-67,67c0,0.2,0,0.4,0,0.6
C25.3,31.1,55.1,1.5,91.9,1.5L91.9,1.5z" />
<path class="st5" d="M95.9,214.3c-0.2,2.1-1.9,3.6-4,3.6s-3.8-1.5-4-3.6c-6.5-71.8-62.5-98.2-63-148.1c0,0.4,0,0.6,0,1.1
c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6c6.8-72,63.2-98.4,63.2-148.9c0-0.4,0-0.6,0-1.1
C158.4,116,102.4,142.4,95.9,214.3L95.9,214.3z" />
</g>
<path class="st6"
d="M76.5,34.3v40.6h11v33.2l25.8-44.4H98.5l14.8-29.6C113.4,34.3,76.5,34.3,76.5,34.3z" />
</g>
</g>
<g class="st7">
<path class="st8"
d="M307.9,102.9h-45.4v39.6h52.2v6.9h-60.4V52.3h60.1v6.9h-51.9v36.7h45.4V102.9z" />
<path class="st8"
d="M361.2,137.4l0.5,2.1l0.6-2.1l30.5-85.1h9l-36.1,97.1h-7.9l-36.1-97.1h8.9L361.2,137.4z" />
<path class="st8" d="M427,52.4l35.8,85.7l35.9-85.7h10.9v97.1h-8.2v-42.3l0.7-43.3L466,149.5h-6.3l-36-85.3l0.7,42.7v42.5h-8.2
V52.3H427V52.4z" />
<path class="st8" d="M578,149.4c-0.8-2.3-1.3-5.6-1.5-10.1c-2.8,3.6-6.4,6.5-10.7,8.4c-4.3,2-8.9,3-13.8,3
c-6.9,0-12.5-1.9-16.8-5.8s-6.4-8.8-6.4-14.7c0-7,2.9-12.6,8.8-16.7c5.8-4.1,14-6.1,24.4-6.1h14.5v-8.2c0-5.2-1.6-9.2-4.8-12.2
s-7.8-4.4-13.9-4.4c-5.6,0-10.2,1.4-13.8,4.3c-3.6,2.8-5.5,6.3-5.5,10.3l-8-0.1c0-5.7,2.7-10.7,8-14.9s11.9-6.3,19.7-6.3
c8,0,14.4,2,19,6s7,9.6,7.2,16.8v34.1c0,7,0.7,12.2,2.2,15.7v0.8H578V149.4z M552.9,143.7c5.3,0,10.1-1.3,14.3-3.9
s7.3-6,9.2-10.3v-15.9h-14.3c-8,0.1-14.2,1.5-18.7,4.4c-4.5,2.8-6.7,6.7-6.7,11.6c0,4,1.5,7.4,4.5,10.1S548.1,143.7,552.9,143.7z
" />
<path class="st8" d="M663.6,114.1c0,11.2-2.5,20.2-7.5,26.8s-11.6,9.9-20,9.9c-9.9,0-17.4-3.5-22.7-10.4v36.8h-7.9V77.3h7.4
l0.4,10.2C618.5,79.8,626,76,635.9,76c8.6,0,15.4,3.3,20.3,9.8c4.9,6.5,7.4,15.6,7.4,27.2V114.1z M655.6,112.7
c0-9.2-1.9-16.5-5.7-21.8s-9-8-15.8-8c-4.9,0-9.1,1.2-12.6,3.5c-3.5,2.4-6.2,5.8-8.1,10.3v34.6c1.9,4.1,4.6,7.3,8.2,9.5
c3.6,2.2,7.8,3.3,12.6,3.3c6.7,0,11.9-2.7,15.7-8C653.7,130.6,655.6,122.9,655.6,112.7z" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -21,8 +21,8 @@ android {
minSdkVersion 21
targetSdkVersion 31
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 116
versionName "1.3.11"
versionCode 124
versionName "1.3.13"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(",")
@@ -107,6 +107,7 @@ android {
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
namespace 'net.vonforst.evmap'
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all { variant ->
@@ -151,11 +152,11 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.0-beta01'
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.6.1'
@@ -185,7 +186,7 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
def carAppVersion = '1.2.0-rc01'
def carAppVersion = '1.3.0-beta01'
googleImplementation "androidx.car.app:app:$carAppVersion"
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
@@ -257,7 +258,7 @@ dependencies {
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
}
private static String decode(String s, String key) {

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.vonforst.evmap">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />

View File

@@ -3,10 +3,12 @@ package net.vonforst.evmap.auto
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
@@ -29,9 +31,12 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
@@ -244,15 +249,9 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val operatorText = generateOperatorText(charger)
setTitle(operatorText)
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
charger.cost?.let { addText(generateCostStatusText(it)) }
charger.faultReport?.let { fault ->
addText(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
addText(generateFaultReportTitle(fault))
}
}.build())
} else {
@@ -267,20 +266,14 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val operatorText = generateOperatorText(charger)
setTitle(operatorText)
charger.cost?.let {
addText(it.getStatusText(carContext, emoji = true))
addText(generateCostStatusText(it))
it.getDetailText()?.let { addText(it) }
}
}.build())
// row 3: fault report (if exists)
charger.faultReport?.let { fault ->
rows.add(Row.Builder().apply {
setTitle(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
setTitle(generateFaultReportTitle(fault))
fault.description?.let {
addText(
HtmlCompat.fromHtml(
@@ -305,18 +298,79 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
return rows
}
private fun generateCostStatusText(cost: Cost): CharSequence {
val string = SpannableString(cost.getStatusText(carContext, emoji = true))
// replace emoji with CarIcon
string.indexOf('⚡').takeIf { it >= 0 }?.let { index ->
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_lightning
)
).setTint(CarColor.YELLOW).build()
), index, index + 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
}
string.indexOf('\uD83C').takeIf { it >= 0 }?.let { index ->
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_parking
)
).setTint(CarColor.BLUE).build()
), index, index + 2, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
}
return string
}
private fun generateFaultReportTitle(fault: FaultReport): CharSequence {
val string = SpannableString(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
// replace emoji with CarIcon
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_fault_report
)
).setTint(CarColor.YELLOW).build()
), 0, 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
return string
}
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
if (i > 0) chargepointsText.append(" · ")
chargepointsText.append(
"${cp.count}× ${
nameForPlugType(
carContext.stringProvider(),
cp.type
)
} ${cp.formatPower()}"
)
"${cp.count}× "
).append(
nameForPlugType(carContext.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
iconForPlugType(cp.type)
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
).append(" ").append(cp.formatPower())
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",

View File

@@ -47,30 +47,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
setHeaderAction(Action.BACK)
setActionStrip(
ActionStrip.Builder().apply {
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (prefs.placeSearchResultAndroidAuto != null) {
R.drawable.ic_search_off
} else {
R.drawable.ic_search
}
)
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
screenManager.pop()
} else {
screenManager.push(PlaceSearchScreen(carContext, session))
}
})
}.build())
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(

View File

@@ -5,7 +5,6 @@ import android.location.Location
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
@@ -65,6 +64,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private var location: Location? = null
private var lastDistanceUpdateTime: Instant? = null
private var chargers: List<ChargeLocation>? = null
private var loadingError = false
private var prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val repo =
@@ -108,8 +108,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
override fun onGetTemplate(): Template {
session.requestLocationUpdates()
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
setTitle(
@@ -123,17 +121,23 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
)
)
searchLocation?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
if (prefs.placeSearchResultAndroidAutoName != null) {
setMarker(
PlaceMarker.Builder()
.setColor(CarColor.PRIMARY)
.build()
)
}
}.build())
} ?: setLoading(true)
if (prefs.placeSearchResultAndroidAutoName != null) {
searchLocation?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
if (prefs.placeSearchResultAndroidAutoName != null) {
setMarker(
PlaceMarker.Builder()
.setColor(CarColor.PRIMARY)
.build()
)
}
}.build())
} ?: setLoading(true)
} else {
location?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
} ?: setLoading(true)
}
chargers?.take(maxRows)?.let { chargerList ->
val builder = ItemList.Builder()
// only show the city if not all chargers are in the same city
@@ -152,7 +156,17 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
builder.setOnItemsVisibilityChangedListener(this@MapScreen)
setItemList(builder.build())
} ?: setLoading(true)
} ?: run {
if (loadingError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.connection_error)
)
setItemList(builder.build())
} else {
setLoading(true)
}
}
setCurrentLocationEnabled(true)
setHeaderAction(Action.APP_ICON)
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
@@ -178,6 +192,42 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
session.mapScreen = null
}
.build())
.addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (prefs.placeSearchResultAndroidAuto != null) {
R.drawable.ic_search_off
} else {
R.drawable.ic_search
}
)
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
screenManager.pushForResult(DummyReturnScreen(carContext)) {
chargers = null
loadChargers()
}
} else {
screenManager.pushForResult(
PlaceSearchScreen(
carContext,
session
)
) {
chargers = null
loadChargers()
}
session.mapScreen = null
}
})
}.build())
.addAction(
Action.Builder()
.setIcon(
@@ -196,7 +246,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
.build())
.build())
setOnContentRefreshListener(this@MapScreen)
if (carContext.carAppApiLevel >= 5) {
setOnContentRefreshListener(this@MapScreen)
}
}.build()
}
@@ -313,6 +365,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
this.searchLocation = searchLocation
updateCoroutine = lifecycleScope.launch {
loadingError = false
try {
filterStatus = prefs.filterStatus
val filterValues =
@@ -337,7 +390,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR) {
withContext(Dispatchers.Main) { showLoadingError() }
loadingError = true
return@launch
}
var chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
@@ -351,7 +404,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR) {
withContext(Dispatchers.Main) { showLoadingError() }
loadingError = true
invalidate()
return@launch
}
chargers =
@@ -365,16 +419,12 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) { showLoadingError() }
loadingError = true
invalidate()
}
}
}
private fun showLoadingError() {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()
}
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
val isUpdate = this.energyLevel == null
this.energyLevel = energyLevel
@@ -383,6 +433,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
override fun onStart(owner: LifecycleOwner) {
setupListeners()
session.requestLocationUpdates()
// Reloading chargers in onStart does not seem to count towards content limit.
// So let's do this so the user gets fresh chargers when re-entering the app.

View File

@@ -3,13 +3,13 @@ package net.vonforst.evmap.auto
import android.content.Context
import android.graphics.Bitmap
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.*
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import java.util.*
import kotlin.math.roundToInt
@@ -152,4 +152,17 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
}
}
return true
}
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
/*
Dummy screen to get around template refresh limitations.
It immediately pops back to the previous screen.
*/
override fun onGetTemplate(): Template {
screenManager.pop()
return MessageTemplate.Builder(carContext.getString(R.string.loading)).setLoading(true)
.build()
}
}

View File

@@ -32,4 +32,5 @@
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap (Mapbox) für die Kartendaten wechseln.</string>
<string name="selecting_all">alle Einträge ausgewählt</string>
<string name="selecting_none">alle Einträge abgewählt</string>
<string name="loading">Lade…</string>
</resources>

View File

@@ -32,4 +32,5 @@
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap (Mapbox) for the map data.</string>
<string name="selecting_all">selected all items</string>
<string name="selecting_none">deselected all items</string>
<string name="loading">Loading…</string>
</resources>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.vonforst.evmap">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.car.permission.CAR_INFO" />
<uses-permission android:name="android.car.permission.CAR_ENERGY" />

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.vonforst.evmap">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

View File

@@ -114,8 +114,8 @@ interface ChargepriceApi {
@JvmStatic
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
// list of countries updated 2021/08/24
"goingelectric" -> country in listOf(
// list of countries according to Chargeprice.app, 2021/08/24
"Deutschland",
"Österreich",
"Schweiz",
@@ -133,9 +133,28 @@ interface ChargepriceApi {
"Italien",
"Spanien",
"Großbritannien",
"Irland"
"Irland",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
"Finnland",
"Lettland",
"Litauen",
"Estland",
"Liechtenstein",
"Rumänien",
"Slowakei",
"Slowenien",
"Polen",
"Serbien",
"Bulgarien",
"Kosovo",
"Montenegro",
"Albanien",
"Griechenland",
"Portugal",
"Island"
)
"openchargemap" -> country in listOf(
// list of countries according to Chargeprice.app, 2021/08/24
"DE",
"AT",
"CH",
@@ -153,7 +172,25 @@ interface ChargepriceApi {
"IT",
"ES",
"GB",
"IE"
"IE",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
"FI",
"LV",
"LT",
"EE",
"LI",
"RO",
"SK",
"SI",
"PL",
"RS",
"BG",
"XK",
"ME",
"AL",
"GR",
"PT",
"IS"
)
else -> false
}

View File

@@ -393,7 +393,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.charger.value?.data
if (charger?.editUrl != null) {
(activity as? MapsActivity)?.openUrl(charger.editUrl)
if (vm.apiId == "going_electric") {
if (vm.apiId.value == "going_electric") {
// instructions specific to GoingElectric
Toast.makeText(
requireContext(),

View File

@@ -26,7 +26,9 @@ abstract class LocationEngine(protected val context: Context) {
*/
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun requestLocationUpdates(priority: Priority, intervalMs: Long, listener: LocationListener) {
requests.add(LocationRequest(priority, intervalMs, listener))
if (!requests.any { it.listener == listener }) {
requests.add(LocationRequest(priority, intervalMs, listener))
}
enable()
}

View File

@@ -64,6 +64,7 @@ abstract class FilterValueDao {
}
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
emit(null)
emit(getFilterValuesAsync(filterStatus, dataSource))
}

View File

@@ -12,13 +12,19 @@ import kotlin.reflect.full.cast
fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>>
): MediatorLiveData<FilterValues> =
MediatorLiveData<FilterValues>().apply {
filterValues: LiveData<List<FilterValue>?>
): MediatorLiveData<FilterValues?> =
MediatorLiveData<FilterValues?>().apply {
listOf(filters, filterValues).forEach {
addSource(it) {
val f = filters.value ?: return@addSource
val values = filterValues.value ?: return@addSource
val f = filters.value ?: run {
value = null
return@addSource
}
val values = filterValues.value ?: run {
value = null
return@addSource
}
value = filtersWithValue(f, values)
}
}

View File

@@ -20,11 +20,11 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
private val repo = ChargeLocationsRepository(api, viewModelScope, db, prefs)
private val filters = repo.getFilters(application.stringProvider())
private val filterValues: LiveData<List<FilterValue>> by lazy {
private val filterValues: LiveData<List<FilterValue>?> by lazy {
db.filterValueDao().getFilterValues(FILTERS_CUSTOM, prefs.dataSource)
}
val filtersWithValue: LiveData<FilterValues> by lazy {
val filtersWithValue: LiveData<FilterValues?> by lazy {
filtersWithValue(filters, filterValues)
}

View File

@@ -39,11 +39,6 @@ internal fun getClusterDistance(zoom: Float): Int? {
class MapViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
val apiId: String
get() = repo.api.value!!.id
val apiName: String
get() = repo.api.value!!.name
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
private val repo = ChargeLocationsRepository(
@@ -53,6 +48,15 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
prefs
)
val apiId = repo.api.map { it.id }
init {
// necessary so that apiId is updated
apiId.observeForever { }
}
val apiName = repo.api.map { it.name }
val bottomSheetState: MutableLiveData<Int> by lazy {
state.getLiveData("bottomSheetState")
}
@@ -69,12 +73,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
}
private val filterValues: LiveData<List<FilterValue>> = repo.api.switchMap {
private val filterValues: LiveData<List<FilterValue>?> = repo.api.switchMap {
db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
}
private val filters = repo.getFilters(application.stringProvider())
private val filtersWithValue: LiveData<FilterValues> by lazy {
private val filtersWithValue: LiveData<FilterValues?> by lazy {
filtersWithValue(filters, filterValues)
}
@@ -88,9 +92,9 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
MediatorLiveData<Int>().apply {
value = 0
addSource(filtersWithValue) { filtersWithValue ->
value = filtersWithValue.count {
value = filtersWithValue?.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
}
} ?: 0
}
}
}
@@ -164,15 +168,15 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val location: MutableLiveData<LatLng> by lazy {
MutableLiveData<LatLng>()
}
val availability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
addSource(chargerSparse) { charger ->
if (charger != null) {
viewModelScope.launch {
loadAvailability(charger)
private val triggerAvailabilityRefresh = MutableLiveData<Boolean>(true)
val availability: LiveData<Resource<ChargeLocationStatus>> by lazy {
chargerSparse.switchMap { charger ->
charger?.let {
triggerAvailabilityRefresh.switchMap {
liveData {
emit(Resource.loading(null))
emit(getAvailability(charger))
}
} else {
value = null
}
}
}
@@ -235,7 +239,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
fun reloadPrefs() {
filterStatus.value = prefs.filterStatus
if (prefs.dataSource != apiId) {
if (prefs.dataSource != apiId.value) {
repo.api.value = createApi(prefs.dataSource, getApplication())
}
}
@@ -342,46 +346,43 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
chargepoints.addSource(result) {
chargepoints.value = it
if (apiId == "going_electric") {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()
val apiId = apiId.value
when (apiId) {
"going_electric" -> {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else if (apiId == "open_charge_map") {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
repo.referenceData.value!! as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
}
"open_charge_map" -> {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
repo.referenceData.value!! as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
}
else -> {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
}
}
}
}
private suspend fun loadAvailability(charger: ChargeLocation) {
availability.value = Resource.loading(null)
availability.value = getAvailability(charger)
}
fun reloadAvailability() {
val charger = chargerSparse.value ?: return
viewModelScope.launch {
loadAvailability(charger)
}
triggerAvailabilityRefresh.value = true
}
fun loadChargerById(chargerId: Long) {

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M11,21h-1l1,-7H7.5c-0.58,0 -0.57,-0.32 -0.38,-0.66 0.19,-0.34 0.05,-0.08 0.07,-0.12C8.48,10.94 10.42,7.54 13,3h1l-1,7h3.5c0.49,0 0.56,0.33 0.47,0.51l-0.07,0.15C12.96,17.55 11,21 11,21z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M13,3L6,3v18h4v-6h3c3.31,0 6,-2.69 6,-6s-2.69,-6 -6,-6zM13.2,11L10,11L10,7h3.2c1.1,0 2,0.9 2,2s-0.9,2 -2,2z" />
</vector>

View File

@@ -244,7 +244,7 @@
<string name="help">Hilfe</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Schieflast erlauben</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Einphasiges laden mit mehr als 4.5 kW erlauben</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Einphasiges Laden mit mehr als 4.5 kW erlauben</string>
<string name="pref_map_rotate_gestures_enabled">Kartenrotation</string>
<string name="pref_map_rotate_gestures_on">Karte mit zwei Fingern rotieren</string>
<string name="pref_map_rotate_gestures_off">Karte immer nach Norden ausrichten</string>

View File

@@ -3,14 +3,14 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.5.1'
ext.nav_version = '2.5.2'
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"

View File

@@ -0,0 +1,8 @@
Verbesserungen:
- Weitere unterstützte Länder für Preisvergleich mit Chargeprice.app
- Android Auto: Suchbutton nun auf dem Hauptbildschirm
- Android Auto: Emoji durch Icons ersetzt
Fehler behoben:
- Abstürze / Inkonsistenzen nach Wechsel der Datenquelle
- Probleme beim Laden der Echtzeitdaten

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- verschiedene filterabhängige Anzeigen waren seit 1.3.11 nicht mehr korrekt

View File

@@ -0,0 +1,8 @@
Improvements:
- More European countries supported for price comparison with Chargeprice.app
- Android Auto: Search button is now located on main screen
- Android Auto: Replaced emojis with proper icons
Bugfixes:
- Crashes / inconsistencies after switching data source
- Problems when loading realtime availability data

View File

@@ -0,0 +1,2 @@
Bugfixes:
- some filter-dependent views were not correct anymore since 1.3.11

View File

@@ -1,6 +1,6 @@
#Sat Aug 06 15:33:46 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME