Compare commits

...

13 Commits
1.3.1 ... 1.3.2

Author SHA1 Message Date
johan12345
d02dd41127 release 1.3.2 2022-06-12 20:17:24 +02:00
johan12345
41bafbcf46 fix issues after Kotlin upgrade 2022-06-12 17:56:10 +02:00
johan12345
c135e87be5 fix build flavor check in MapFragment 2022-06-12 17:44:12 +02:00
johan12345
f6fd8866da update dependencies 2022-06-12 17:43:30 +02:00
johan12345
3c485ff0c0 EnBwAvailabilityDetector: fix crash when maxPowerInKw == null 2022-06-12 17:23:24 +02:00
johan12345
0ca8fb0eee Add ability to refresh availability data
fixes #175
2022-06-12 17:22:50 +02:00
johan12345
dc9f47df8a EnBW AvailabilityDetector: support "OUT_OF_SERVICE" status 2022-06-12 16:23:22 +02:00
johan12345
4fab0fbf04 remove label "MapFragment"
(which sometimes appears for a short time during navigation)
2022-06-10 22:00:11 +02:00
johan12345
7bdd277c92 fix color for filteredAvailability 2022-06-10 21:47:16 +02:00
johan12345
3c3d6de867 properly handle opening hours that go past midnight 2022-06-10 21:42:29 +02:00
johan12345
b9d79994f1 detail view: do not show cost description twice
if freeparking and freecharging == null
2022-06-10 21:19:57 +02:00
johan12345
133a2be961 fix layout issues with long charger names 2022-06-10 21:11:23 +02:00
johan12345
cd934ff448 update stored favorite data when loading its details 2022-06-10 20:57:29 +02:00
21 changed files with 204 additions and 73 deletions

View File

@@ -18,8 +18,8 @@ android {
minSdkVersion 21
targetSdkVersion 31
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 78
versionName "1.3.1"
versionCode 80
versionName "1.3.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -137,17 +137,18 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
@@ -184,7 +185,7 @@ dependencies {
}
// Google Places
implementation 'com.google.android.libraries.places:places:2.5.0'
implementation 'com.google.android.libraries.places:places:2.6.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
// Mapbox Geocoding

View File

@@ -83,8 +83,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
return PaneTemplate.Builder(
Pane.Builder().apply {
charger?.let { charger ->
if (largeImageSupported && photo != null) {
setImage(CarIcon.Builder(IconCompat.createWithBitmap(photo)).build())
if (largeImageSupported) {
photo?.let {
setImage(CarIcon.Builder(IconCompat.createWithBitmap(it)).build())
}
}
generateRows(charger).forEach { addRow(it) }
addAction(
@@ -205,6 +207,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private fun generateRows(charger: ChargeLocation): List<Row> {
val rows = mutableListOf<Row>()
val photo = photo
// Row 1: address + chargepoints
rows.add(Row.Builder().apply {
@@ -266,7 +269,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
setTitle(operatorText)
charger.cost?.let {
addText(it.getStatusText(carContext, emoji = true))
(it.descriptionShort ?: it.descriptionLong)?.let { addText(it) }
it.getDetailText()?.let { addText(it) }
}
}.build())
// row 3: fault report (if exists)

View File

@@ -91,7 +91,7 @@ fun buildDetails(
R.drawable.ic_cost,
R.string.cost,
loc.cost.getStatusText(ctx),
loc.cost.descriptionLong ?: loc.cost.descriptionShort
loc.cost.getDetailText()
)
else null,
if (loc.chargecards != null && loc.chargecards.isNotEmpty() || loc.barrierFree == true)

View File

@@ -59,7 +59,7 @@ interface EnBwApi {
@JsonClass(generateAdapter = true)
data class EnBwConnector(
val plugTypeName: String,
val maxPowerInKw: Double,
val maxPowerInKw: Double?,
)
@JsonClass(generateAdapter = true)
@@ -162,7 +162,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
val enbwStatus = mutableMapOf<Long, ChargepointStatus>()
connectorStatus.forEachIndexed { index, (connector, statusStr) ->
val id = index.toLong()
val power = connector.maxPowerInKw
val power = connector.maxPowerInKw ?: 0.0
val type = when (connector.plugTypeName) {
"Typ 3A" -> Chargepoint.TYPE_3
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
@@ -175,6 +175,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
}
val status = when (statusStr) {
"UNAVAILABLE" -> ChargepointStatus.FAULTED
"OUT_OF_SERVICE" -> ChargepointStatus.FAULTED
"AVAILABLE" -> ChargepointStatus.AVAILABLE
"OCCUPIED" -> ChargepointStatus.CHARGING
"UNSPECIFIED" -> ChargepointStatus.UNKNOWN

View File

@@ -29,7 +29,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
location?.let {
proximity(Point.fromLngLat(location.longitude, location.latitude))
}
languages(ConfigurationCompat.getLocales(context.resources.configuration)[0].language)
languages(ConfigurationCompat.getLocales(context.resources.configuration)[0]?.language)
accessToken(context.getString(R.string.mapbox_key))
autocomplete(true)
this.query(query)

View File

@@ -110,6 +110,12 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
createTouchHelper().attachToRecyclerView(binding.favsList)
locationClient!!.connect()
binding.swipeRefresh.setOnRefreshListener {
vm.reloadAvailability() {
binding.swipeRefresh.isRefreshing = false
}
}
}
override fun onConnected() {

View File

@@ -95,28 +95,10 @@ import net.vonforst.evmap.utils.checkFineLocationPermission
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.viewmodel.*
import java.io.IOException
import kotlin.collections.List
import kotlin.collections.Set
import kotlin.collections.any
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.contains
import kotlin.collections.emptyList
import kotlin.collections.filterIsInstance
import kotlin.collections.find
import kotlin.collections.forEach
import kotlin.collections.getOrNull
import kotlin.collections.isNotEmpty
import kotlin.collections.iterator
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.mapNotNull
import kotlin.collections.set
import kotlin.collections.sortedBy
import kotlin.collections.sortedByDescending
import kotlin.collections.toList
import kotlin.collections.toSet
import kotlin.collections.toTypedArray
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
@@ -423,6 +405,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
else -> false
}
}
binding.detailView.btnRefreshLiveData.setOnClickListener {
vm.reloadAvailability()
}
}
var searchKeyListener: KeyListener? = null
@@ -820,7 +805,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
this.map = map
chargerIconGenerator = ChargerIconGenerator(requireContext(), map.bitmapDescriptorFactory)
if (BuildConfig.FLAVOR == "google" && mapFragment!!.priority[0] == MapFragment.GOOGLE) {
if (BuildConfig.FLAVOR.contains("google") && mapFragment!!.priority[0] == MapFragment.GOOGLE) {
// Google Maps: icons can be generated in background thread
lifecycleScope.launch {
withContext(Dispatchers.IO) {

View File

@@ -199,6 +199,18 @@ data class Cost(
return ""
}
}
fun getDetailText(): CharSequence? {
return if (freecharging == null && freeparking == null) {
if (descriptionShort != null && descriptionLong != descriptionShort) {
descriptionLong
} else {
null
}
} else {
descriptionLong ?: descriptionShort
}
}
}
@Parcelize
@@ -215,26 +227,63 @@ data class OpeningHours(
if (twentyfourSeven) {
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
} else if (days != null) {
val hours = days.getHoursForDate(LocalDate.now())
?: return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
val today = LocalDate.now()
val hours = days.getHoursForDate(today)
val nextDayHours = days.getHoursForDate(today.plusDays(1))
val previousDayHours = days.getHoursForDate(today.minusDays(1))
val now = LocalTime.now()
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
if (previousDayHours != null && previousDayHours.end.isBefore(previousDayHours.start) && previousDayHours.end.isAfter(
now
)
) {
// previous day has opening hours that go past midnight
return HtmlCompat.fromHtml(
ctx.getString(
R.string.open_closesat,
hours.end.toString()
previousDayHours.end.format(fmt)
), 0
)
} else if (hours.end.isBefore(now)) {
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
} else {
} else if (hours != null && hours.start.isBefore(hours.end)
&& hours.start.isBefore(now) && hours.end.isAfter(now)
) {
// current day has opening hours that do not go past midnight
return HtmlCompat.fromHtml(
ctx.getString(
R.string.open_closesat,
hours.end.format(fmt)
), 0
)
} else if (hours != null && hours.end.isBefore(hours.start)
&& hours.start.isBefore(now)
) {
// current day has opening hours that go past midnight
return HtmlCompat.fromHtml(
ctx.getString(
R.string.open_closesat,
hours.end.format(fmt)
), 0
)
} else if (hours != null && !hours.start.isBefore(now)) {
// currently closed, will still open on this day
return HtmlCompat.fromHtml(
ctx.getString(
R.string.closed_opensat,
hours.start.toString()
hours.start.format(fmt)
), 0
)
} else if (nextDayHours != null) {
// currently closed, will open next day
return HtmlCompat.fromHtml(
ctx.getString(
R.string.closed_opensat,
nextDayHours.start.format(fmt)
), 0
)
} else {
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
}
} else {
return ""

View File

@@ -32,21 +32,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>>().apply {
addSource(favorites) { favorites ->
if (favorites != null) {
val chargers = favorites.map { it.charger }
viewModelScope.launch {
val data = hashMapOf<Long, Resource<ChargeLocationStatus>>()
chargers.forEach { charger ->
data[charger.id] = Resource.loading(null)
}
availability.value = data
chargers.map { charger ->
async {
data[charger.id] = getAvailability(charger)
availability.value = data
}
}.awaitAll()
}
reloadAvailability()
} else {
value = null
}
@@ -54,6 +40,27 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
}
}
fun reloadAvailability(callback: (() -> Unit)? = null) {
val favorites = favorites.value ?: return
val chargers = favorites.map { it.charger }
viewModelScope.launch {
val data = hashMapOf<Long, Resource<ChargeLocationStatus>>()
chargers.forEach { charger ->
data[charger.id] = Resource.loading(null)
}
availability.value = data
chargers.map { charger ->
async {
data[charger.id] = getAvailability(charger)
availability.value = data
}
}.awaitAll()
callback?.invoke()
}
}
val listData: MediatorLiveData<List<FavoritesListItem>> by lazy {
MediatorLiveData<List<FavoritesListItem>>().apply {
val callback = { _: Any ->

View File

@@ -381,6 +381,13 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
availability.value = getAvailability(charger)
}
fun reloadAvailability() {
val charger = chargerSparse.value ?: return
viewModelScope.launch {
loadAvailability(charger)
}
}
private var chargerLoadingTask: Job? = null
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
@@ -388,7 +395,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
chargerLoadingTask?.cancel()
chargerLoadingTask = viewModelScope.launch {
try {
chargerDetails.value = api.value!!.getChargepointDetail(referenceData, charger.id)
val chargerDetail = api.value!!.getChargepointDetail(referenceData, charger.id)
chargerDetails.value = chargerDetail
if (favorites.value?.any { it.charger.id == chargerDetail.data?.id } == true) {
// update data of stored favorite
db.chargeLocationsDao().insert(charger)
}
} catch (e: IOException) {
chargerDetails.value = Resource.error(e.message, null)
e.printStackTrace()

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</vector>

View File

@@ -82,6 +82,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:ellipsize="end"
android:hyphenationFrequency="normal"
android:maxLines="@{expanded ? 3 : 1}"
android:text="@{charger.data.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
@@ -134,9 +135,9 @@
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(filteredAvailability.data.status.values())), filteredAvailability.data.totalChargepoints)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(availability.data.status.values())}"
app:invisibleUnlessAnimated="@{availability.data != null &amp;&amp; !expanded}"
app:invisibleUnless="@{availability.data != null}"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
app:invisibleUnlessAnimated="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/txtName"
tools:backgroundTint="@color/available"
@@ -292,10 +293,11 @@
android:id="@+id/textView13"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="right|end"
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : @string/realtime_data_unavailable}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/connectors"
tools:text="Echtzeitdaten nicht verfügbar" />
@@ -375,6 +377,18 @@
app:layout_constraintBottom_toBottomOf="parent"
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
<Button
android:id="@+id/btnRefreshLiveData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.App.Button.OutlinedButton.IconOnly.Small"
android:contentDescription="@string/refresh_live_data"
android:enabled="@{availability.status != Status.LOADING}"
app:icon="@drawable/ic_refresh"
app:layout_constraintBottom_toBottomOf="@+id/textView13"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/connectors" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@@ -33,16 +33,21 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/favs_list"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="0dp"
android:layout_height="0dp"
app:data="@{vm.listData}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
app:layout_constraintTop_toBottomOf="@+id/toolbar_container">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/favs_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:data="@{vm.listData}" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"

View File

@@ -50,13 +50,18 @@
<TextView
android:id="@+id/textView15"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@{item.charger.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:maxLines="2"
android:ellipsize="end"
android:hyphenationFrequency="normal"
app:layout_constraintEnd_toStartOf="@+id/textView16"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
tools:text="Nikola-Tesla-Parkhaus mit extra langem Namen, der auf mehrere Zeilen umbricht" />
<TextView
android:id="@+id/textView2"
@@ -110,7 +115,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{item.available.data}"
app:goneUnless="@{item.available.status == Status.SUCCESS}"
app:invisibleUnless="@{item.available.status == Status.SUCCESS}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnDelete"
tools:backgroundTint="@color/available"

View File

@@ -7,7 +7,7 @@
<fragment
android:id="@+id/map"
android:name="net.vonforst.evmap.fragment.MapFragment"
android:label="MapFragment"
android:label=""
tools:layout="@layout/fragment_map">
<action
android:id="@+id/action_map_to_filterFragment"

View File

@@ -258,4 +258,5 @@
<string name="pref_map_rotate_gestures_enabled">Kartenrotation erlauben</string>
<string name="pref_map_rotate_gestures_on">Karte kann mit Zweifingergeste rotiert werden</string>
<string name="pref_map_rotate_gestures_off">Karte bleibt fest nach Norden ausgerichtet</string>
<string name="refresh_live_data">Echtzeitstatus aktualisieren</string>
</resources>

View File

@@ -243,4 +243,5 @@
<string name="pref_map_rotate_gestures_enabled">Enable map rotation</string>
<string name="pref_map_rotate_gestures_on">Map can be rotated with two-finger gesture</string>
<string name="pref_map_rotate_gestures_off">Map will be fixed to north-up</string>
<string name="refresh_live_data">refresh real-time status</string>
</resources>

View File

@@ -56,4 +56,15 @@
<item name="android:minHeight">48dp</item>
</style>
<style name="Widget.App.Button.OutlinedButton.IconOnly.Small" parent="Widget.Material3.Button.OutlinedButton">
<item name="iconPadding">0dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:paddingLeft">7dp</item>
<item name="android:paddingRight">7dp</item>
<item name="android:minWidth">30dp</item>
<item name="android:minHeight">30dp</item>
<item name="iconTint">?android:textColorSecondary</item>
</style>
</resources>

View File

@@ -1,9 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.31'
ext.kotlin_version = '1.6.21'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.4.1'
ext.nav_version = '2.4.2'
repositories {
google()
mavenCentral()

View File

@@ -0,0 +1,10 @@
Verbesserungen:
- Button um Echtzeitdaten neu zu laden
Fehler behoben:
- Verfügbarkeit bei defekten Ladestationen wurde als "unbekannt" angezeigt
- Kostenbeschreibung wurde in manchen Fällen doppelt angezeigt
- Falsche Darstellung von Öffnungszeiten nach Mitternacht
- Gespeicherte Details von Favoriten wurden bei Änderungen nicht aktualisiert
- ggf. falsche Farbe für Echtzeitstatus bei Filter nach Anschlüssen
- Absturz behoben

View File

@@ -0,0 +1,10 @@
Improvements:
- Added button to reload live availability
Bugfixes:
- Availability of some broken chargers was shown as "unknown"
- Cost description was sometimes shown twice
- Incorrect handling of opening hours after midnight
- Saved details of favorite chargers were not updated after changes
- When filtering for specific connectors, realtime status may have had incorrect color
- Fixed crash