Compare commits

...

21 Commits
1.3.0 ... 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
johan12345
518cf11dc8 Release 1.3.1 2022-06-09 22:02:05 +02:00
Johan von Forstner
2f3d4dd90e switch car app category to POI 2022-06-09 22:00:57 +02:00
johan12345
c8b2c34f47 fix unit tests broken with a7c18fc325 2022-06-09 21:48:21 +02:00
johan12345
57a16ec5f8 use POST requests for GoingElectric instead of GET
fixes #174
2022-06-09 21:48:20 +02:00
johan12345
a4bbb15f64 add option to disable map rotation gestures 2022-06-09 21:27:37 +02:00
johan12345
a7c18fc325 AvailabilityDetectors: increase threshold for merging locations 2022-06-09 19:35:53 +02:00
Johan von Forstner
13034df25e Merge pull request #173 from johan12345/dependabot/bundler/jmespath-1.6.1
Bump jmespath from 1.4.0 to 1.6.1
2022-06-08 07:01:44 +02:00
dependabot[bot]
6e22f26e54 Bump jmespath from 1.4.0 to 1.6.1
Bumps [jmespath](https://github.com/trevorrowe/jmespath.rb) from 1.4.0 to 1.6.1.
- [Release notes](https://github.com/trevorrowe/jmespath.rb/releases)
- [Changelog](https://github.com/jmespath/jmespath.rb/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trevorrowe/jmespath.rb/compare/v1.4.0...v1.6.1)

---
updated-dependencies:
- dependency-name: jmespath
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-07 21:46:24 +00:00
33 changed files with 299 additions and 104 deletions

View File

@@ -113,7 +113,7 @@ GEM
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
jmespath (1.6.1)
json (2.3.1)
jwt (2.2.1)
memoist (0.16.2)

View File

@@ -18,8 +18,8 @@ android {
minSdkVersion 21
targetSdkVersion 31
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 74
versionName "1.3.0"
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'
@@ -174,14 +175,17 @@ dependencies {
googleAutomotiveImplementation 'androidx.car.app:app-automotive:1.2.0-rc01'
// AnyMaps
def anyMapsVersion = '751daec281'
def anyMapsVersion = '3c67d7a1dc'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.0.2'
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
implementation("com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
}
// 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

@@ -39,7 +39,7 @@
<intent-filter>
<action
android:name="androidx.car.app.CarAppService"
android:category="androidx.car.app.category.CHARGING" />
android:category="androidx.car.app.category.POI" />
</intent-filter>
</service>

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

@@ -12,7 +12,7 @@ import retrofit2.http.Path
import retrofit2.http.Query
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
private const val maxDistance = 15 // max distance between reported positions in meters
private const val maxDistance = 40 // max distance between reported positions in meters
interface EnBwApi {
@GET("chargestations?grouping=false")
@@ -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

@@ -11,8 +11,8 @@ import retrofit2.http.GET
import retrofit2.http.Path
import java.util.*
private const val coordRange = 0.1 // range of latitude and longitude for loading the map
private const val maxDistance = 15 // max distance between reported positions in meters
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
private const val maxDistance = 40 // max distance between reported positions in meters
interface NewMotionApi {
@GET("markers/{lngMin}/{lngMax}/{latMin}/{latMax}")

View File

@@ -21,50 +21,51 @@ import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.*
import java.io.IOException
interface GoingElectricApi {
@GET("chargepoints/")
@FormUrlEncoded
@POST("chargepoints/")
suspend fun getChargepoints(
@Query("sw_lat") sw_lat: Double, @Query("sw_lng") sw_lng: Double,
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
@Query("zoom") zoom: Float,
@Query("clustering") clustering: Boolean = false,
@Query("cluster_distance") clusterDistance: Int? = null,
@Query("freecharging") freecharging: Boolean = false,
@Query("freeparking") freeparking: Boolean = false,
@Query("min_power") minPower: Int = 0,
@Query("plugs") plugs: String? = null,
@Query("chargecards") chargecards: String? = null,
@Query("networks") networks: String? = null,
@Query("categories") categories: String? = null,
@Query("startkey") startkey: Int? = null,
@Query("open_twentyfourseven") open247: Boolean = false,
@Query("barrierfree") barrierfree: Boolean = false,
@Query("exclude_faults") excludeFaults: Boolean = false
@Field("sw_lat") sw_lat: Double, @Field("sw_lng") sw_lng: Double,
@Field("ne_lat") ne_lat: Double, @Field("ne_lng") ne_lng: Double,
@Field("zoom") zoom: Float,
@Field("clustering") clustering: Boolean = false,
@Field("cluster_distance") clusterDistance: Int? = null,
@Field("freecharging") freecharging: Boolean = false,
@Field("freeparking") freeparking: Boolean = false,
@Field("min_power") minPower: Int = 0,
@Field("plugs") plugs: String? = null,
@Field("chargecards") chargecards: String? = null,
@Field("networks") networks: String? = null,
@Field("categories") categories: String? = null,
@Field("startkey") startkey: Int? = null,
@Field("open_twentyfourseven") open247: Boolean = false,
@Field("barrierfree") barrierfree: Boolean = false,
@Field("exclude_faults") excludeFaults: Boolean = false
): Response<GEChargepointList>
@GET("chargepoints/")
@FormUrlEncoded
@POST("chargepoints/")
suspend fun getChargepointsRadius(
@Query("lat") lat: Double, @Query("lng") lng: Double,
@Query("radius") radius: Int,
@Query("zoom") zoom: Float,
@Query("orderby") orderby: String = "distance",
@Query("clustering") clustering: Boolean = false,
@Query("cluster_distance") clusterDistance: Int? = null,
@Query("freecharging") freecharging: Boolean = false,
@Query("freeparking") freeparking: Boolean = false,
@Query("min_power") minPower: Int = 0,
@Query("plugs") plugs: String? = null,
@Query("chargecards") chargecards: String? = null,
@Query("networks") networks: String? = null,
@Query("categories") categories: String? = null,
@Query("startkey") startkey: Int? = null,
@Query("open_twentyfourseven") open247: Boolean = false,
@Query("barrierfree") barrierfree: Boolean = false,
@Query("exclude_faults") excludeFaults: Boolean = false
@Field("lat") lat: Double, @Field("lng") lng: Double,
@Field("radius") radius: Int,
@Field("zoom") zoom: Float,
@Field("orderby") orderby: String = "distance",
@Field("clustering") clustering: Boolean = false,
@Field("cluster_distance") clusterDistance: Int? = null,
@Field("freecharging") freecharging: Boolean = false,
@Field("freeparking") freeparking: Boolean = false,
@Field("min_power") minPower: Int = 0,
@Field("plugs") plugs: String? = null,
@Field("chargecards") chargecards: String? = null,
@Field("networks") networks: String? = null,
@Field("categories") categories: String? = null,
@Field("startkey") startkey: Int? = null,
@Field("open_twentyfourseven") open247: Boolean = false,
@Field("barrierfree") barrierfree: Boolean = false,
@Field("exclude_faults") excludeFaults: Boolean = false
): Response<GEChargepointList>
@GET("chargepoints/")

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

@@ -23,7 +23,10 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.*
import androidx.core.view.MenuCompat
import androidx.core.view.doOnLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -92,6 +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.component1
import kotlin.collections.component2
import kotlin.collections.contains
import kotlin.collections.set
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
@@ -398,6 +405,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
else -> false
}
}
binding.detailView.btnRefreshLiveData.setOnClickListener {
vm.reloadAvailability()
}
}
var searchKeyListener: KeyListener? = null
@@ -795,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) {
@@ -811,6 +821,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
animator = MarkerAnimator(chargerIconGenerator)
map.uiSettings.setTiltGesturesEnabled(false)
map.uiSettings.setRotateGesturesEnabled(prefs.mapRotateGesturesEnabled)
map.setIndoorEnabled(false)
map.uiSettings.setIndoorLevelPickerEnabled(false)
map.setOnCameraIdleListener {

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

@@ -29,6 +29,12 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putBoolean("navigate_use_maps", value).apply()
}
var mapRotateGesturesEnabled: Boolean
get() = sp.getBoolean("map_rotate_gestures_enabled", true)
set(value) {
sp.edit().putBoolean("map_rotate_gestures_enabled", value).apply()
}
var lastGeReferenceDataUpdate: Instant
get() = Instant.ofEpochMilli(sp.getLong("last_ge_reference_data_update", 0L))
set(value) {

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

@@ -255,4 +255,8 @@
<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"><![CDATA[Erlaubt das Laden mit >4.5 kW an AC-Stationen für Autos mit 1-phasigem Lader]]></string>
<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

@@ -240,4 +240,8 @@
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Enable unbalanced load</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary"><![CDATA[Allow charging with >4.5 kW at AC stations for cars with single-phase charger]]></string>
<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

@@ -15,6 +15,12 @@
android:entryValues="@array/pref_darkmode_values"
android:defaultValue="default"
android:summary="@string/pref_darkmode_summary" />
<CheckBoxPreference
android:key="map_rotate_gestures_enabled"
android:title="@string/pref_map_rotate_gestures_enabled"
android:summaryOn="@string/pref_map_rotate_gestures_on"
android:summaryOff="@string/pref_map_rotate_gestures_off"
android:defaultValue="true" />
<CheckBoxPreference
android:key="navigate_use_maps"
android:title="@string/pref_navigate_use_maps"

View File

@@ -43,8 +43,8 @@ class NewMotionAvailabilityDetectorTest {
"nm/markers" -> {
val urlTail = segments.subList(2, segments.size).joinToString("/")
val id = when (urlTail) {
"9.47108/9.67108/54.4116/54.6116" -> 2105
"9.444284/9.644283999999999/54.376699/54.576699000000005" -> 18284
"9.56608/9.576080000000001/54.5066/54.516600000000004" -> 2105
"9.539283999999999/9.549284/54.471699/54.481699000000006" -> 18284
else -> -1
}
return okResponse("/newmotion/$id/markers.json")

View File

@@ -3,6 +3,7 @@ package net.vonforst.evmap.api.goingelectric
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.notFoundResponse
import net.vonforst.evmap.okResponse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
@@ -33,10 +34,13 @@ class GoingElectricApiTest {
if (id != null) {
return okResponse("/chargers/$id.json")
} else {
val body = request.body.readUtf8()
val bodyQuery = "http://host?$body".toHttpUrl()
val freeparking =
request.requestUrl!!.queryParameter("freeparking")!!.toBoolean()
bodyQuery.queryParameter("freeparking")!!.toBoolean()
val freecharging =
request.requestUrl!!.queryParameter("freecharging")!!.toBoolean()
bodyQuery.queryParameter("freecharging")!!.toBoolean()
return if (freeparking && freecharging) {
okResponse("/chargers/list-empty.json")
} else if (freecharging) {

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,6 @@
Verbesserungen:
- Neue Einstellung zum Deaktivieren der Kartenrotation
Fehler behoben:
- Fehler bei Filter nach vielen Verbünden behoben (GoingElectric)
- geringfügige Verbesserung bei Echtzeitdaten

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,6 @@
Improvements:
- New option to disable map rotation
Bugfixes:
- Fixed bug when filtering for many networks (GoingElectric)
- Minor improvements for realtime data

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