Compare commits

...

58 Commits

Author SHA1 Message Date
Johan von Forstner
897b439cca fix crash when favorite exists but charger has been deleted from cache 2025-12-24 16:40:16 +01:00
Robert Högberg
141b2c76b1 Nobil: Add real-time availability support 2025-12-24 16:40:16 +01:00
Johan von Forstner
fc22b16111 NewMotionAvailabilityDetector: get correct charger power if available 2025-12-24 12:08:43 +01:00
Hosted Weblate
f41ea230de Translated using Weblate (German)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Pixelcode <pixelcode@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2025-12-24 11:46:56 +01:00
dependabot[bot]
ceb5081757 Bump aws-sdk-s3 from 1.78.0 to 1.208.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.78.0 to 1.208.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.208.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 06:11:21 -05:00
Johan von Forstner
28bb8cef5f Update androidx.core.splashscreen 2025-12-10 17:42:39 +01:00
Robert Högberg
ba17cb989a Nobil: Fix data expiration 2025-12-03 22:18:21 +01:00
johan12345
d08aaa3325 Auto: make AboutScreen only accessible when parked 2025-12-02 17:46:20 +01:00
Johan von Forstner
0f24608d2a Remove references to Chargeprice from README.md 2025-11-30 15:09:19 +01:00
Hosted Weblate
92e9539286 Translated using Weblate (Norwegian Bokmål)
Currently translated at 74.0% (279 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2025-11-22 12:37:02 +01:00
Hosted Weblate
b373f49180 Translated using Weblate (Swedish)
Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/sv/
Translation: EVMap/Android
2025-11-22 12:37:02 +01:00
Hosted Weblate
ec8728a253 Translated using Weblate (French)
Currently translated at 91.2% (344 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2025-11-22 12:37:02 +01:00
Hosted Weblate
9ca470cd46 Translated using Weblate (Dutch)
Currently translated at 80.9% (305 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nl/
Translation: EVMap/Android
2025-11-22 12:37:02 +01:00
johan12345
38a1bf2da5 fix #403: add edit URL for OSM 2025-11-22 12:34:56 +01:00
johan12345
5c1dad82b1 add x64 versions of libc++_shared.so to fortify exceptions 2025-11-07 20:22:58 +01:00
Hosted Weblate
5647820f3e Translated using Weblate (Swedish)
Currently translated at 99.7% (376 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/sv/
Translation: EVMap/Android
2025-11-07 20:10:27 +01:00
Hosted Weblate
092a3e50bc Translated using Weblate (Czech)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2025-11-07 20:10:27 +01:00
Hosted Weblate
7b27fe2cac Translated using Weblate (Estonian)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/et/
Translation: EVMap/Android
2025-11-07 20:10:27 +01:00
johan12345
8991cb4e4a increase minSdk to 23 2025-11-07 20:04:33 +01:00
johan12345
66d6aee97e upgrade spatia-room 2025-11-07 19:57:11 +01:00
Johan von Forstner
3a5646a3ac Release 2.0.2 2025-10-30 16:32:08 +01:00
Johan von Forstner
14eadef10d AAOS new map screen: increase default zoom level 2025-10-30 16:23:12 +01:00
Johan von Forstner
cea0878267 OnboardingFragment: use CustomTabs for privacy policy 2025-10-30 16:02:02 +01:00
Johan von Forstner
2b4c0829a8 remove unused Chargeprice icons 2025-10-29 12:21:41 +01:00
Johan von Forstner
8e9d9d15c4 Auto: make new map screen optional for now
mainly due to bug https://issuetracker.google.com/issues/389974133
2025-10-29 11:58:02 +01:00
Hosted Weblate
ca9a7df8b0 Translated using Weblate (Swedish)
Currently translated at 99.7% (375 of 376 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (92 of 92 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/sv/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/sv/
Translation: EVMap/Android
Translation: EVMap/App Store metadata
2025-10-29 11:00:29 +01:00
Hosted Weblate
51aecd179c Translated using Weblate (Czech)
Currently translated at 100.0% (376 of 376 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2025-10-29 11:00:28 +01:00
Hosted Weblate
6781989266 Translated using Weblate (Estonian)
Currently translated at 100.0% (376 of 376 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/et/
Translation: EVMap/Android
2025-10-29 11:00:27 +01:00
johan12345
872d3c5143 Automotive: update "my location" button icon more quickly 2025-10-26 23:40:22 +01:00
johan12345
69622c6816 update AnyMaps 2025-10-26 23:30:17 +01:00
johan12345
15fdac6348 Automotive: implement map rotation by compass 2025-10-26 23:09:23 +01:00
johan12345
6c206c7a25 revert work-runtime-ktx version 2025-10-26 22:24:12 +01:00
johan12345
8f49b1f238 update dependencies 2025-10-26 22:18:00 +01:00
Hosted Weblate
31bd2b7dd4 Translated using Weblate (Swedish)
Currently translated at 99.7% (373 of 374 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/sv/
Translation: EVMap/Android
2025-10-26 21:32:10 +01:00
Hosted Weblate
5524d14562 Translated using Weblate (Italian)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/it/
Translation: EVMap/Android
2025-10-26 21:32:10 +01:00
johan12345
5a360a7ee0 remove Chargeprice API key 2025-10-26 21:30:20 +01:00
johan12345
98d3c91686 remove Chargeprice tests 2025-10-26 21:30:20 +01:00
johan12345
12c1c6a5ec add dialog to explain removal of Chargeprice data 2025-10-26 21:30:20 +01:00
johan12345
21e23efb50 remove native Chargeprice integration
fixes #320
2025-10-26 21:30:20 +01:00
johan12345
f6f2b15f41 OSM: parse output power without units
assume kW if the number is <1000, W otherwise
#393
2025-10-07 21:05:32 +02:00
Hosted Weblate
c3776758b3 Translated using Weblate (Swedish)
Currently translated at 99.7% (373 of 374 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (373 of 374 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/sv/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/sv/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/sv/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
2025-10-07 20:48:03 +02:00
Hosted Weblate
6d9e34667c Translated using Weblate (Czech)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2025-10-07 20:48:02 +02:00
Hosted Weblate
24b94a055e Translated using Weblate (Estonian)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/et/
Translation: EVMap/Android
2025-10-07 20:48:02 +02:00
Robert Högberg
1d2a7e4af9 Nobil: Adapt to dropped support for Denmark, Finland and Iceland 2025-10-07 20:40:18 +02:00
johan12345
fa86c7c15a fix typo 2025-09-26 18:22:27 +02:00
Robert Högberg
4cd9872d0f Fix connector name for Type 1 connectors in EnBW availability data 2025-09-26 18:19:08 +02:00
johan12345
1e78ffce7e Update Nobil description: Only Sweden and Norway supported
https://info.nobil.no/nyheter/264-important-update-on-nobil-data-nobil-will-no-longer-support-data-from-denmark-finland-and-iceland
2025-09-26 18:16:40 +02:00
johan12345
3eaa97ea4f MapSurfaceCallback: workaround for coordinnate offset on Renault 5 2025-09-24 21:31:29 +02:00
Hosted Weblate
adaf2f0c87 Translated using Weblate (Czech)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2025-09-24 21:08:59 +02:00
Hosted Weblate
5802526d14 Translated using Weblate (Estonian)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/et/
Translation: EVMap/Android
2025-09-24 21:08:56 +02:00
johan12345
fe731f71e8 update LocaleConfigX 2025-09-24 21:08:37 +02:00
johan12345
c22173b79e TeslaGuestApi: nullability fix 2025-09-24 21:08:37 +02:00
Robert Högberg
82a5730aed Enable NewMotion availability checks for Nobil 2025-09-23 21:51:02 +02:00
johan12345
3386092bf8 Revert "update AGP"
This reverts commit abf9165602.
2025-09-21 22:45:00 +02:00
johan12345
1318126780 Release 2.0.1 2025-09-21 22:35:04 +02:00
johan12345
abf9165602 update AGP 2025-09-21 22:35:04 +02:00
johan12345
2c35df6360 fix #390 2025-09-21 22:23:21 +02:00
johan12345
4ed046df7a trigger website update after release 2025-09-21 17:34:56 +02:00
86 changed files with 1539 additions and 3488 deletions

View File

@@ -119,3 +119,13 @@ jobs:
asset_path: licenses_fossNormalRelease_appning.csv
asset_name: licenses_fossNormalRelease_appning.csv
asset_content_type: text/csv
- name: Trigger Website update
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/ev-map/ev-map.github.io/dispatches \
-d "{\"event_type\": \"trigger-workflow\"}"

View File

@@ -75,8 +75,10 @@ jobs:
run: |
checksec --output=json --dir=lib > checksec_output.json
jq --argjson exceptions '[
"lib/arm64-v8a/libc++_shared.so",
"lib/armeabi-v7a/libc++_shared.so",
"lib/x86/libc++_shared.so"
"lib/x86/libc++_shared.so",
"lib/x86_64/libc++_shared.so"
]' '
to_entries
| map(select(.value.fortify_source == "no" and (.key as $lib | $exceptions | index($lib) | not)))

View File

@@ -5,23 +5,28 @@ GEM
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)
aws-partitions (1.354.0)
aws-sdk-core (3.104.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.36.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.78.0)
aws-sdk-core (~> 3, >= 3.104.3)
aws-eventstream (1.4.0)
aws-partitions (1.1196.0)
aws-sdk-core (3.240.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.208.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.3)
base64 (0.3.0)
bigdecimal (4.0.1)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
@@ -113,9 +118,10 @@ GEM
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
jmespath (1.6.2)
json (2.3.1)
jwt (2.2.1)
logger (1.7.0)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)

View File

@@ -20,7 +20,6 @@ Features
- Search for places
- Advanced filtering options, including saved filter profiles
- Favorites list, also with availability information
- Integrated price comparison using [Chargeprice.app](https://chargeprice.app) (only in Europe)
- Android Auto & Android Automotive OS integration
- No ads, fully open source
- Compatible with Android 5.0 and above
@@ -90,9 +89,5 @@ information on the [Donate page](https://ev-map.app/donate/) on the EVMap websit
Since May 2024, **JawgMaps** provides their OpenStreetMap vector map tiles service to EVMap for
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="38"/></a><br>
Since April 2021, **Chargeprice.app** provide their price comparison API at a greatly reduced
price for EVMap. This data is used in EVMap's price comparison feature.
<a href="https://www.jetbrains.com/community/opensource/"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains logo" height="38"/></a><br>
As part of its support program for Open-source projects, **JetBrains** supports the development of EVMap since December 2023 with a license of their software suite.

View File

@@ -4,9 +4,9 @@
<string name="jawg_key" translatable="false">ci</string>
<string name="arcgis_key" translatable="false">ci</string>
<string name="goingelectric_key" translatable="false">ci</string>
<string name="chargeprice_key" translatable="false">ci</string>
<string name="openchargemap_key" translatable="false">ci</string>
<string name="nobil_key" translatable="false">ci</string>
<string name="fronyx_key" translatable="false">ci</string>
<string name="acra_credentials" translatable="false">ci:ci</string>
<string name="evmap_key" translatable="false">ci</string>
</resources>

View File

@@ -18,11 +18,11 @@ android {
defaultConfig {
applicationId = "net.vonforst.evmap"
compileSdk = 36
minSdk = 21
minSdk = 23
targetSdk = 36
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 262
versionName = "2.0.0"
versionCode = 268
versionName = "2.0.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -129,6 +129,17 @@ android {
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all {
var evmapKey =
System.getenv("EVMAP_API_KEY") ?: project.findProperty("EVMAP_API_KEY")?.toString()
if (evmapKey == null && project.hasProperty("EVMAP_API_KEY_ENCRYPTED")) {
evmapKey = decode(
project.findProperty("EVMAP_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (evmapKey != null) {
resValue("string", "evmap_key", evmapKey)
}
val goingelectricKey =
System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY")
?.toString()
@@ -197,18 +208,6 @@ android {
if (arcgisKey != null) {
resValue("string", "arcgis_key", jawgKey)
}
var chargepriceKey =
System.getenv("CHARGEPRICE_API_KEY") ?: project.findProperty("CHARGEPRICE_API_KEY")
?.toString()
if (chargepriceKey == null && project.hasProperty("CHARGEPRICE_API_KEY_ENCRYPTED")) {
chargepriceKey = decode(
project.findProperty("CHARGEPRICE_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (chargepriceKey != null) {
resValue("string", "chargeprice_key", chargepriceKey)
}
var fronyxKey =
System.getenv("FRONYX_API_KEY") ?: project.findProperty("FRONYX_API_KEY")?.toString()
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
@@ -299,19 +298,19 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("androidx.core:core-splashscreen:1.2.0")
implementation("androidx.activity:activity-ktx:1.11.0")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.13.0-rc01")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.browser:browser:1.9.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.viewpager2:viewpager2:1.1.0")
implementation("androidx.security:security-crypto:1.1.0")
implementation("androidx.work:work-runtime-ktx:2.10.3")
implementation("androidx.work:work-runtime-ktx:2.10.5")
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
@@ -324,11 +323,11 @@ dependencies {
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
implementation("com.airbnb.android:lottie:6.6.7")
implementation("com.airbnb.android:lottie:6.6.10")
implementation("io.michaelrocks.bimap:bimap:1.1.0")
implementation("com.github.pengrad:mapscaleview:1.6.0")
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
implementation("com.github.ev-map:locale-config-x:c97ce250b9")
implementation("com.github.ev-map:locale-config-x:58b036abf4")
// Android Auto
val carAppVersion = "1.7.0"
@@ -337,7 +336,7 @@ dependencies {
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
// AnyMaps
val anyMapsVersion = "1174ef9375"
val anyMapsVersion = "65e06c4c9a"
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
googleImplementation("com.google.android.gms:play-services-maps:19.2.0")
@@ -370,13 +369,7 @@ dependencies {
implementation("androidx.room:room-runtime:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
implementation("com.github.anboralabs:spatia-room:0.3.0") {
exclude("com.github.dalgarins", "android-spatialite")
}
// forked version with upgraded sqlite & libxml & 16 KB page size support
// https://github.com/dalgarins/android-spatialite/pull/11
// https://github.com/dalgarins/android-spatialite/pull/12
implementation("io.github.ev-map:android-spatialite:2.2.1-alpha")
implementation("com.github.anboralabs:spatia-room:1.0.1")
// billing library
val billingVersion = "7.0.0"
@@ -397,7 +390,7 @@ dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
//noinspection GradleDependency
testImplementation("org.robolectric:robolectric:4.16-beta-1")
testImplementation("org.robolectric:robolectric:4.16")
testImplementation("androidx.test:core:1.7.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("androidx.car.app:app-testing:$carAppVersion")

View File

@@ -0,0 +1,938 @@
{
"formatVersion": 1,
"database": {
"version": 28,
"identityHash": "84f71cce385c444726ba336834ddf6b4",
"entities": [
{
"tableName": "ChargeLocation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `accessibility` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `coordinatesProjected` BLOB NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "coordinates",
"columnName": "coordinates",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "chargepoints",
"columnName": "chargepoints",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "network",
"columnName": "network",
"affinity": "TEXT"
},
{
"fieldPath": "dataSourceUrl",
"columnName": "dataSourceUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT"
},
{
"fieldPath": "editUrl",
"columnName": "editUrl",
"affinity": "TEXT"
},
{
"fieldPath": "verified",
"columnName": "verified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "barrierFree",
"columnName": "barrierFree",
"affinity": "INTEGER"
},
{
"fieldPath": "operator",
"columnName": "operator",
"affinity": "TEXT"
},
{
"fieldPath": "generalInformation",
"columnName": "generalInformation",
"affinity": "TEXT"
},
{
"fieldPath": "amenities",
"columnName": "amenities",
"affinity": "TEXT"
},
{
"fieldPath": "locationDescription",
"columnName": "locationDescription",
"affinity": "TEXT"
},
{
"fieldPath": "photos",
"columnName": "photos",
"affinity": "TEXT"
},
{
"fieldPath": "chargecards",
"columnName": "chargecards",
"affinity": "TEXT"
},
{
"fieldPath": "accessibility",
"columnName": "accessibility",
"affinity": "TEXT"
},
{
"fieldPath": "license",
"columnName": "license",
"affinity": "TEXT"
},
{
"fieldPath": "networkUrl",
"columnName": "networkUrl",
"affinity": "TEXT"
},
{
"fieldPath": "chargerUrl",
"columnName": "chargerUrl",
"affinity": "TEXT"
},
{
"fieldPath": "timeRetrieved",
"columnName": "timeRetrieved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "coordinatesProjected",
"columnName": "coordinatesProjected",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "address.city",
"columnName": "city",
"affinity": "TEXT"
},
{
"fieldPath": "address.country",
"columnName": "country",
"affinity": "TEXT"
},
{
"fieldPath": "address.postcode",
"columnName": "postcode",
"affinity": "TEXT"
},
{
"fieldPath": "address.street",
"columnName": "street",
"affinity": "TEXT"
},
{
"fieldPath": "faultReport.created",
"columnName": "fault_report_created",
"affinity": "INTEGER"
},
{
"fieldPath": "faultReport.description",
"columnName": "fault_report_description",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.twentyfourSeven",
"columnName": "twentyfourSeven",
"affinity": "INTEGER"
},
{
"fieldPath": "openinghours.description",
"columnName": "description",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.monday.start",
"columnName": "mostart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.monday.end",
"columnName": "moend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.tuesday.start",
"columnName": "tustart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.tuesday.end",
"columnName": "tuend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.wednesday.start",
"columnName": "westart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.wednesday.end",
"columnName": "weend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.thursday.start",
"columnName": "thstart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.thursday.end",
"columnName": "thend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.friday.start",
"columnName": "frstart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.friday.end",
"columnName": "frend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.saturday.start",
"columnName": "sastart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.saturday.end",
"columnName": "saend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.sunday.start",
"columnName": "sustart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.sunday.end",
"columnName": "suend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.holiday.start",
"columnName": "hostart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.holiday.end",
"columnName": "hoend",
"affinity": "TEXT"
},
{
"fieldPath": "cost.freecharging",
"columnName": "freecharging",
"affinity": "INTEGER"
},
{
"fieldPath": "cost.freeparking",
"columnName": "freeparking",
"affinity": "INTEGER"
},
{
"fieldPath": "cost.descriptionShort",
"columnName": "descriptionShort",
"affinity": "TEXT"
},
{
"fieldPath": "cost.descriptionLong",
"columnName": "descriptionLong",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.country",
"columnName": "chargepricecountry",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.network",
"columnName": "chargepricenetwork",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.plugTypes",
"columnName": "chargepriceplugTypes",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
}
},
{
"tableName": "Favorite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "favoriteId",
"columnName": "favoriteId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargerId",
"columnName": "chargerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargerDataSource",
"columnName": "chargerDataSource",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"favoriteId"
]
},
"indices": [
{
"name": "index_Favorite_chargerId_chargerDataSource",
"unique": false,
"columnNames": [
"chargerId",
"chargerDataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
}
],
"foreignKeys": [
{
"table": "ChargeLocation",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"chargerId",
"chargerDataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "BooleanFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_BooleanFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "MultipleChoiceFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "values",
"columnName": "values",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "all",
"columnName": "all",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "SliderFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_SliderFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "FilterProfile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"dataSource",
"id"
]
},
"indices": [
{
"name": "index_FilterProfile_dataSource_name",
"unique": true,
"columnNames": [
"dataSource",
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
}
]
},
{
"tableName": "RecentAutocompletePlace",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primaryText",
"columnName": "primaryText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secondaryText",
"columnName": "secondaryText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "latLng",
"columnName": "latLng",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "viewport",
"columnName": "viewport",
"affinity": "TEXT"
},
{
"fieldPath": "types",
"columnName": "types",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
}
},
{
"tableName": "GEPlug",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "GENetwork",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "GEChargeCard",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMConnectionType",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formalName",
"columnName": "formalName",
"affinity": "TEXT"
},
{
"fieldPath": "discontinued",
"columnName": "discontinued",
"affinity": "INTEGER"
},
{
"fieldPath": "obsolete",
"columnName": "obsolete",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMCountry",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isoCode",
"columnName": "isoCode",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "continentCode",
"columnName": "continentCode",
"affinity": "TEXT"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMOperator",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "websiteUrl",
"columnName": "websiteUrl",
"affinity": "TEXT"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contactEmail",
"columnName": "contactEmail",
"affinity": "TEXT"
},
{
"fieldPath": "contactTelephone1",
"columnName": "contactTelephone1",
"affinity": "TEXT"
},
{
"fieldPath": "contactTelephone2",
"columnName": "contactTelephone2",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OSMNetwork",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "SavedRegion",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "region",
"columnName": "region",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeRetrieved",
"columnName": "timeRetrieved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filters",
"columnName": "filters",
"affinity": "TEXT"
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_SavedRegion_filters_dataSource",
"unique": false,
"columnNames": [
"filters",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84f71cce385c444726ba336834ddf6b4')"
]
}
}

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="chargeprice_api_url">https://staging-api.chargeprice.app/v1/</string>
<string name="chargeprice_key">20c0d68918c9dc96c564784b711a6570</string>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EVMap</string>
<string name="app_name">EVMap (debug)</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Tycker du att EVMap är praktisk? Stöd utvecklingen genom att skicka en donation till utvecklaren.</string>
<string name="donations_info" formatted="false">Har du nytta av EVMap? Stöd utvecklingen genom att skicka en donation till utvecklaren.</string>
<string name="donate_paypal">Donera med PayPal</string>
<string name="data_sources_hint">Kartdata i appen tillhandahålls av OpenStreetMap.</string>
</resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Tycker du att EVMap är praktisk? Stöd utvecklingen genom att skicka en donation till utvecklaren.\n\nGoogle tar 15% av alla donationer.</string>
<string name="donations_info" formatted="false">Har du nytta av EVMap? Stöd utvecklingen genom att skicka en donation till utvecklaren.\n\nGoogle tar 15% av alla donationer.</string>
<string name="data_sources_hint">I inställningarna kan du välja mellan Google Maps och OpenStreetMap som kartleverantör.</string>
</resources>

View File

@@ -1,27 +1,16 @@
package net.vonforst.evmap.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import net.vonforst.evmap.BR
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.chargeprice.ChargePrice
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
import net.vonforst.evmap.databinding.ItemChargepriceBinding
import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.ui.CheckableConstraintLayout
import java.time.Instant
interface Equatable {
@@ -106,141 +95,4 @@ class ConnectorDetailsAdapter : DataBindingAdapter<ConnectorDetailsAdapter.Conne
Equatable
override fun getItemViewType(position: Int): Int = R.layout.dialog_connector_details_item
}
class ChargepriceAdapter :
DataBindingAdapter<ChargePrice>() {
val viewPool = RecyclerView.RecycledViewPool()
var meta: ChargepriceChargepointMeta? = null
set(value) {
field = value
notifyDataSetChanged()
}
var myTariffs: Set<String>? = null
set(value) {
field = value
notifyDataSetChanged()
}
var myTariffsAll: Boolean? = null
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<ChargePrice> {
val holder = super.onCreateViewHolder(parent, viewType)
val binding = holder.binding as ItemChargepriceBinding
binding.rvTags.apply {
adapter = ChargepriceTagsAdapter()
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
recycleChildrenOnDetach = true
}
itemAnimator = null
setRecycledViewPool(viewPool)
}
return holder
}
override fun bind(holder: ViewHolder<ChargePrice>, item: ChargePrice) {
super.bind(holder, item)
(holder.binding as ItemChargepriceBinding).apply {
this.meta = this@ChargepriceAdapter.meta
this.myTariffs = this@ChargepriceAdapter.myTariffs
this.myTariffsAll = this@ChargepriceAdapter.myTariffsAll
}
}
}
class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
private var checkedItem: Int? = 0
var enabledConnectors: List<String>? = null
get() = field
set(value) {
field = value
checkedItem?.let {
if (value != null && getItem(it).type !in value) {
checkedItem = currentList.indexOfFirst {
it.type in value
}.takeIf { it != -1 }
onCheckedItemChangedListener?.invoke(getCheckedItem())
}
}
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int = R.layout.item_connector_button
override fun onBindViewHolder(holder: ViewHolder<Chargepoint>, position: Int) {
val item = getItem(position)
super.bind(holder, item)
val binding = holder.binding as ItemConnectorButtonBinding
binding.enabled = enabledConnectors?.let { item.type in it } ?: true
val root = binding.root as CheckableConstraintLayout
root.setOnCheckedChangeListener { _, _ -> }
root.isChecked = checkedItem == position
root.setOnClickListener {
root.isChecked = true
}
root.setOnCheckedChangeListener { _, checked: Boolean ->
if (checked) {
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
root.post {
notifyDataSetChanged()
}
getCheckedItem()?.let { onCheckedItemChangedListener?.invoke(it) }
}
}
}
fun getCheckedItem(): Chargepoint? = checkedItem?.let { getItem(it) }
fun setCheckedItem(item: Chargepoint?) {
checkedItem = item?.let { currentList.indexOf(item) }.takeIf { it != -1 }
}
var onCheckedItemChangedListener: ((Chargepoint?) -> Unit)? = null
}
class ChargepriceTagsAdapter() :
DataBindingAdapter<ChargepriceTag>() {
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag
}
class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
private var checkedItem: ChargepriceCar? = null
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_vehicle_chip
override fun onBindViewHolder(holder: ViewHolder<ChargepriceCar>, position: Int) {
val item = getItem(position)
super.bind(holder, item)
val binding = holder.binding as ItemChargepriceVehicleChipBinding
val root = binding.root as Chip
root.isChecked = checkedItem == item
root.setOnClickListener {
root.isChecked = true
}
root.setOnCheckedChangeListener { _, checked: Boolean ->
if (checked && item != checkedItem) {
checkedItem = item
root.post {
notifyDataSetChanged()
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
}
}
}
}
fun getCheckedItem(): ChargepriceCar? = checkedItem
fun setCheckedItem(item: ChargepriceCar?) {
checkedItem = item
}
var onCheckedItemChangedListener: ((ChargepriceCar?) -> Unit)? = null
}

View File

@@ -175,6 +175,7 @@ class AvailabilityRepository(context: Context) {
RheinenergieAvailabilityDetector(okhttp),
teslaOwnerAvailabilityDetector,
TeslaGuestAvailabilityDetector(okhttp),
NobilAvailabilityDetector(okhttp, context),
EnBwAvailabilityDetector(okhttp),
NewMotionAvailabilityDetector(okhttp)
)

View File

@@ -203,7 +203,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
"Typ 3A" -> Chargepoint.TYPE_3A
"Typ 3C \"Scame\"" -> Chargepoint.TYPE_3C
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
"Typ 1" -> Chargepoint.TYPE_1
"Typ 1 Steckdose" -> Chargepoint.TYPE_1
"Steckdose(D)" -> Chargepoint.SCHUKO
"CCS (Typ 1)" -> Chargepoint.CCS_TYPE_1 // US CCS, aka type1_combo
"CCS (Typ 2)" -> Chargepoint.CCS_TYPE_2 // EU CCS, aka type2_combo

View File

@@ -61,8 +61,9 @@ interface NewMotionApi {
)
@JsonClass(generateAdapter = true)
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int) {
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int, val maxElectricPower: Double?) {
fun getPower(): Double {
maxElectricPower?.let { return it }
val phases = when (powerType) {
"AC1Phase" -> 1
"AC3Phase" -> 3
@@ -220,6 +221,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
// NewMotion is our fallback
return when (charger.dataSource) {
"goingelectric" -> charger.network != "Tesla Supercharger"
"nobil" -> charger.network != "Tesla"
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
"openstreetmap" -> charger.operator !in listOf("Tesla, Inc.", "Tesla")
else -> false

View File

@@ -0,0 +1,109 @@
package net.vonforst.evmap.api.availability
import android.content.Context
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import net.vonforst.evmap.R
import net.vonforst.evmap.model.ChargeLocation
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import java.time.Instant
internal class InstantStringAdapter {
@FromJson
fun fromJson(value: String?): Instant? = value?.let {
Instant.parse(value)
}
@ToJson
fun toJson(value: Instant?): String? = value?.toString()
}
interface NobilRealtimeApi {
@GET("{nobilId}")
suspend fun getAvailability(
@Path("nobilId") nobilId: String,
@Header("X-Api-Key") apiKey: String
): List<NobilChargepointState>
companion object {
fun create(client: OkHttpClient): NobilRealtimeApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.ev-map.app/nobil/api/realtime/")
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder().add(InstantStringAdapter()).build()
)
)
.client(client)
.build()
return retrofit.create(NobilRealtimeApi::class.java)
}
}
}
@JsonClass(generateAdapter = true)
data class NobilChargepointState(
val evseUid: String,
val status: String,
val timestamp: Instant
)
class NobilAvailabilityDetector(client: OkHttpClient, context: Context) :
BaseAvailabilityDetector(client) {
val api = NobilRealtimeApi.create(client)
val apiKey = context.getString(R.string.evmap_key)
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
val nobilId = when (location.address?.country) {
"Norway" -> "NOR"
"Sweden" -> "SWE"
else -> throw AvailabilityDetectorException("nobil: unsupported country")
} + "_%05d".format(location.id)
val availability = api.getAvailability(nobilId, apiKey)
if (availability.isEmpty()) {
throw AvailabilityDetectorException("nobil: no real-time data available")
}
return ChargeLocationStatus(
location.chargepointsMerged.associateWith { cp ->
cp.evseUIds!!.map { evseUId ->
when (availability.find { it.evseUid == evseUId }?.status) {
"AVAILABLE" -> ChargepointStatus.AVAILABLE
"BLOCKED" -> ChargepointStatus.OCCUPIED
"CHARGING" -> ChargepointStatus.CHARGING
"INOPERATIVE" -> ChargepointStatus.FAULTED
"OUTOFORDER" -> ChargepointStatus.FAULTED
"PLANNED" -> ChargepointStatus.FAULTED
"REMOVED" -> ChargepointStatus.FAULTED
"RESERVED" -> ChargepointStatus.OCCUPIED
"UNKNOWN" -> ChargepointStatus.UNKNOWN
else -> ChargepointStatus.UNKNOWN
}
}
},
"Nobil",
location.chargepointsMerged.associateWith { cp ->
if (cp.evseIds != null) cp.evseIds.map { it ?: "??" } else listOf()
},
lastChange = location.chargepointsMerged.associateWith { cp ->
cp.evseUIds!!.map { evseUId ->
availability.find { it.evseUid == evseUId }?.timestamp
}
}
)
}
override fun isChargerSupported(charger: ChargeLocation): Boolean {
return when (charger.dataSource) {
"nobil" -> charger.chargepoints.any { it.evseUIds?.isNotEmpty() == true }
else -> false
}
}
}

View File

@@ -115,7 +115,7 @@ interface TeslaChargingGuestGraphQlApi {
val activeOutages: List<Outage>?,
val chargerList: List<ChargerDetail>,
val trtId: Long,
val maxPowerKw: Int,
val maxPowerKw: Int?,
val name: String,
val pricing: Pricing?,
val publicStallCount: Int

View File

@@ -1,99 +1,16 @@
package net.vonforst.evmap.api.chargeprice
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import jsonapi.Document
import jsonapi.JsonApiFactory
import jsonapi.retrofit.DocumentConverterFactory
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.model.ChargeLocation
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import java.util.*
import java.util.Locale
interface ChargepriceApi {
@POST("charge_prices")
suspend fun getChargePrices(
@Body @jsonapi.retrofit.Document request: ChargepriceRequest,
@Header("Accept-Language") language: String
): Document<List<ChargePrice>>
@GET("vehicles")
@jsonapi.retrofit.Document
suspend fun getVehicles(): List<ChargepriceCar>
@GET("tariffs")
@jsonapi.retrofit.Document
suspend fun getTariffs(): List<ChargepriceTariff>
@POST("user_feedback")
suspend fun userFeedback(@Body @jsonapi.retrofit.Document feedback: ChargepriceUserFeedback)
companion object {
private val cacheSize = 1L * 1024 * 1024 // 1MB
val supportedLanguages = setOf("de", "en", "fr", "nl")
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
private val jsonApiAdapterFactory = JsonApiFactory.Builder()
.addType(ChargepriceRequest::class.java)
.addType(ChargepriceTariff::class.java)
.addType(ChargepriceBrand::class.java)
.addType(ChargePrice::class.java)
.addType(ChargepriceCar::class.java)
.build()
val moshi = Moshi.Builder()
.add(jsonApiAdapterFactory)
.add(
PolymorphicJsonAdapterFactory.of(ChargepriceUserFeedback::class.java, "type")
.withSubtype(ChargepriceMissingPriceFeedback::class.java, "missing_price")
.withSubtype(ChargepriceWrongPriceFeedback::class.java, "wrong_price")
.withSubtype(ChargepriceMissingVehicleFeedback::class.java, "missing_vehicle")
)
.build()
fun create(
apikey: String,
baseurl: String = "https://api.chargeprice.app/v1/",
context: Context? = null
): ChargepriceApi {
val client = OkHttpClient.Builder().apply {
addInterceptor { chain ->
// add API key to every request
val original = chain.request()
val new = original.newBuilder()
.header("API-Key", apikey)
.header("Content-Type", "application/json")
.build()
chain.proceed(new)
}
if (BuildConfig.DEBUG) {
addDebugInterceptors()
}
if (context != null) {
cache(Cache(context.cacheDir, cacheSize))
}
}.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseurl)
.addConverterFactory(DocumentConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(client)
.build()
return retrofit.create(ChargepriceApi::class.java)
}
fun getChargepriceLanguage(): String {
val locale = Locale.getDefault().language
return if (supportedLanguages.contains(locale)) {

View File

@@ -1,466 +0,0 @@
package net.vonforst.evmap.api.chargeprice
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.Patterns
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import jsonapi.*
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.ui.currency
import kotlin.math.ceil
import kotlin.math.floor
@Resource("charge_price_request")
@JsonClass(generateAdapter = true)
data class ChargepriceRequest(
@Json(name = "data_adapter")
val dataAdapter: String,
val station: ChargepriceStation,
val options: ChargepriceOptions,
@ToMany("tariffs")
val tariffs: List<ChargepriceTariff>? = null,
@ToOne("vehicle")
val vehicle: ChargepriceCar? = null,
@RelationshipsObject var relationships: Relationships? = null
)
@JsonClass(generateAdapter = true)
data class ChargepriceStation(
val longitude: Double,
val latitude: Double,
val country: String?,
val network: String?,
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepoint>
) {
companion object {
fun fromEvmap(
charger: ChargeLocation,
compatibleConnectors: List<String>,
): ChargepriceStation {
if (charger.chargepriceData == null) throw IllegalArgumentException()
val plugTypes =
charger.chargepriceData.plugTypes ?: charger.chargepoints.map { it.type }
return ChargepriceStation(
charger.coordinates.lng,
charger.coordinates.lat,
charger.chargepriceData.country,
charger.chargepriceData.network,
charger.chargepoints.zip(plugTypes)
.filter { equivalentPlugTypes(it.first.type).any { it in compatibleConnectors } }
.map { ChargepriceChargepoint(it.first.power ?: 0.0, it.second) }
)
}
}
}
@JsonClass(generateAdapter = true)
data class ChargepriceChargepoint(
val power: Double,
val plug: String
)
@JsonClass(generateAdapter = true)
data class ChargepriceOptions(
@Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null,
val energy: Double? = null,
val duration: Int? = null,
@Json(name = "battery_range") val batteryRange: List<Double>? = null,
@Json(name = "car_ac_phases") val carAcPhases: Int? = null,
val currency: String? = null,
@Json(name = "start_time") val startTime: Int? = null,
@Json(name = "allow_unbalanced_load") val allowUnbalancedLoad: Boolean? = null,
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null,
@Json(name = "show_price_unavailable") val showPriceUnavailable: Boolean? = null,
@Json(name = "show_all_brand_restricted_tariffs") val showAllBrandRestrictedTariffs: Boolean? = null
)
@Resource("tariff")
@Parcelize
@JsonClass(generateAdapter = true)
data class ChargepriceTariff(
@Id val id_: String?,
val provider: String,
val name: String,
@Json(name = "direct_payment")
val directPayment: Boolean = false,
@Json(name = "provider_customer_tariff")
val providerCustomerTariff: Boolean = false,
@Json(name = "supported_countries")
val supportedCountries: Set<String>,
@Json(name = "charge_card_id")
val chargeCardId: String?, // GE charge card ID
) : Parcelable {
val id: String
get() = id_!!
}
@JsonClass(generateAdapter = true)
@Resource("car")
@Parcelize
data class ChargepriceCar(
@Id val id_: String?,
val name: String,
val brand: String,
@Json(name = "dc_charge_ports")
val dcChargePorts: List<String>,
@Json(name = "usable_battery_size")
val usableBatterySize: Float,
@Json(name = "ac_max_power")
val acMaxPower: Float,
@Json(name = "dc_max_power")
val dcMaxPower: Float?
) : Equatable, Parcelable {
fun formatSpecs(): String = buildString {
append("%.0f kWh".format(usableBatterySize))
append(" | ")
append("AC %.0f kW".format(acMaxPower))
dcMaxPower?.let {
append(" | ")
append("DC %.0f kW".format(it))
}
}
companion object {
private val acConnectors = listOf(
Chargepoint.CEE_BLAU,
Chargepoint.CEE_ROT,
Chargepoint.SCHUKO,
Chargepoint.TYPE_1,
Chargepoint.TYPE_2_UNKNOWN,
Chargepoint.TYPE_2_SOCKET,
Chargepoint.TYPE_2_PLUG
)
private val plugMapping = mapOf(
"ccs" to Chargepoint.CCS_UNKNOWN,
"tesla_suc" to Chargepoint.SUPERCHARGER,
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
"chademo" to Chargepoint.CHADEMO
)
}
val id: String
get() = id_!!
val compatibleEvmapConnectors: List<String>
get() = dcChargePorts.mapNotNull {
plugMapping[it]
}.plus(acConnectors)
}
@JsonClass(generateAdapter = true)
@Resource("brand")
@Parcelize
data class ChargepriceBrand(
@Id val id: String?
) : Parcelable
@JsonClass(generateAdapter = true)
@Resource("charge_price")
@Parcelize
data class ChargePrice(
val provider: String,
@Json(name = "tariff_name")
val tariffName: String,
val url: String,
@Json(name = "monthly_min_sales")
val monthlyMinSales: Double = 0.0,
@Json(name = "total_monthly_fee")
val totalMonthlyFee: Double = 0.0,
@Json(name = "flat_rate")
val flatRate: Boolean = false,
@Json(name = "direct_payment")
val directPayment: Boolean = false,
@Json(name = "provider_customer_tariff")
val providerCustomerTariff: Boolean = false,
val currency: String,
@Json(name = "start_time")
val startTime: Int = 0,
val tags: List<ChargepriceTag>,
@Json(name = "charge_point_prices")
val chargepointPrices: List<ChargepointPrice>,
@Json(name = "branding")
val branding: ChargepriceBranding? = null,
@RelationshipsObject
val relationships: @WriteWith<RelationshipsParceler>() Relationships? = null,
) : Equatable, Cloneable, Parcelable {
val tariffId: String?
get() = (relationships?.get("tariff") as? Relationship.ToOne)?.data?.id
fun formatMonthlyFees(ctx: Context): String {
return listOfNotNull(
if (totalMonthlyFee > 0) {
ctx.getString(R.string.chargeprice_base_fee, totalMonthlyFee, currency(currency))
} else null,
if (monthlyMinSales > 0) {
ctx.getString(R.string.chargeprice_min_spend, monthlyMinSales, currency(currency))
} else null
).joinToString(", ")
}
}
/**
* Parceler implementation for the Relationships object.
* Note that this ignores certain fields that we don't need (links, meta, etc.)
*/
internal object RelationshipsParceler : Parceler<Relationships?> {
override fun create(parcel: Parcel): Relationships? {
if (parcel.readInt() == 0) return null
val nMembers = parcel.readInt()
val members = (0 until nMembers).associate { _ ->
val key = parcel.readString()!!
val value = if (parcel.readInt() == 0) {
val type = parcel.readString()
val id = parcel.readString()
val ri = if (type != null && id != null) {
ResourceIdentifier(type, id)
} else null
Relationship.ToOne(ri)
} else {
val size = parcel.readInt()
val ris = (0 until size).map { _ ->
val type = parcel.readString()!!
val id = parcel.readString()!!
ResourceIdentifier(type, id)
}
Relationship.ToMany(ris)
}
key to value
}
return Relationships(members)
}
override fun Relationships?.write(parcel: Parcel, flags: Int) {
if (this == null) {
parcel.writeInt(0)
return
} else {
parcel.writeInt(1)
}
parcel.writeInt(members.size)
for (member in this.members) {
parcel.writeString(member.key)
when (val value = member.value) {
is Relationship.ToOne -> {
parcel.writeInt(0)
parcel.writeString(value.data?.type)
parcel.writeString(value.data?.id)
}
is Relationship.ToMany -> {
parcel.writeInt(1)
parcel.writeInt(value.data.size)
for (ri in value.data) {
parcel.writeString(ri.type)
parcel.writeString(ri.id)
}
}
}
}
}
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepointPrice(
val power: Double,
val plug: String,
val price: Double?,
@Json(name = "price_distribution") val priceDistribution: PriceDistribution,
@Json(name = "blocking_fee_start") val blockingFeeStart: Int?,
@Json(name = "no_price_reason") var noPriceReason: String?
) : Parcelable {
fun formatDistribution(ctx: Context): String {
fun percent(value: Double): String {
return ctx.getString(R.string.percent_format, value * 100) + "\u00a0"
}
fun time(value: Int): String {
val h = floor(value.toDouble() / 60).toInt()
val min = ceil(value.toDouble() % 60).toInt()
return if (h == 0 && min > 0) "${min}min";
// be slightly sloppy (3:01 is shown as 3h) to save space
else if (h > 0 && (min == 0 || min == 1)) "${h}h";
else "%d:%02dh".format(h, min)
}
// based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js
with(priceDistribution) {
return listOfNotNull(
if (session != null && session > 0.0) {
(if (session < 1) percent(session) else "") + ctx.getString(R.string.chargeprice_session_fee)
} else null,
if (kwh != null && kwh > 0.0 && !isOnlyKwh) {
(if (kwh < 1) percent(kwh) else "") + ctx.getString(R.string.chargeprice_per_kwh)
} else null,
if (minute != null && minute > 0.0) {
(if (minute < 1) percent(minute) else "") + ctx.getString(R.string.chargeprice_per_minute) +
if (blockingFeeStart != null) {
" (${
ctx.getString(
R.string.chargeprice_blocking_fee,
time(blockingFeeStart)
)
})"
} else ""
} else null,
if ((minute == null || minute == 0.0) && blockingFeeStart != null) {
ctx.getString(R.string.chargeprice_blocking_fee, time(blockingFeeStart))
} else null
).joinToString(" +\u00a0")
}
}
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepriceBranding(
@Json(name = "background_color") val backgroundColor: String,
@Json(name = "text_color") val textColor: String,
@Json(name = "logo_url") val logoUrl: String
) : Parcelable
@JsonClass(generateAdapter = true)
@Parcelize
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) :
Parcelable {
val isOnlyKwh
get() = kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable,
Parcelable
@JsonClass(generateAdapter = true)
data class ChargepriceMeta(
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
)
enum class ChargepriceInclude {
@Json(name = "filter")
FILTER,
@Json(name = "always")
ALWAYS,
@Json(name = "exclusive")
EXCLUSIVE
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepriceRequestTariffMeta(
val include: ChargepriceInclude
) : Parcelable
@JsonClass(generateAdapter = true)
data class ChargepriceChargepointMeta(
val power: Double,
val plug: String,
val energy: Double,
val duration: Double
)
@Resource("user_feedback")
sealed class ChargepriceUserFeedback(
val notes: String,
val email: String,
val context: String,
val language: String
) {
init {
if (email.isBlank() || email.length > 100 || !Patterns.EMAIL_ADDRESS.matcher(email)
.matches()
) {
throw IllegalArgumentException("invalid email")
}
if (!ChargepriceApi.supportedLanguages.contains(language)) {
throw IllegalArgumentException("invalid language")
}
if (context.length > 500) throw IllegalArgumentException("invalid context")
if (notes.length > 1000) throw IllegalArgumentException("invalid notes")
}
}
@JsonClass(generateAdapter = true)
@Resource(type = "missing_price")
class ChargepriceMissingPriceFeedback(
val tariff: String,
val cpo: String,
val price: String,
@Json(name = "poi_link") val poiLink: String,
notes: String,
email: String,
context: String,
language: String
) : ChargepriceUserFeedback(notes, email, context, language) {
init {
if (tariff.isBlank() || tariff.length > 100) throw IllegalArgumentException("invalid tariff")
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
if (price.isBlank() || price.length > 100) throw IllegalArgumentException("invalid price")
if (poiLink.isBlank() || poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
}
}
@JsonClass(generateAdapter = true)
@Resource(type = "wrong_price")
class ChargepriceWrongPriceFeedback(
val tariff: String,
val cpo: String,
@Json(name = "displayed_price") val displayedPrice: String,
@Json(name = "actual_price") val actualPrice: String,
@Json(name = "poi_link") val poiLink: String,
notes: String,
email: String,
context: String,
language: String,
) : ChargepriceUserFeedback(notes, email, context, language) {
init {
if (tariff.length > 100) throw IllegalArgumentException("invalid tariff")
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
if (displayedPrice.length > 100) throw IllegalArgumentException("invalid displayedPrice")
if (actualPrice.length > 100) throw IllegalArgumentException("invalid actualPrice")
if (poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
}
}
@JsonClass(generateAdapter = true)
@Resource(type = "missing_vehicle")
class ChargepriceMissingVehicleFeedback(
val brand: String,
val model: String,
notes: String,
email: String,
context: String,
language: String,
) : ChargepriceUserFeedback(notes, email, context, language) {
init {
if (brand.length > 100) throw IllegalArgumentException("invalid brand")
if (model.length > 100) throw IllegalArgumentException("invalid model")
}
}

View File

@@ -120,7 +120,7 @@ class NobilApiWrapper(
override suspend fun fullDownload(): FullDownloadResult<NobilReferenceData> {
var numTotalChargepoints = 0
arrayOf("DAN", "FIN", "ISL", "NOR", "SWE").forEach { countryCode ->
arrayOf("NOR", "SWE").forEach { countryCode ->
val request = NobilNumChargepointsRequest(apikey, countryCode)
val response = api.getNumChargepoints(request)
if (!response.isSuccessful) {

View File

@@ -125,9 +125,6 @@ data class NobilChargerStation(
Address(
chargerStationData.city,
when (chargerStationData.landCode) {
"DAN" -> "Denmark"
"FIN" -> "Finland"
"ISL" -> "Iceland"
"NOR" -> "Norway"
"SWE" -> "Sweden"
else -> ""
@@ -282,9 +279,10 @@ data class NobilChargerStation(
val connectionVoltage = if (attribs["12"]?.attrVal is String) attribs["12"]?.attrVal.toString().toDoubleOrNull() else null
val connectionCurrent = if (attribs["31"]?.attrVal is String) attribs["31"]?.attrVal.toString().toDoubleOrNull() else null
val evseUId = if (attribs["27"]?.attrVal is String) listOf(attribs["27"]?.attrVal.toString()) else null
val evseId = if (attribs["28"]?.attrVal is String) listOf(attribs["28"]?.attrVal.toString()) else null
return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId)
return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId, evseUId)
}
}
}

View File

@@ -3,7 +3,13 @@ package net.vonforst.evmap.api.openstreetmap
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.model.*
import net.vonforst.evmap.model.Address
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.model.Coordinate
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.OpeningHours
import okhttp3.internal.immutableListOf
import java.time.Instant
import java.time.ZonedDateTime
@@ -239,10 +245,24 @@ data class OSMChargingStation(
if (rawOutput == null) {
return null
}
val pattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
val matchResult = pattern.matchEntire(rawOutput) ?: return null
val numberString = matchResult.groupValues[1].replace(',', '.')
return numberString.toDoubleOrNull()
val kwPattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
kwPattern.matchEntire(rawOutput)?.let { matchResult ->
val numberString = matchResult.groupValues[1].replace(',', '.')
return numberString.toDoubleOrNull()
}
val numberPattern = Regex("([0-9.,]+)")
numberPattern.matchEntire(rawOutput)?.let { matchResult ->
// just a number is mapped without unit
val numberString = matchResult.groupValues[1].replace(',', '.')
val number = numberString.toDoubleOrNull()
return number?.let {
// assume kW if the number is < 1000, otherwise assume W and convert to kW
if (number < 1000) number else number / 1000
}
}
return null
}
}
}

View File

@@ -109,6 +109,7 @@ class CarAppService : androidx.car.app.CarAppService() {
@ExperimentalCarApi
class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver {
private val TAG = "EVMapSession"
lateinit var intent: Intent
var mapScreen: LocationAwareScreen? = null
set(value) {
field = value
@@ -132,7 +133,8 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
}
override fun onCreateScreen(intent: Intent): Screen {
val mapScreen = if (supportsNewMapScreen(carContext)) {
this.intent = intent
val mapScreen = if (supportsNewMapScreen(carContext) && prefs.androidAutoNewMapScreenEnabled) {
MapScreen(carContext, this)
} else {
LegacyMapScreen(carContext, this)

View File

@@ -1,402 +0,0 @@
package net.vonforst.evmap.auto
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.Model
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
import jsonapi.Meta
import jsonapi.Relationship
import jsonapi.Relationships
import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.ChargePrice
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import net.vonforst.evmap.ui.time
import retrofit2.HttpException
import java.io.IOException
import kotlin.math.roundToInt
@ExperimentalCarApi
class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger: ChargeLocation) :
Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
ChargepriceApi.create(
carContext.getString(R.string.chargeprice_key),
carContext.getString(R.string.chargeprice_api_url)
)
}
private var prices: List<ChargePrice>? = null
private var meta: ChargepriceChargepointMeta? = null
private var chargepoint: Chargepoint? = null
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
private var errorMessage: String? = null
private val batteryRange = prefs.chargepriceBatteryRangeAndroidAuto
override fun onGetTemplate(): Template {
if (prices == null) loadData()
return ListTemplate.Builder().apply {
setTitle(
carContext.getString(
R.string.chargeprice_battery_range,
batteryRange[0],
batteryRange[1]
) + " · " + carContext.getString(R.string.powered_by_chargeprice)
)
setHeaderAction(Action.BACK)
if (prices == null && errorMessage == null) {
setLoading(true)
} else {
val header = meta?.let { meta ->
chargepoint?.let { chargepoint ->
"${
nameForPlugType(
carContext.stringProvider(),
chargepoint.type
)
} ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
carContext.getString(
R.string.chargeprice_stats,
meta.energy,
time(meta.duration.roundToInt()),
meta.energy / meta.duration * 60
)
}"
}
}
val myTariffs = prefs.chargepriceMyTariffs
val myTariffsAll = prefs.chargepriceMyTariffsAll
val prices = prices?.take(maxRows)
if (prices != null && prices.isNotEmpty() && !myTariffsAll && myTariffs != null) {
val (myPrices, otherPrices) = prices.partition { price -> price.tariffId in myTariffs }
val myPricesList = buildPricesList(myPrices)
val otherPricesList = buildPricesList(otherPrices)
if (myPricesList.items.isNotEmpty() && otherPricesList.items.isNotEmpty()) {
addSectionedList(
SectionedItemList.create(
myPricesList,
(header?.let { it + "\n" } ?: "") +
carContext.getString(R.string.chargeprice_header_my_tariffs)
)
)
addSectionedList(
SectionedItemList.create(
otherPricesList,
carContext.getString(R.string.chargeprice_header_other_tariffs)
)
)
} else {
val list =
if (myPricesList.items.isNotEmpty()) myPricesList else otherPricesList
if (header != null) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
}
}
} else {
val list = buildPricesList(prices)
if (header != null && list.items.isNotEmpty()) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
}
}
}
setActionStrip(
ActionStrip.Builder().addAction(
Action.Builder().setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_chargeprice
)
).build()
).setOnClickListener {
openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
}.build()
).build()
)
}.build()
}
private fun buildPricesList(prices: List<ChargePrice>?): ItemList {
return ItemList.Builder().apply {
setNoItemsMessage(
errorMessage
?: carContext.getString(R.string.chargeprice_no_tariffs_found)
)
prices?.forEach { price ->
addItem(Row.Builder().apply {
setTitle(formatProvider(price))
addText(formatPrice(price))
}.build())
}
}.build()
}
private fun formatProvider(price: ChargePrice): String {
if (!price.tariffName.startsWith(price.provider)) {
return price.provider + " " + price.tariffName
} else {
return price.tariffName
}
}
private fun formatPrice(price: ChargePrice): String {
val amount = price.chargepointPrices.first().price
?: return "${carContext.getString(R.string.chargeprice_price_not_available)} (${price.chargepointPrices.first().noPriceReason})"
val totalPrice = carContext.getString(
R.string.charge_price_format,
amount,
currency(price.currency)
)
val kwhPrice = if (amount > 0f) {
carContext.getString(
if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
R.string.charge_price_kwh_format
} else {
R.string.charge_price_average_format
},
amount / meta!!.energy,
currency(price.currency)
)
} else null
val monthlyFees = if (price.totalMonthlyFee > 0 || price.monthlyMinSales > 0) {
price.formatMonthlyFees(carContext)
} else null
var text = totalPrice
if (kwhPrice != null && monthlyFees != null) {
text += " ($kwhPrice, $monthlyFees)"
} else if (kwhPrice != null) {
text += " ($kwhPrice)"
} else if (monthlyFees != null) {
text += " ($monthlyFees)"
}
return text
}
private fun loadData() {
if (supportsCarApiLevel3(carContext)) {
val exec = ContextCompat.getMainExecutor(carContext)
val hardwareMan =
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
hardwareMan.carInfo.fetchModel(exec) { model ->
loadPrices(model)
}
} else {
loadPrices(null)
}
}
private fun loadPrices(model: Model?) {
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
val manufacturer = getVehicleBrand(model?.manufacturer?.value)
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
lifecycleScope.launch {
try {
val car = determineVehicle(manufacturer, modelName)
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
if (cpStation.chargePoints.isEmpty()) {
errorMessage =
carContext.getString(R.string.chargeprice_no_compatible_connectors)
invalidate()
return@launch
}
val result = api.getChargePrices(
ChargepriceRequest(
dataAdapter = dataAdapter,
station = cpStation,
vehicle = car,
options = ChargepriceOptions(
batteryRange = batteryRange.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency,
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad,
showPriceUnavailable = true
),
relationships = if (!prefs.chargepriceMyTariffsAll) {
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
Relationships(
"tariffs" to Relationship.ToMany(
myTariffs.map {
ResourceIdentifier(
"tariff",
id = it
)
},
meta = Meta.from(
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
ChargepriceApi.moshi
)
)
)
} else null
), ChargepriceApi.getChargepriceLanguage()
)
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint
// (we have already filtered so that only compatible ones are included)
val chargepoint = cpStation.chargePoints.maxByOrNull { it.power }
val index = cpStation.chargePoints.indexOf(chargepoint)
this@ChargepriceScreen.chargepoint =
charger.chargepoints.filter { equivalentPlugTypes(it.type).any { it in car.compatibleEvmapConnectors } }[index]
if (chargepoint == null) {
errorMessage =
carContext.getString(R.string.chargeprice_no_compatible_connectors)
invalidate()
return@launch
}
val metaMapped =
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
meta = metaMapped.chargePoints.maxByOrNull { it.power }
prices = result.data!!.mapNotNull { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.copy(
chargepointPrices = filteredPrices
)
}
}
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariffId in myTariffs
}
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(
carContext,
R.string.chargeprice_connection_error,
CarToast.LENGTH_LONG
)
.show()
}
} catch (e: HttpException) {
withContext(Dispatchers.Main) {
CarToast.makeText(
carContext,
R.string.chargeprice_connection_error,
CarToast.LENGTH_LONG
)
.show()
}
} catch (e: NoVehicleSelectedException) {
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
invalidate()
} catch (e: VehicleUnknownException) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_unknown,
manufacturer,
modelName
)
invalidate()
} catch (e: VehicleAmbiguousException) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_ambiguous,
manufacturer,
modelName
)
invalidate()
} catch (e: VehicleUnavailableException) {
errorMessage =
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
invalidate()
}
}
}
private class NoVehicleSelectedException : Exception()
private class VehicleUnknownException : Exception()
private class VehicleAmbiguousException : Exception()
private class VehicleUnavailableException : Exception()
private suspend fun determineVehicle(
manufacturer: String?,
modelName: String?
): ChargepriceCar {
var vehicles = api.getVehicles().filter {
it.id in prefs.chargepriceMyVehicles
}
if (vehicles.isEmpty()) {
throw NoVehicleSelectedException()
} else if (vehicles.size > 1) {
if (manufacturer != null) {
vehicles = vehicles.filter {
it.brand.lowercase() == getVehicleBrand(manufacturer)?.lowercase()
}
if (vehicles.isEmpty()) {
throw VehicleUnknownException()
} else if (vehicles.size > 1) {
if (modelName != null) {
vehicles = vehicles.filter {
it.name.lowercase().startsWith(modelName.lowercase())
}
if (vehicles.isEmpty()) {
throw VehicleUnknownException()
} else if (vehicles.size > 1) {
throw VehicleAmbiguousException()
}
} else {
throw VehicleAmbiguousException()
}
}
} else {
throw VehicleUnavailableException()
}
}
return vehicles[0]
}
}

View File

@@ -32,7 +32,6 @@ import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
import coil.imageLoader
import coil.request.ImageRequest
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -142,32 +141,26 @@ class ChargerDetailScreen(
if (ChargepriceApi.isChargerSupported(charger)) {
addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_chargeprice
)
).build()
)
.setTitle(carContext.getString(R.string.auto_prices))
.setOnClickListener {
if (prefs.chargepriceNativeIntegration) {
if (!prefs.chargepriceRemoval2025DialogShown) {
screenManager.push(
ChargepriceScreen(
TextDialogScreen(
carContext,
session,
charger
R.string.chargeprice_removal_2025_dialog_title,
R.string.chargeprice_removal_2025_dialog_detail
)
)
} else {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(ChargepriceApi.getPoiUrl(charger))
)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
session.cas.startActivity(intent)
prefs.chargepriceRemoval2025DialogShown = true
return@setOnClickListener
}
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(ChargepriceApi.getPoiUrl(charger))
)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
session.cas.startActivity(intent)
}
.build())
}

View File

@@ -59,7 +59,6 @@ import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.MarkerManager
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.utils.headingDiff
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.await
@@ -70,12 +69,12 @@ import java.io.IOException
import java.time.Duration
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.collections.set
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.time.DurationUnit
import kotlin.time.TimeSource
private const val DEFAULT_ZOOM_MYLOCATION = 14f
/**
* Main map screen showing either nearby chargers or favorites.
*
@@ -146,6 +145,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private var map: AnyMap? = null
private var markerManager: MarkerManager? = null
private var myLocationEnabled = false
private var compassEnabled = false
private var myLocationNeedsUpdate = false
private val formatter = ChargerListFormatter(ctx, this, session.cas)
@@ -241,11 +241,17 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
.addAction(Action.PAN)
.addAction(
Action.Builder().setIcon(
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_location))
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (compassEnabled) R.drawable.ic_compass else R.drawable.ic_location
)
)
.setTint(if (myLocationEnabled) CarColor.SECONDARY else CarColor.DEFAULT)
.build()
).setOnClickListener {
enableLocation(true)
enableLocation(true, myLocationEnabled && !compassEnabled)
invalidate()
}.build()
)
.addAction(
@@ -385,8 +391,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
val map = map ?: return
if (myLocationEnabled) {
val bearing = if (compassEnabled) getBearing(location) else 0f
if (oldLoc == null) {
mapSurfaceCallback.animateCamera(map.cameraUpdateFactory.newLatLngZoom(latLng, 13f))
mapSurfaceCallback.animateCamera(
map.cameraUpdateFactory.newLatLngZoomBearing(
latLng,
DEFAULT_ZOOM_MYLOCATION,
bearing
)
)
} else if (latLng != oldLoc && distanceBetween(
latLng.latitude,
latLng.longitude,
@@ -395,7 +408,11 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
) > 1
) {
// only update map if location changed by more than 1 meter
val camUpdate = map.cameraUpdateFactory.newLatLng(latLng)
val camUpdate = map.cameraUpdateFactory.newLatLngZoomBearing(
latLng,
map.cameraPosition.zoom,
bearing
)
mapSurfaceCallback.animateCamera(camUpdate)
}
}
@@ -545,6 +562,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
availabilities.clear()
location = null
myLocationEnabled = false
compassEnabled = false
removeListeners()
}
@@ -556,6 +574,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
prefs.currentMapZoom = it.cameraPosition.zoom
}
prefs.currentMapMyLocationEnabled = myLocationEnabled
prefs.androidAutoCompassEnabled = compassEnabled
}
private fun removeListeners() {
@@ -625,9 +644,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
onClusterClick = {
val newZoom = map.cameraPosition.zoom + 2
mapSurfaceCallback.animateCamera(
map.cameraUpdateFactory.newLatLngZoom(
map.cameraUpdateFactory.newLatLngZoomBearing(
LatLng(it.coordinates.lat, it.coordinates.lng),
newZoom
newZoom,
if (compassEnabled) location?.let { getBearing(it) } ?: 0f else 0f
)
)
}
@@ -657,6 +677,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
prefs.placeSearchResultAndroidAuto?.let { place ->
// move to the location of the search result
myLocationEnabled = false
compassEnabled = false
markerManager?.searchResult = place
if (place.viewport != null) {
map.moveCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
@@ -664,7 +685,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
map.moveCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
}
} ?: if (prefs.currentMapMyLocationEnabled) {
enableLocation(false)
enableLocation(false, prefs.androidAutoCompassEnabled)
} else {
// use position saved in preferences, fall back to default (Europe)
val cameraUpdate =
@@ -692,14 +713,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
loadChargers()
}
private fun enableLocation(animated: Boolean) {
private fun enableLocation(animated: Boolean, withCompass: Boolean) {
myLocationEnabled = true
compassEnabled = withCompass
myLocationNeedsUpdate = true
if (location != null) {
location?.let { location ->
val map = map ?: return
val update = map.cameraUpdateFactory.newLatLngZoom(
val update = map.cameraUpdateFactory.newLatLngZoomBearing(
LatLng.fromLocation(location),
13f
DEFAULT_ZOOM_MYLOCATION,
if (withCompass) getBearing(location) else 0f
)
if (animated) {
mapSurfaceCallback.animateCamera(update)
@@ -708,4 +731,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
}
}
private fun getBearing(location: Location): Float =
heading?.orientations?.value?.get(0) ?: location.bearing
}

View File

@@ -171,8 +171,9 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
flingAnimator?.cancel()
val map = map ?: return
val offsetX = (focusX - mapView.width / 2) * (scaleFactor - 1f)
val offsetY = (offsetY(focusY) - mapView.height / 2) * (scaleFactor - 1f)
val (x, y) = offsetScreen(focusX, focusY)
val offsetX = (x - mapView.width / 2) * (scaleFactor - 1f)
val offsetY = (y - mapView.height / 2) * (scaleFactor - 1f)
Log.i("MapSurfaceCallback", "focus: $focusX, $focusY, scaleFactor: $scaleFactor")
if (scaleFactor == 2f) {
@@ -223,13 +224,13 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
flingAnimator?.cancel()
val downTime: Long = SystemClock.uptimeMillis()
val eventTime: Long = downTime + 100
val yOffset = offsetY(y)
val (xOffset, yOffset) = offsetScreen(x, y)
val downEvent = MotionEvent.obtain(
downTime,
downTime,
MotionEvent.ACTION_DOWN,
x,
xOffset,
yOffset,
0
)
@@ -239,7 +240,7 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
downTime,
eventTime,
MotionEvent.ACTION_UP,
x,
xOffset,
yOffset,
0
)
@@ -247,16 +248,24 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
upEvent.recycle()
}
private fun offsetY(y: Float): Float {
private fun offsetScreen(x: Float, y: Float): Pair<Float, Float> {
if (BuildConfig.FLAVOR_automotive != "automotive") {
return y
return x to y
}
// On AAOS, touch locations seem to be offset by the status bar height
// On AAOS, touch locations don't seem to take into account system bar insets
// related: https://issuetracker.google.com/issues/256905247
val resId = ctx.resources.getIdentifier("status_bar_height", "dimen", "android")
val offset = resId.takeIf { it > 0 }?.let { ctx.resources.getDimensionPixelSize(it) } ?: 0
return y + offset
val yOffset = resId.takeIf { it > 0 }?.let { ctx.resources.getDimensionPixelSize(it) } ?: 0
val xOffset = if (Build.MODEL == "AIVI2 R FULL DOM" && width > height) {
// Renault 5 left system bar
120
} else {
0
}
return x + xOffset to y + yOffset
}
private fun createMap(ctx: Context): MapContainerView {

View File

@@ -1,9 +1,7 @@
package net.vonforst.evmap.auto
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager.NameNotFoundException
import android.hardware.Sensor
import android.hardware.SensorManager
@@ -17,8 +15,6 @@ import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.GridItem
import androidx.car.app.model.GridTemplate
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.MessageTemplate
@@ -27,15 +23,12 @@ import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.core.content.IntentCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.launch
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.EXTRA_DONATE
@@ -44,11 +37,6 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import net.vonforst.evmap.currencyDisplayName
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
import net.vonforst.evmap.getPackageInfoCompat
import net.vonforst.evmap.storage.AppDatabase
@@ -57,12 +45,15 @@ import net.vonforst.evmap.storage.PreferenceDataSource
import okhttp3.OkHttpClient
import java.io.IOException
import java.time.Instant
import kotlin.math.max
import kotlin.math.min
@ExperimentalCarApi
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), DefaultLifecycleObserver {
val prefs = PreferenceDataSource(ctx)
val newMapScreenEnabledPrevious = prefs.androidAutoNewMapScreenEnabled
init {
lifecycle.addObserver(this)
}
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
@@ -86,23 +77,6 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
screenManager.push(DataSettingsScreen(carContext, session))
}
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.settings_chargeprice))
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_chargeprice
)
).setTint(
CarColor.DEFAULT
).build()
)
setBrowsable(true)
setOnClickListener {
screenManager.push(ChargepriceSettingsScreen(carContext))
}
}.build())
if (supportsCarApiLevel3(carContext)) {
addItem(
Row.Builder()
@@ -118,7 +92,26 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
}
.build()
)
if (carContext.carAppApiLevel < 7 || !carContext.isAppDrivenRefreshSupported) {
if (supportsNewMapScreen(carContext)) {
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.auto_use_new_map_screen))
.setToggle(Toggle.Builder {
prefs.androidAutoNewMapScreenEnabled = it
invalidate()
}.setChecked(prefs.androidAutoNewMapScreenEnabled).build())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_developer
)
).setTint(CarColor.DEFAULT).build()
)
.build()
)
}
if (!supportsNewMapScreen(carContext) || !prefs.androidAutoNewMapScreenEnabled) {
// this option is only supported in LegacyMapScreen
addItem(
Row.Builder()
@@ -150,14 +143,23 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
).setTint(CarColor.DEFAULT).build()
)
.setBrowsable(true)
.setOnClickListener {
.setOnClickListener(ParkedOnlyOnClickListener.create {
screenManager.push(AboutScreen(carContext, session))
}
})
.build()
)
}.build())
}.build()
}
override fun onStop(owner: LifecycleOwner) {
if (newMapScreenEnabledPrevious != prefs.androidAutoNewMapScreenEnabled) {
val newMapScreen = session.onCreateScreen(session.intent)
val oldMapScreen = screenManager.screenStack.last()
screenManager.push(newMapScreen)
screenManager.remove(oldMapScreen)
}
}
}
@ExperimentalCarApi
@@ -507,341 +509,6 @@ class ChooseDataSourceScreen(
}
}
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(carContext)
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.settings_chargeprice))
setHeaderAction(Action.BACK)
setSingleList(ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_native_integration))
addText(carContext.getString(if (prefs.chargepriceNativeIntegration) R.string.pref_chargeprice_native_integration_on else R.string.pref_chargeprice_native_integration_off))
setToggle(Toggle.Builder {
prefs.chargepriceNativeIntegration = it
invalidate()
}.setChecked(prefs.chargepriceNativeIntegration).build())
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_my_vehicle))
setBrowsable(true)
setOnClickListener {
screenManager.push(SelectVehiclesScreen(carContext))
}
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_my_tariffs))
setBrowsable(true)
setOnClickListener {
screenManager.push(SelectTariffsScreen(carContext))
}
addText(
if (prefs.chargepriceMyTariffsAll) {
carContext.getString(R.string.chargeprice_all_tariffs_selected)
} else {
val n = prefs.chargepriceMyTariffs?.size ?: 0
carContext.resources
.getQuantityString(
R.plurals.chargeprice_some_tariffs_selected,
n,
n
) + "\n" + carContext.resources.getQuantityString(
R.plurals.pref_my_tariffs_summary,
n
)
}
)
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
setBrowsable(true)
val range = prefs.chargepriceBatteryRangeAndroidAuto
addText(
carContext.getString(
R.string.chargeprice_battery_range,
range[0],
range[1]
)
)
setOnClickListener {
screenManager.push(SelectChargingRangeScreen(carContext))
}
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_currency))
val values =
carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
val names = values.map(::currencyDisplayName)
val index = values.indexOf(prefs.chargepriceCurrency)
addText(if (index >= 0) names[index] else "")
setBrowsable(true)
setOnClickListener {
screenManager.push(SelectCurrencyScreen(carContext))
}
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_no_base_fee))
setToggle(Toggle.Builder {
prefs.chargepriceNoBaseFee = it
}.setChecked(prefs.chargepriceNoBaseFee).build())
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
if (maxRows > 6) {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs))
addText(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs_summary))
setToggle(Toggle.Builder {
prefs.chargepriceShowProviderCustomerTariffs = it
}.setChecked(prefs.chargepriceShowProviderCustomerTariffs).build())
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load))
addText(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load_summary))
setToggle(Toggle.Builder {
prefs.chargepriceAllowUnbalancedLoad = it
}.setChecked(prefs.chargepriceAllowUnbalancedLoad).build())
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
}
}.build())
}.build()
}
}
class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceCar>(ctx) {
private val prefs = PreferenceDataSource(carContext)
private var api = ChargepriceApi.create(
carContext.getString(R.string.chargeprice_key),
carContext.getString(R.string.chargeprice_api_url)
)
override val isMultiSelect = true
override val shouldShowSelectAll = false
override fun isSelected(it: ChargepriceCar): Boolean {
return prefs.chargepriceMyVehicles.contains(it.id)
}
override fun toggleSelected(item: ChargepriceCar) {
if (isSelected(item)) {
prefs.chargepriceMyVehicles = prefs.chargepriceMyVehicles.minus(item.id)
} else {
prefs.chargepriceMyVehicles = prefs.chargepriceMyVehicles.plus(item.id)
}
}
override fun getLabel(it: ChargepriceCar) = "${it.brand} ${it.name}"
override fun getDetails(it: ChargepriceCar) = it.formatSpecs()
override suspend fun loadData(): List<ChargepriceCar> {
return api.getVehicles()
}
}
class SelectTariffsScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceTariff>(ctx) {
private val prefs = PreferenceDataSource(carContext)
private var api = ChargepriceApi.create(
carContext.getString(R.string.chargeprice_key),
carContext.getString(R.string.chargeprice_api_url)
)
override val isMultiSelect = true
override val shouldShowSelectAll = true
override fun isSelected(it: ChargepriceTariff): Boolean {
return prefs.chargepriceMyTariffsAll or (prefs.chargepriceMyTariffs?.contains(it.id)
?: false)
}
override fun toggleSelected(item: ChargepriceTariff) {
val tariffs = prefs.chargepriceMyTariffs ?: if (prefs.chargepriceMyTariffsAll) {
fullList!!.map { it.id }.toSet()
} else {
emptySet()
}
if (isSelected(item)) {
prefs.chargepriceMyTariffs = tariffs.minus(item.id)
prefs.chargepriceMyTariffsAll = false
} else {
prefs.chargepriceMyTariffs = tariffs.plus(item.id)
if (prefs.chargepriceMyTariffs == fullList!!.map { it.id }.toSet()) {
prefs.chargepriceMyTariffsAll = true
}
}
}
override fun selectAll() {
prefs.chargepriceMyTariffsAll = true
super.selectAll()
}
override fun selectNone() {
prefs.chargepriceMyTariffsAll = false
prefs.chargepriceMyTariffs = emptySet()
super.selectNone()
}
override fun getLabel(it: ChargepriceTariff): String {
return if (!it.name.lowercase().startsWith(it.provider.lowercase())) {
"${it.provider} ${it.name}"
} else {
it.name
}
}
override suspend fun loadData(): List<ChargepriceTariff> {
return api.getTariffs()
}
}
class SelectCurrencyScreen(ctx: CarContext) : MultiSelectSearchScreen<Pair<String, String>>(ctx) {
private val prefs = PreferenceDataSource(carContext)
override val isMultiSelect = false
override val shouldShowSelectAll = false
override fun isSelected(it: Pair<String, String>): Boolean =
prefs.chargepriceCurrency == it.second
override fun toggleSelected(item: Pair<String, String>) {
prefs.chargepriceCurrency = item.second
}
override fun getLabel(it: Pair<String, String>): String = it.first
override suspend fun loadData(): List<Pair<String, String>> {
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
val names = values.map(::currencyDisplayName)
return names.zip(values)
}
}
class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(carContext)
private val maxItems = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)
} else 6
override fun onGetTemplate(): Template {
return GridTemplate.Builder().apply {
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
setHeaderAction(Action.BACK)
setSingleList(
ItemList.Builder().apply {
addItem(GridItem.Builder().apply {
setTitle(carContext.getString(R.string.chargeprice_battery_range_from))
setText(
carContext.getString(
R.string.percent_format,
prefs.chargepriceBatteryRangeAndroidAuto[0]
)
)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_add
)
).build()
)
setOnClickListener {
prefs.chargepriceBatteryRangeAndroidAuto =
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
this[0] = min(this[1] - 5, this[0] + 5)
}
invalidate()
}
}.build())
addItem(GridItem.Builder().apply {
setTitle(carContext.getString(R.string.chargeprice_battery_range_to))
setText(
carContext.getString(
R.string.percent_format,
prefs.chargepriceBatteryRangeAndroidAuto[1]
)
)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_add
)
).build()
)
setOnClickListener {
prefs.chargepriceBatteryRangeAndroidAuto =
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
this[1] = min(100f, this[1] + 5)
}
invalidate()
}
}.build())
val nSpacers = when {
maxItems % 3 == 0 -> 1
maxItems == 100 -> 0 // AA has increased the limit to 100 and changed the way items are laid out
maxItems % 4 == 0 -> 2
else -> 0
}
for (i in 0..nSpacers) {
addItem(GridItem.Builder().apply {
setTitle(" ")
setImage(emptyCarIcon)
}.build())
}
addItem(GridItem.Builder().apply {
setTitle(" ")
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_remove
)
).build()
)
setOnClickListener {
prefs.chargepriceBatteryRangeAndroidAuto =
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
this[0] = max(0f, this[0] - 5)
}
invalidate()
}
}.build())
addItem(GridItem.Builder().apply {
setTitle(" ")
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_remove
)
).build()
)
setOnClickListener {
prefs.chargepriceBatteryRangeAndroidAuto =
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
this[1] = max(this[0] + 5, this[1] - 5)
}
invalidate()
}
}.build())
}.build()
)
}.build()
}
}
@ExperimentalCarApi
class AboutScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)

View File

@@ -0,0 +1,21 @@
package net.vonforst.evmap.auto
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.LongMessageTemplate
import androidx.car.app.model.Template
class TextDialogScreen(
ctx: CarContext,
@StringRes val title: Int,
@StringRes val message: Int
) : Screen(ctx) {
override fun onGetTemplate(): Template {
return LongMessageTemplate.Builder(carContext.getString(message)).apply {
setTitle(carContext.getString(title))
setHeaderAction(Action.BACK)
}.build()
}
}

View File

@@ -1,275 +0,0 @@
package net.vonforst.evmap.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialContainerTransform
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.ChargepriceAdapter
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
import net.vonforst.evmap.adapter.SingleViewAdapter
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.databinding.FragmentChargepriceHeaderBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
import java.text.NumberFormat
class ChargepriceFragment : Fragment() {
private lateinit var binding: FragmentChargepriceBinding
private lateinit var headerBinding: FragmentChargepriceHeaderBinding
private var connectionErrorSnackbar: Snackbar? = null
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
savedStateViewModelFactory { state ->
ChargepriceViewModel(
requireActivity().application,
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url),
state
)
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform()
if (savedInstanceState == null) {
val prefs = PreferenceDataSource(requireContext())
prefs.chargepriceCounter += 1
if ((prefs.chargepriceCounter).mod(30) == 0) {
showDonationDialog()
}
}
}
override fun onResume() {
super.onResume()
vm.reloadPrefs()
}
private fun showDonationDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.chargeprice_donation_dialog_title)
.setMessage(R.string.chargeprice_donation_dialog_detail)
.setNegativeButton(R.string.ok) { di, _ ->
di.cancel()
}
.setPositiveButton(R.string.donate) { di, _ ->
di.dismiss()
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToDonateFragment())
}
.show()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_chargeprice, container, false
)
headerBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_chargeprice_header, container, false
)
binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm
headerBinding.lifecycleOwner = viewLifecycleOwner
headerBinding.vm = vm
binding.toolbar.inflateMenu(R.menu.chargeprice)
binding.toolbar.setTitle(R.string.chargeprice_title)
ViewCompat.setOnApplyWindowInsetsListener(binding.chargePricesList) { v, insets ->
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
WindowInsetsCompat.CONSUMED
}
return binding.root
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setupWithNavController(
findNavController(),
(requireActivity() as MapsActivity).appBarConfiguration
)
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
val charger = fragmentArgs.charger
vm.charger.value = charger
if (vm.chargepoint.value == null) {
vm.chargepoint.value = charger.chargepointsMerged[0]
}
val vehicleAdapter = CheckableChargepriceCarAdapter()
headerBinding.vehicleSelection.adapter = vehicleAdapter
val vehicleObserver: Observer<ChargepriceCar?> = Observer {
vehicleAdapter.setCheckedItem(it)
}
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
vehicleAdapter.onCheckedItemChangedListener = {
vm.vehicle.removeObserver(vehicleObserver)
vm.vehicle.value = it
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
}
val chargepriceAdapter = ChargepriceAdapter().apply {
onClickListener = {
(requireActivity() as MapsActivity).openUrl(it.url, binding.root)
}
}
val joinedAdapter = ConcatAdapter(
SingleViewAdapter(headerBinding.root),
chargepriceAdapter
)
binding.chargePricesList.apply {
adapter = joinedAdapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
DividerItemDecoration(
context, LinearLayoutManager.VERTICAL
)
)
}
vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) {
chargepriceAdapter.meta = it?.data
}
vm.myTariffs.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffs = it
}
vm.myTariffsAll.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffsAll = it
}
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
chargepriceAdapter.submitList(it?.data ?: emptyList())
}
val connectorsAdapter = CheckableConnectorAdapter()
val observer: Observer<Chargepoint?> = Observer {
connectorsAdapter.setCheckedItem(it)
}
vm.chargepoint.observe(viewLifecycleOwner, observer)
connectorsAdapter.onCheckedItemChangedListener = {
vm.chargepoint.removeObserver(observer)
vm.chargepoint.value = it
vm.chargepoint.observe(viewLifecycleOwner, observer)
}
vm.vehicleCompatibleConnectors.observe(viewLifecycleOwner) { plugs ->
connectorsAdapter.enabledConnectors =
plugs?.flatMap { plug -> equivalentPlugTypes(plug) }
}
headerBinding.connectorsList.apply {
adapter = connectorsAdapter
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
binding.imgChargepriceLogo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
}
binding.btnSettings.setOnClickListener {
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToChargepriceSettingsFragment())
}
headerBinding.batteryRange.setLabelFormatter { value: Float ->
val fmt = NumberFormat.getNumberInstance()
fmt.maximumFractionDigits = 0
fmt.format(value.toDouble()) + "%"
}
headerBinding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> vm.batteryRangeSliderDragging.value = true
MotionEvent.ACTION_UP -> vm.batteryRangeSliderDragging.value = false
}
false
}
headerBinding.tvChargeFromTo.setOnClickListener {
it.postDelayed({
vm.resetBatteryRangeToDefault()
}, 250)
}
binding.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_help -> {
(activity as? MapsActivity)?.openUrl(
getString(R.string.chargeprice_faq_link),
binding.root
)
true
}
else -> false
}
}
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) { res ->
when (res?.status) {
Status.ERROR -> {
if (vm.vehicle.value == null) return@observe
connectionErrorSnackbar?.dismiss()
connectionErrorSnackbar = Snackbar
.make(
view,
R.string.chargeprice_connection_error,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.retry) {
connectionErrorSnackbar?.dismiss()
vm.loadPrices()
}
connectionErrorSnackbar!!.show()
}
Status.SUCCESS, null -> {
connectionErrorSnackbar?.dismiss()
}
Status.LOADING -> {
}
}
}
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
}
}

View File

@@ -47,7 +47,6 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
@@ -448,19 +447,28 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
}
binding.detailView.btnChargeprice.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
if (prefs.chargepriceNativeIntegration) {
val extras =
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
findNavController().safeNavigate(
MapFragmentDirections.actionMapToChargepriceFragment(charger),
extras
)
} else {
(activity as? MapsActivity)?.openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
if (prefs.chargepriceCounter > 0 && !prefs.chargepriceRemoval2025DialogShown) {
// user has been using the native Chargeprice integration before
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.chargeprice_removal_2025_dialog_title)
.setMessage(R.string.chargeprice_removal_2025_dialog_detail)
.setPositiveButton(R.string.ok) { di, _ ->
di.cancel()
prefs.chargepriceRemoval2025DialogShown = true
(activity as? MapsActivity)?.openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
}
.show()
return@setOnClickListener
}
(activity as? MapsActivity)?.openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
}
binding.detailView.btnChargerWebsite.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener

View File

@@ -6,6 +6,9 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.URLSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -13,6 +16,7 @@ import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.text.getSpans
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
@@ -27,6 +31,8 @@ import net.vonforst.evmap.databinding.FragmentOnboardingWelcomeBinding
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.CustomUrlSpan
import net.vonforst.evmap.ui.replaceUrlSpansWithCustom
import net.vonforst.evmap.waitForLayout
class OnboardingFragment : Fragment() {
@@ -237,13 +243,14 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.cbAcceptPrivacy.text =
val text =
HtmlCompat.fromHtml(
getString(
R.string.accept_privacy,
getString(R.string.privacy_link)
), HtmlCompat.FROM_HTML_MODE_LEGACY
)
).replaceUrlSpansWithCustom()
binding.cbAcceptPrivacy.text = text
binding.cbAcceptPrivacy.linksClickable = true
binding.cbAcceptPrivacy.movementMethod = LinkMovementMethodCompat.getInstance()
binding.btnGetStarted.visibility = View.INVISIBLE

View File

@@ -1,43 +0,0 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.RangeSliderPreference
import java.text.NumberFormat
class AndroidAutoSettingsFragment : BaseSettingsFragment() {
override val isTopLevel = false
private lateinit var rangePreference: RangeSliderPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rangePreference = findPreference("chargeprice_battery_range_android_auto")!!
rangePreference.labelFormatter = { value: Float ->
val fmt = NumberFormat.getNumberInstance()
fmt.maximumFractionDigits = 0
fmt.format(value.toDouble()) + "%"
}
updateRangePreferenceSummary()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_android_auto, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
"chargeprice_battery_range_android_auto_min", "chargeprice_battery_range_android_auto_max" -> {
updateRangePreferenceSummary()
}
}
}
private fun updateRangePreferenceSummary() {
val range = prefs.chargepriceBatteryRangeAndroidAuto
rangePreference.summary = getString(R.string.chargeprice_battery_range, range[0], range[1])
}
}

View File

@@ -1,147 +0,0 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.RelativeSizeSpan
import android.view.View
import androidx.fragment.app.viewModels
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import net.vonforst.evmap.R
import net.vonforst.evmap.currencyDisplayName
import net.vonforst.evmap.ui.MultiSelectDialogPreference
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class ChargepriceSettingsFragment : BaseSettingsFragment() {
override val isTopLevel = false
private val vm: SettingsViewModel by viewModels(factoryProducer = {
viewModelFactory {
SettingsViewModel(
requireActivity().application,
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url)
)
}
})
private lateinit var myVehiclePreference: MultiSelectDialogPreference
private lateinit var myTariffsPreference: MultiSelectDialogPreference
private lateinit var nativeIntegrationPreference: CheckBoxPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
nativeIntegrationPreference = findPreference("chargeprice_native_integration")!!
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
vm.vehicles.observe(viewLifecycleOwner) { res ->
res.data?.let { cars ->
val sortedCars = cars.sortedBy { it.brand }
myVehiclePreference.entryValues = sortedCars.map { it.id }.toTypedArray()
myVehiclePreference.entries = sortedCars.map {
SpannableStringBuilder().apply {
appendLine("${it.brand} ${it.name}")
append(
it.formatSpecs(),
RelativeSizeSpan(0.86f),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
}
}.toTypedArray()
myVehiclePreference.isEnabled = nativeIntegrationPreference.isChecked
updateMyVehiclesSummary()
}
}
myTariffsPreference = findPreference("chargeprice_my_tariffs")!!
myTariffsPreference.isEnabled = false
vm.tariffs.observe(viewLifecycleOwner) { res ->
res.data?.let { tariffs ->
myTariffsPreference.entryValues = tariffs.map { it.id }.toTypedArray()
myTariffsPreference.entries = tariffs.map {
if (!it.name.lowercase().startsWith(it.provider.lowercase())) {
"${it.provider} ${it.name}"
} else {
it.name
}
}.toTypedArray()
myTariffsPreference.isEnabled = nativeIntegrationPreference.isChecked
updateMyTariffsSummary()
}
}
updateNativeIntegrationState()
val currencyPreference = findPreference<ListPreference>("chargeprice_currency")!!
currencyPreference.entries = currencyPreference.entryValues.map {
currencyDisplayName(it.toString()).replaceFirstChar { it.uppercase() }
}.toTypedArray()
}
private fun updateNativeIntegrationState() {
for (i in 0 until preferenceScreen.preferenceCount) {
val pref = preferenceScreen.getPreference(i)
if (pref == nativeIntegrationPreference) {
continue
} else if (pref == myTariffsPreference) {
pref.isEnabled =
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
} else if (pref == myVehiclePreference) {
pref.isEnabled =
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
} else {
pref.isEnabled = nativeIntegrationPreference.isChecked
}
}
}
private fun updateMyTariffsSummary() {
myTariffsPreference.summary =
if (prefs.chargepriceMyTariffsAll) {
getString(R.string.chargeprice_all_tariffs_selected)
} else {
val n = prefs.chargepriceMyTariffs?.size ?: 0
requireContext().resources
.getQuantityString(
R.plurals.chargeprice_some_tariffs_selected,
n,
n
) + "\n" + requireContext().resources
.getQuantityString(R.plurals.pref_my_tariffs_summary, n)
}
}
private fun updateMyVehiclesSummary() {
vm.vehicles.value?.data?.let { cars ->
val vehicles = cars.filter { it.id in prefs.chargepriceMyVehicles }
val summary = vehicles.joinToString(", ") {
"${it.brand} ${it.name}"
}
myVehiclePreference.summary = summary
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_chargeprice, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
"chargeprice_my_vehicle" -> {
updateMyVehiclesSummary()
}
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}
"chargeprice_native_integration" -> {
updateNativeIntegrationState()
}
}
}
}

View File

@@ -1,10 +1,10 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.core.net.toUri
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
@@ -24,7 +24,6 @@ import net.vonforst.evmap.viewmodel.viewModelFactory
import okhttp3.OkHttpClient
import okio.IOException
import java.time.Instant
import androidx.core.net.toUri
class DataSettingsFragment : BaseSettingsFragment() {
override val isTopLevel = false
@@ -33,8 +32,6 @@ class DataSettingsFragment : BaseSettingsFragment() {
viewModelFactory {
SettingsViewModel(
requireActivity().application,
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url)
)
}
})

View File

@@ -140,10 +140,12 @@ data class ChargeLocation(
.filter { it.type == variant.type && it.power == variant.power }
val count = filtered.sumOf { it.count }
val mergedEvseIds = filtered.map { if (it.evseIds == null) List(it.count) {null} else it.evseIds }.flatten()
val mergedEvseUIds = filtered.map { if (it.evseUIds == null) List(it.count) {null} else it.evseUIds }.flatten()
Chargepoint(variant.type, variant.power, count,
filtered.map { it.current }.distinct().singleOrNull(),
filtered.map { it.voltage }.distinct().singleOrNull(),
if (mergedEvseIds.all { it == null }) null else mergedEvseIds
if (mergedEvseIds.all { it == null }) null else mergedEvseIds,
if (mergedEvseUIds.all { it == null }) null else mergedEvseUIds
)
}
}
@@ -425,7 +427,9 @@ data class Chargepoint(
// (each of the three can be separately limited)
val voltage: Double? = null,
// Electric Vehicle Supply Equipment Ids for this Chargepoint's plugs/sockets
val evseIds: List<String?>? = null
val evseIds: List<String?>? = null,
// Electric Vehicle Supply Equipment Unique Ids for this Chargepoint's plugs/sockets
val evseUIds: List<String?>? = null
) : Equatable, Parcelable {
fun hasKnownPower(): Boolean = power != null
fun hasKnownVoltageAndCurrent(): Boolean = voltage != null && current != null

View File

@@ -25,4 +25,4 @@ data class Favorite(
data class FavoriteWithDetail(
@Embedded val favorite: Favorite,
@Embedded val charger: ChargeLocation
)
)

View File

@@ -37,6 +37,7 @@ class CustomNavigator(
"goingelectric" -> "https://www.goingelectric.de/stromtankstellen/new/"
"nobil" -> "http://nobil.no/api/chargerregistration/chargerregistration.php?action=register"
"openchargemap" -> "https://openchargemap.org/site/poi/add"
"openstreetmap" -> "https://www.openstreetmap.org/edit"
else -> throw IllegalArgumentException()
}
launchCustomTab(url)

View File

@@ -69,8 +69,11 @@ abstract class ChargeLocationsDao {
@Query("DELETE FROM chargelocation WHERE NOT EXISTS (SELECT 1 FROM favorite WHERE favorite.chargerId = chargelocation.id)")
abstract suspend fun deleteAllIfNotFavorite()
@Query("DELETE FROM chargelocation WHERE dataSource == :dataSource AND id NOT IN (:chargerIds)")
abstract suspend fun deleteIdNotIn(dataSource: String, chargerIds: List<Long>)
@Query("SELECT id FROM chargelocation WHERE dataSource == :dataSource")
abstract suspend fun getAllIds(dataSource: String): List<Long>
@Query("DELETE FROM chargelocation WHERE dataSource == :dataSource AND id IN (:chargerIds)")
abstract suspend fun deleteById(dataSource: String, chargerIds: List<Long>)
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND id == :id AND isDetailed == 1 AND timeRetrieved > :after")
abstract suspend fun getChargeLocationById(
@@ -706,7 +709,7 @@ class ChargeLocationsRepository(
val result = api.fullDownload()
try {
var insertJob: Job? = null
val chargerIds = mutableListOf<Long>()
val idsToDelete = chargeLocationsDao.getAllIds(api.id).toMutableSet()
result.chargers.chunked(1024).forEach {
insertJob?.join()
insertJob = withContext(Dispatchers.IO) {
@@ -714,11 +717,11 @@ class ChargeLocationsRepository(
chargeLocationsDao.insert(*it.toTypedArray())
}
}
chargerIds.addAll(it.map { it.id })
idsToDelete.removeAll(it.map { it.id })
fullDownloadProgress.value = result.progress
}
// delete chargers that have been removed
chargeLocationsDao.deleteIdNotIn(api.id, chargerIds)
chargeLocationsDao.deleteById(api.id, idsToDelete.toList())
val region = Mbr(
-180.0,

View File

@@ -15,7 +15,7 @@ class CleanupCacheWorker(appContext: Context, workerParams: WorkerParameters) :
val savedRegionDao = db.savedRegionDao()
val now = Instant.now()
val dataSources = listOf("openchargemap", "openstreetmap", "goingelectric")
val dataSources = listOf("openchargemap", "openstreetmap", "goingelectric", "nobil")
for (dataSource in dataSources) {
val api = createApi(dataSource, applicationContext)
val limit = now.minus(api.cacheLimit).toEpochMilli()

View File

@@ -40,7 +40,7 @@ import net.vonforst.evmap.model.SliderFilterValue
OCMOperator::class,
OSMNetwork::class,
SavedRegion::class
], version = 27
], version = 28
)
@TypeConverters(Converters::class, GeometryConverters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -85,7 +85,7 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21,
MIGRATION_22, MIGRATION_23, MIGRATION_24, MIGRATION_25, MIGRATION_26,
MIGRATION_27
MIGRATION_27, MIGRATION_28
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -547,6 +547,14 @@ abstract class AppDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE `ChargeLocation` ADD `accessibility` TEXT")
}
}
private val MIGRATION_28 = object : Migration(27, 28) {
override fun migrate(db: SupportSQLiteDatabase) {
// Force nobil data refresh to fetch EVSE UId attributes needed for real-time data
db.execSQL("DELETE FROM SavedRegion WHERE `dataSource` = 'nobil'")
db.execSQL("DELETE FROM ChargeLocation WHERE `dataSource` = 'nobil'")
}
}
}
/**

View File

@@ -13,10 +13,10 @@ interface FavoritesDao {
@Delete
suspend fun delete(vararg favorites: Favorite)
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL")
fun getAllFavorites(): LiveData<List<FavoriteWithDetail>>
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL")
suspend fun getAllFavoritesAsync(): List<FavoriteWithDetail>
@SkipQueryVerification

View File

@@ -12,6 +12,7 @@ import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import java.time.Instant
import androidx.core.content.edit
class PreferenceDataSource(val context: Context) {
val sp = PreferenceManager.getDefaultSharedPreferences(context)
@@ -152,87 +153,6 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply()
}
var chargepriceNativeIntegration: Boolean
get() = sp.getBoolean("chargeprice_native_integration", true)
set(value) {
sp.edit().putBoolean("chargeprice_native_integration", value).apply()
}
var chargepriceMyVehicles: Set<String>
get() = try {
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
} catch (e: ClassCastException) {
// backwards compatibility
sp.getString("chargeprice_my_vehicle", null)?.let { setOf(it) } ?: emptySet()
}
set(value) {
sp.edit().putStringSet("chargeprice_my_vehicle", value).apply()
}
var chargepriceLastSelectedVehicle: String?
get() = sp.getString("chargeprice_last_vehicle", null)
set(value) {
sp.edit().putString("chargeprice_last_vehicle", value).apply()
}
var chargepriceMyTariffs: Set<String>?
get() = sp.getStringSet("chargeprice_my_tariffs", null)
set(value) {
sp.edit().putStringSet("chargeprice_my_tariffs", value).apply()
}
var chargepriceMyTariffsAll: Boolean
get() = sp.getBoolean("chargeprice_my_tariffs_all", true)
set(value) {
sp.edit().putBoolean("chargeprice_my_tariffs_all", value).apply()
}
var chargepriceNoBaseFee: Boolean
get() = sp.getBoolean("chargeprice_no_base_fee", false)
set(value) {
sp.edit().putBoolean("chargeprice_no_base_fee", value).apply()
}
var chargepriceShowProviderCustomerTariffs: Boolean
get() = sp.getBoolean("chargeprice_show_provider_customer_tariffs", false)
set(value) {
sp.edit().putBoolean("chargeprice_show_provider_customer_tariffs", value).apply()
}
var chargepriceAllowUnbalancedLoad: Boolean
get() = sp.getBoolean("chargeprice_allow_unbalanced_load", false)
set(value) {
sp.edit().putBoolean("chargeprice_allow_unbalanced_load", value).apply()
}
var chargepriceCurrency: String
get() = sp.getString("chargeprice_currency", null) ?: "EUR"
set(value) {
sp.edit().putString("chargeprice_currency", value).apply()
}
var chargepriceBatteryRange: List<Float>
get() = listOf(
sp.getFloat("chargeprice_battery_range_min", 20f),
sp.getFloat("chargeprice_battery_range_max", 80f),
)
set(value) {
sp.edit().putFloat("chargeprice_battery_range_min", value[0])
.putFloat("chargeprice_battery_range_max", value[1])
.apply()
}
var chargepriceBatteryRangeAndroidAuto: List<Float>
get() = listOf(
sp.getFloat("chargeprice_battery_range_android_auto_min", 20f),
sp.getFloat("chargeprice_battery_range_android_auto_max", 80f),
)
set(value) {
sp.edit().putFloat("chargeprice_battery_range_android_auto_min", value[0])
.putFloat("chargeprice_battery_range_android_auto_max", value[1])
.apply()
}
/** App start counter, introduced with Version 1.0.0 */
var appStartCounter: Long
get() = sp.getLong("app_start_counter", 0)
@@ -248,6 +168,12 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putLong("chargeprice_counter", value).apply()
}
var chargepriceRemoval2025DialogShown: Boolean
get() = sp.getBoolean("chargeprice_removal_2025_dialog_shown", false)
set(value) {
sp.edit().putBoolean("chargeprice_removal_2025_dialog_shown", value).apply()
}
var opensourceDonationsDialogLastShown: Instant
get() = Instant.ofEpochMilli(sp.getLong("opensource_donations_dialog_last_shown", 0L))
set(value) {
@@ -323,6 +249,18 @@ class PreferenceDataSource(val context: Context) {
set(value) {
sp.edit().putBoolean("privacy_accepted", value).apply()
}
var androidAutoCompassEnabled: Boolean
get() = sp.getBoolean("android_auto_compass_enabled", false)
set(value) {
sp.edit().putBoolean("android_auto_compass_enabled", value).apply()
}
var androidAutoNewMapScreenEnabled: Boolean
get() = sp.getBoolean("android_auto_new_map_screen_enabled", false)
set(value) {
sp.edit { putBoolean("android_auto_new_map_screen_enabled", value) }
}
}
fun SharedPreferences.getLatLng(key: String): LatLng? =

View File

@@ -23,7 +23,7 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
var insertJob: Job? = null
val result = api.fullDownload()
val chargerIds = mutableListOf<Long>()
val idsToDelete = chargeLocations.getAllIds(api.id).toMutableSet()
result.chargers.chunked(1024).forEach {
insertJob?.join()
insertJob = withContext(Dispatchers.IO) {
@@ -31,11 +31,11 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
chargeLocations.insert(*it.toTypedArray())
}
}
chargerIds.addAll(it.map { it.id })
idsToDelete.removeAll(it.map { it.id })
}
// delete chargers that have been removed
chargeLocations.deleteIdNotIn(api.id, chargerIds)
chargeLocations.deleteById(api.id, idsToDelete.toList())
when (api) {
is OpenStreetMapApiWrapper -> {

View File

@@ -254,19 +254,6 @@ fun setChargepriceTagColor(view: TextView, kind: String) {
)
}
@BindingAdapter("chargepriceTagIcon")
fun setChargepriceTagIcon(view: TextView, kind: String) {
view.setCompoundDrawablesRelativeWithIntrinsicBounds(
when (kind) {
"star" -> R.drawable.ic_chargeprice_star
"alert" -> R.drawable.ic_chargeprice_alert
"info" -> R.drawable.ic_chargeprice_info
"lock" -> R.drawable.ic_chargeprice_lock
else -> 0
}, 0, 0, 0
)
}
private fun availabilityColor(
status: List<ChargepointStatus>?,
context: Context

View File

@@ -0,0 +1,27 @@
package net.vonforst.evmap.ui
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.URLSpan
import android.view.View
import androidx.core.text.getSpans
import net.vonforst.evmap.MapsActivity
class CustomUrlSpan(url: String): URLSpan(url) {
override fun onClick(widget: View) {
(widget.context as? MapsActivity)?.let {
it.openUrl(url, widget.rootView)
} ?: {
super.onClick(widget)
}
}
}
fun Spanned.replaceUrlSpansWithCustom(): Spanned {
val builder = SpannableStringBuilder(this)
builder.getSpans<URLSpan>().forEach {
builder.setSpan(CustomUrlSpan(it.url), builder.getSpanStart(it), builder.getSpanEnd(it), builder.getSpanFlags(it))
builder.removeSpan(it)
}
return builder
}

View File

@@ -1,320 +0,0 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.viewModelScope
import jsonapi.Meta
import jsonapi.Relationship
import jsonapi.Relationships
import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.chargeprice.ChargePrice
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import retrofit2.HttpException
import java.io.IOException
class ChargepriceViewModel(
application: Application,
chargepriceApiKey: String,
chargepriceApiUrl: String,
private val state: SavedStateHandle
) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
private var prefs = PreferenceDataSource(application)
val charger: MutableLiveData<ChargeLocation> by lazy {
state.getLiveData("charger")
}
val chargepoint: MutableLiveData<Chargepoint?> by lazy {
state.getLiveData("chargepoint")
}
private val vehicleIds: MutableLiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>().apply {
value = prefs.chargepriceMyVehicles
}
}
val vehicles: LiveData<Resource<List<ChargepriceCar>>> by lazy {
MediatorLiveData<Resource<List<ChargepriceCar>>>().apply {
addSource(vehicleIds.distinctUntilChanged()) { vehicleIds ->
if (vehicleIds.isEmpty()) {
value = Resource.success(emptyList())
} else {
value = Resource.loading(null)
viewModelScope.launch {
value = try {
val result = api.getVehicles()
Resource.success(result.filter {
it.id in vehicleIds
})
} catch (e: IOException) {
Resource.error(e.message, null)
} catch (e: HttpException) {
Resource.error(e.message, null)
}
}
}
}
observeForever {
vehicle.value = it.data?.firstOrNull()
}
}
}
val vehicle: MutableLiveData<ChargepriceCar> by lazy {
state.getLiveData("vehicle")
}
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
MediatorLiveData<List<String>>().apply {
addSource(vehicle) {
value = it?.compatibleEvmapConnectors
}
}
}
val noCompatibleConnectors: LiveData<Boolean> by lazy {
MediatorLiveData<Boolean>().apply {
value = false
listOf(charger, vehicleCompatibleConnectors).forEach {
addSource(it) {
val charger = charger.value ?: return@addSource
val connectors = vehicleCompatibleConnectors.value ?: return@addSource
value = !charger.chargepoints.flatMap { equivalentPlugTypes(it.type) }
.any { it in connectors }
}
}
}
}
val batteryRange: MutableLiveData<List<Float>> by lazy {
MutableLiveData<List<Float>>().apply {
value = prefs.chargepriceBatteryRange
observeForever {
if (it[0] == it[1]) {
value = if (it[0] < 1.0) {
listOf(it[0], it[1] + 1)
} else {
listOf(it[0] - 1, it[1])
}
}
prefs.chargepriceBatteryRange = value!!
}
}
}
val batteryRangeSliderDragging: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = false
}
}
val chargePrices: MutableLiveData<Resource<List<ChargePrice>>> by lazy {
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
value = state["chargePrices"] ?: Resource.loading(null)
listOf(
vehicle,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors,
myTariffs, myTariffsAll, charger
).forEach {
addSource(it.distinctUntilChanged()) {
if (!batteryRangeSliderDragging.value!!) {
loadPrices()
state["chargePrices"] = this.value
}
}
}
}
}
val chargePriceMeta: MutableLiveData<Resource<ChargepriceMeta>> by lazy {
MutableLiveData<Resource<ChargepriceMeta>>().apply {
value = Resource.loading(null)
}
}
val chargePricesForChargepoint: MediatorLiveData<Resource<List<ChargePrice>>> by lazy {
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
listOf(chargePrices, chargepoint).forEach {
addSource(it) {
val cps = chargePrices.value
val chargepoint = chargepoint.value
if (cps == null || chargepoint == null) {
value = null
} else if (cps.status == Status.ERROR) {
value = Resource.error(cps.message, null)
} else if (cps.status == Status.LOADING) {
value = Resource.loading(null)
} else {
val myTariffs = prefs.chargepriceMyTariffs
value = Resource.success(cps.data!!.mapNotNull { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == getChargepricePlugType(chargepoint) && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.copy(
chargepointPrices = filteredPrices
)
}
}
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariffId in myTariffs
}
)
}
}
}
}
}
fun reloadPrefs() {
vehicleIds.value = prefs.chargepriceMyVehicles
}
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
return type
}
val myTariffs: LiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>().apply {
value = prefs.chargepriceMyTariffs
}
}
val myTariffsAll: LiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = prefs.chargepriceMyTariffsAll
}
}
val chargepriceMetaForChargepoint: MediatorLiveData<Resource<ChargepriceChargepointMeta>> by lazy {
MediatorLiveData<Resource<ChargepriceChargepointMeta>>().apply {
listOf(chargePriceMeta, chargepoint).forEach {
addSource(it) {
val cpMeta = chargePriceMeta.value
val chargepoint = chargepoint.value
if (cpMeta == null || chargepoint == null) {
value = null
} else if (cpMeta.status == Status.ERROR) {
value = Resource.error(cpMeta.message, null)
} else if (cpMeta.status == Status.LOADING) {
value = Resource.loading(null)
} else {
val result = cpMeta.data!!.chargePoints.filter {
it.plug == getChargepricePlugType(
chargepoint
) && it.power == chargepoint.power
}.elementAtOrNull(0)
value = if (result != null) {
Resource.success(result)
} else {
Resource.error("matching chargepoint not found", null)
}
}
}
}
}
}
private var loadPricesJob: Job? = null
fun loadPrices() {
chargePrices.value = Resource.loading(null)
chargePriceMeta.value = Resource.loading(null)
val charger = charger.value
val car = vehicle.value
val compatibleConnectors = vehicleCompatibleConnectors.value
val myTariffs = myTariffs.value
val myTariffsAll = myTariffsAll.value
if (charger == null || car == null || compatibleConnectors == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
chargePrices.value = Resource.error(null, null)
return
}
val cpStation = ChargepriceStation.fromEvmap(charger, compatibleConnectors)
if (cpStation.chargePoints.isEmpty()) {
// no compatible connectors
chargePrices.value = Resource.success(emptyList())
chargePriceMeta.value = Resource.success(ChargepriceMeta(emptyList()))
return
}
loadPricesJob?.cancel()
loadPricesJob = viewModelScope.launch {
try {
val result = api.getChargePrices(
ChargepriceRequest(
dataAdapter = ChargepriceApi.getDataAdapter(charger),
station = cpStation,
vehicle = car,
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency,
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad,
showPriceUnavailable = true
),
relationships = if (!myTariffsAll) {
Relationships(
"tariffs" to Relationship.ToMany(
(myTariffs ?: emptySet()).map {
ResourceIdentifier(
"tariff",
id = it
)
},
meta = Meta.from(
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
ChargepriceApi.moshi
)
)
)
} else null
), ChargepriceApi.getChargepriceLanguage()
)
val meta = result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
chargePrices.value = Resource.success(result.data)
chargePriceMeta.value = Resource.success(meta)
} catch (e: IOException) {
chargePrices.value = Resource.error(e.message, null)
chargePriceMeta.value = Resource.error(e.message, null)
} catch (e: HttpException) {
chargePrices.value = Resource.error(e.message, null)
chargePriceMeta.value = Resource.error(e.message, null)
}
}
}
fun resetBatteryRangeToDefault() {
batteryRange.value = prefs.chargepriceBatteryRangeAndroidAuto
}
}

View File

@@ -6,38 +6,16 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import retrofit2.HttpException
import java.io.IOException
class SettingsViewModel(
application: Application,
chargepriceApiKey: String,
chargepriceApiUrl: String
) :
AndroidViewModel(application) {
private val api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
value = Resource.loading(null)
loadVehicles()
}
}
val tariffs: MutableLiveData<Resource<List<ChargepriceTariff>>> by lazy {
MutableLiveData<Resource<List<ChargepriceTariff>>>().apply {
value = Resource.loading(null)
loadTariffs()
}
}
val chargerCacheCount: LiveData<Long> by lazy {
db.chargeLocationsDao().getCount()
}
@@ -52,32 +30,6 @@ class SettingsViewModel(
}
}
private fun loadVehicles() {
viewModelScope.launch {
try {
val result = api.getVehicles()
vehicles.value = Resource.success(result)
} catch (e: IOException) {
vehicles.value = Resource.error(e.message, null)
} catch (e: HttpException) {
vehicles.value = Resource.error(e.message, null)
}
}
}
private fun loadTariffs() {
viewModelScope.launch {
try {
val result = api.getTariffs()
tariffs.value = Resource.success(result)
} catch (e: IOException) {
tariffs.value = Resource.error(e.message, null)
} catch (e: HttpException) {
tariffs.value = Resource.error(e.message, null)
}
}
}
fun deleteRecentSearchResults() {
viewModelScope.launch {
db.recentAutocompletePlaceDao().deleteAll()

View File

@@ -1,5 +0,0 @@
<vector android:height="15.811624dp" android:viewportHeight="131.5"
android:tint="?attr/colorControlNormal"
android:viewportWidth="199.6" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M197.544,65.685l-9.2,-4.8l8,-4.2c2.7,-1.4 2.7,-3.8 0,-5.2l-8.6,-4.5l8.6,-4.5c2.7,-1.4 2.7,-3.8 0,-5.2l-68.9,-36.1c-2.7,-1.4 -7.2,-1.4 -9.9,0l-115.5,59.7c-2.7,1.4 -2.7,3.7 0,5.1l8.8,4.5l-8.8,4.6c-2.7,1.4 -2.7,3.7 0,5.1l9.4,4.8l-8.2,4.3c-2.7,1.4 -2.7,3.7 0,5.1l70.4,36.2c2.7,1.4 7.2,1.4 9.9,0l114,-59.6C200.344,69.385 200.344,67.085 197.544,65.685L197.544,65.685zM123.144,18.785L105.844,38.685c-0.9,1 -0.6,2.3 0.6,2.9l13.7,7.1c1.2,0.6 1.2,1.6 0,2.2l-43.1,22.3c-1.2,0.6 -1.4,0.3 -0.6,-0.7l17.3,-19.9c0.9,-1 0.6,-2.3 -0.6,-2.9l-13.7,-7.1c-1.2,-0.6 -1.2,-1.6 0,-2.2l43.1,-22.3C123.744,17.485 123.944,17.785 123.144,18.785L123.144,18.785z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector android:height="20dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="20dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>

View File

@@ -1,10 +0,0 @@
<vector android:height="20dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="20dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
</vector>

View File

@@ -1,10 +0,0 @@
<vector android:height="16dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>

View File

@@ -1,10 +0,0 @@
<vector android:height="16dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="@android:color/white"
android:pathData="M12,10.9c-0.61,0 -1.1,0.49 -1.1,1.1s0.49,1.1 1.1,1.1c0.61,0 1.1,-0.49 1.1,-1.1s-0.49,-1.1 -1.1,-1.1zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM14.19,14.19L6,18l3.81,-8.19L18,6l-3.81,8.19z" />
</vector>

View File

@@ -1,149 +0,0 @@
<vector android:height="26dp"
android:viewportHeight="257.0819"
android:viewportWidth="1289.0747"
android:width="130.4dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#000000"
android:pathData="m339.23,124.6q14.22,0 23.58,4.5 9.54,4.5 9.54,11.52 0,3.06 -1.98,5.58 -1.98,2.34 -5.04,2.34 -2.34,0 -3.78,-0.72 -1.26,-0.72 -3.6,-2.34 -1.08,-1.08 -3.42,-2.52 -2.16,-1.08 -6.12,-1.8 -3.96,-0.72 -7.2,-0.72 -9.36,0 -16.56,4.32 -7.2,4.32 -11.16,12.06 -3.96,7.56 -3.96,16.92 0,9.54 3.78,17.1 3.96,7.56 10.98,11.88 7.02,4.32 16.02,4.32 9.36,0 15.12,-2.88 1.26,-0.72 3.42,-2.34 1.8,-1.44 3.06,-2.16 1.44,-0.72 3.42,-0.72 3.6,0 5.58,2.34 2.16,2.16 2.16,5.76 0,3.78 -4.86,7.56 -4.68,3.6 -12.78,5.94 -7.92,2.34 -17.1,2.34 -13.68,0 -24.12,-6.3 -10.44,-6.48 -16.2,-17.64 -5.58,-11.34 -5.58,-25.2 0,-13.86 5.94,-25.02 5.94,-11.34 16.56,-17.64 10.62,-6.48 24.3,-6.48z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m437.63,125.14q31.68,0 31.68,39.24l0,48.06q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.52 -2.34,-6.12l0,-48.06q0,-23.4 -19.8,-23.4 -10.62,0 -17.64,6.84 -7.02,6.66 -7.02,16.56l0,48.06q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-115.92q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,45.54q4.5,-7.02 12.6,-11.88 8.1,-5.04 17.28,-5.04z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m571.21,125.5q3.78,0 6.12,2.52 2.52,2.34 2.52,6.3l0,78.12q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-4.68q-4.68,6.3 -12.78,10.8 -8.1,4.32 -17.46,4.32 -12.24,0 -22.32,-6.3 -9.9,-6.3 -15.66,-17.46 -5.58,-11.34 -5.58,-25.38 0,-14.04 5.58,-25.2 5.76,-11.34 15.66,-17.64 9.9,-6.3 21.78,-6.3 9.54,0 17.64,3.96 8.28,3.96 13.14,10.08l0,-4.32q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52zM534.49,207.04q8.46,0 14.94,-4.32 6.66,-4.32 10.26,-11.88 3.78,-7.56 3.78,-17.1 0,-9.36 -3.78,-16.92 -3.6,-7.56 -10.26,-11.88 -6.48,-4.5 -14.94,-4.5 -8.46,0 -15.12,4.32 -6.48,4.32 -10.26,11.88 -3.6,7.56 -3.6,17.1 0,9.54 3.6,17.1 3.78,7.56 10.26,11.88 6.66,4.32 15.12,4.32z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m652.08,124.6q4.68,0 8.1,2.52 3.42,2.34 3.42,5.94 0,4.32 -2.34,6.66 -2.16,2.16 -5.4,2.16 -1.62,0 -4.86,-1.08 -3.78,-1.26 -5.94,-1.26 -5.58,0 -10.98,3.96 -5.22,3.78 -8.64,10.62 -3.24,6.66 -3.24,14.94l0,43.38q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-77.04q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,9.18q3.96,-8.82 11.88,-14.22 7.92,-5.58 18,-5.76z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m755.31,125.5q3.78,0 6.12,2.52 2.52,2.34 2.52,6.3l0,79.2q0,14.58 -6.3,24.3 -6.12,9.9 -16.74,14.58 -10.62,4.68 -23.94,4.68 -7.2,0 -16.92,-2.52 -9.54,-2.52 -12.24,-5.22 -5.58,-2.88 -5.58,-7.2 0,-1.08 0.72,-2.88 1.98,-4.5 6.66,-4.5 2.34,0 5.04,1.08 14.4,5.58 22.5,5.58 14.4,0 21.96,-7.02 7.74,-6.84 7.74,-18.9l0,-9.72q-3.78,7.02 -12.78,12.06 -8.82,5.04 -18.72,5.04 -12.42,0 -22.68,-6.3 -10.26,-6.3 -16.2,-17.46 -5.76,-11.34 -5.76,-25.38 0,-14.04 5.76,-25.2 5.94,-11.34 16.02,-17.64 10.26,-6.3 22.5,-6.3 9.9,0 18.36,4.5 8.64,4.5 13.5,10.98l0,-5.76q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52zM717.33,207.04q8.82,0 15.66,-4.14 6.84,-4.32 10.62,-11.88 3.96,-7.74 3.96,-17.28 0,-9.54 -3.96,-17.1 -3.78,-7.56 -10.62,-11.88 -6.84,-4.32 -15.66,-4.32 -8.64,0 -15.48,4.32 -6.84,4.32 -10.8,12.06 -3.78,7.56 -3.78,16.92 0,9.36 3.78,17.1 3.96,7.56 10.8,11.88 6.84,4.32 15.48,4.32z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m868.61,170.32q-0.18,3.24 -2.7,5.58 -2.52,2.16 -5.94,2.16l-63.36,0q1.26,13.14 9.9,21.06 8.82,7.92 21.42,7.92 8.64,0 14.04,-2.52 5.4,-2.52 9.54,-6.48 2.7,-1.62 5.22,-1.62 3.06,0 5.04,2.16 2.16,2.16 2.16,5.04 0,3.78 -3.6,6.84 -5.22,5.22 -13.86,8.82 -8.64,3.6 -17.64,3.6 -14.58,0 -25.74,-6.12 -10.98,-6.12 -17.1,-17.1 -5.94,-10.98 -5.94,-24.84 0,-15.12 6.12,-26.46 6.3,-11.52 16.38,-17.64 10.26,-6.12 21.96,-6.12 11.52,0 21.6,5.94 10.08,5.94 16.2,16.38 6.12,10.44 6.3,23.4zM824.51,140.44q-10.08,0 -17.46,5.76 -7.38,5.58 -9.72,17.46l53.1,0l0,-1.44q-0.9,-9.54 -8.64,-15.66 -7.56,-6.12 -17.28,-6.12z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m935.8,125.14q12.24,0 22.14,6.3 9.9,6.12 15.48,17.28 5.76,11.16 5.76,25.2 0,14.04 -5.76,25.2 -5.58,10.98 -15.48,17.28 -9.9,6.3 -21.78,6.3 -9.36,0 -17.46,-4.14 -8.1,-4.14 -13.14,-10.08l0,39.96q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.34 -2.34,-6.12l0,-113.22q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.3l0,5.22q4.32,-6.3 12.6,-10.8 8.28,-4.5 17.64,-4.5zM933.82,206.86q8.28,0 14.94,-4.32 6.66,-4.32 10.26,-11.7 3.78,-7.56 3.78,-16.92 0,-9.36 -3.78,-16.74 -3.6,-7.56 -10.26,-11.88 -6.66,-4.32 -14.94,-4.32 -8.46,0 -15.12,4.32 -6.66,4.14 -10.44,11.7 -3.6,7.56 -3.6,16.92 0,9.36 3.6,16.92 3.78,7.56 10.44,11.88 6.66,4.14 15.12,4.14z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1045.83,124.6q4.68,0 8.1,2.52 3.42,2.34 3.42,5.94 0,4.32 -2.34,6.66 -2.16,2.16 -5.4,2.16 -1.62,0 -4.86,-1.08 -3.78,-1.26 -5.94,-1.26 -5.58,0 -10.98,3.96 -5.22,3.78 -8.64,10.62 -3.24,6.66 -3.24,14.94l0,43.38q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-77.04q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,9.18q3.96,-8.82 11.88,-14.22 7.92,-5.58 18,-5.76z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1089.23,212.44q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.52 -2.34,-6.12l0,-77.94q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12zM1080.59,113.98q-5.22,0 -7.56,-1.8 -2.16,-1.98 -2.16,-6.12l0,-2.88q0,-4.32 2.34,-6.12 2.52,-1.8 7.56,-1.8 5.04,0 7.2,1.98 2.34,1.8 2.34,5.94l0,2.88q0,4.32 -2.34,6.12 -2.34,1.8 -7.38,1.8z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1154.85,124.6q14.22,0 23.58,4.5 9.54,4.5 9.54,11.52 0,3.06 -1.98,5.58 -1.98,2.34 -5.04,2.34 -2.34,0 -3.78,-0.72 -1.26,-0.72 -3.6,-2.34 -1.08,-1.08 -3.42,-2.52 -2.16,-1.08 -6.12,-1.8 -3.96,-0.72 -7.2,-0.72 -9.36,0 -16.56,4.32 -7.2,4.32 -11.16,12.06 -3.96,7.56 -3.96,16.92 0,9.54 3.78,17.1 3.96,7.56 10.98,11.88 7.02,4.32 16.02,4.32 9.36,0 15.12,-2.88 1.26,-0.72 3.42,-2.34 1.8,-1.44 3.06,-2.16 1.44,-0.72 3.42,-0.72 3.6,0 5.58,2.34 2.16,2.16 2.16,5.76 0,3.78 -4.86,7.56 -4.68,3.6 -12.78,5.94 -7.92,2.34 -17.1,2.34 -13.68,0 -24.12,-6.3 -10.44,-6.48 -16.2,-17.64 -5.58,-11.34 -5.58,-25.2 0,-13.86 5.94,-25.02 5.94,-11.34 16.56,-17.64 10.62,-6.48 24.3,-6.48z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1289.07,170.32q-0.18,3.24 -2.7,5.58 -2.52,2.16 -5.94,2.16l-63.36,0q1.26,13.14 9.9,21.06 8.82,7.92 21.42,7.92 8.64,0 14.04,-2.52 5.4,-2.52 9.54,-6.48 2.7,-1.62 5.22,-1.62 3.06,0 5.04,2.16 2.16,2.16 2.16,5.04 0,3.78 -3.6,6.84 -5.22,5.22 -13.86,8.82 -8.64,3.6 -17.64,3.6 -14.58,0 -25.74,-6.12 -10.98,-6.12 -17.1,-17.1 -5.94,-10.98 -5.94,-24.84 0,-15.12 6.12,-26.46 6.3,-11.52 16.38,-17.64 10.26,-6.12 21.96,-6.12 11.52,0 21.6,5.94 10.08,5.94 16.2,16.38 6.12,10.44 6.3,23.4zM1244.97,140.44q-10.08,0 -17.46,5.76 -7.38,5.58 -9.72,17.46l53.1,0l0,-1.44q-0.9,-9.54 -8.64,-15.66 -7.56,-6.12 -17.28,-6.12z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m321.33,1q5.1,0 9.7,3.1 4.6,3 7.4,8.2 2.8,5.1 2.8,11.2 0,6 -2.8,11.2 -2.8,5.2 -7.4,8.3 -4.6,3 -9.7,3l-17.4,0l0,18.9q0,2.7 -1.6,4.4 -1.6,1.7 -4.2,1.7 -2.5,0 -4.1,-1.7 -1.6,-1.8 -1.6,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8zM321.33,34.6q1.9,0 3.7,-1.6 1.9,-1.6 3,-4.1 1.2,-2.6 1.2,-5.4 0,-2.8 -1.2,-5.3 -1.1,-2.6 -3,-4.1 -1.8,-1.6 -3.7,-1.6l-17.4,0l0,22.1z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m417.18,36q0,9.9 -4.4,18.2 -4.4,8.2 -12.2,13 -7.7,4.8 -17.4,4.8 -9.7,0 -17.5,-4.8 -7.7,-4.8 -12.1,-13 -4.3,-8.3 -4.3,-18.2 0,-9.9 4.3,-18.1 4.4,-8.3 12.1,-13.1 7.8,-4.8 17.5,-4.8 9.7,0 17.4,4.8 7.8,4.8 12.2,13.1 4.4,8.2 4.4,18.1zM404.18,36q0,-6.7 -2.7,-12.1 -2.7,-5.5 -7.5,-8.7 -4.8,-3.2 -10.8,-3.2 -6.1,0 -10.9,3.2 -4.7,3.1 -7.4,8.6 -2.6,5.5 -2.6,12.2 0,6.7 2.6,12.2 2.7,5.5 7.4,8.7 4.8,3.1 10.9,3.1 6,0 10.8,-3.2 4.8,-3.2 7.5,-8.6 2.7,-5.5 2.7,-12.2z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m506.87,0.7q2.4,0 4.4,1.9 2.1,1.8 2.1,4.6 0,0.9 -0.3,2l-19.7,58q-0.6,1.7 -2.1,2.7 -1.5,1 -3.3,1.1 -1.8,0 -3.4,-1 -1.6,-1 -2.5,-2.9l-14.2,-32.3 -14.3,32.3q-0.9,1.9 -2.5,2.9 -1.6,1 -3.4,1 -1.8,-0.1 -3.3,-1.1 -1.5,-1 -2.1,-2.7l-19.7,-58q-0.3,-1.1 -0.3,-2 0,-2.8 2,-4.6 2.1,-1.9 4.6,-1.9 2,0 3.6,1.1 1.6,1 2.2,2.8l14.9,45.2 13,-31.2q0.8,-1.8 2.3,-2.8 1.5,-1.1 3.4,-1 1.9,-0.1 3.3,1 1.5,1 2.3,2.8l12.3,30.9 14.8,-44.9q0.6,-1.8 2.2,-2.8 1.7,-1.1 3.7,-1.1z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m562.11,59.5q2.6,0 4.3,1.8 1.8,1.7 1.8,4 0,2.5 -1.8,4.1 -1.7,1.6 -4.3,1.6l-33.5,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l33.5,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-27.1,0l0,17l22.6,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-22.6,0l0,18.5z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m634.63,61.2q1.3,0.8 2,2.1 0.8,1.3 0.8,2.7 0,1.8 -1.2,3.3 -1.5,1.8 -4.6,1.8 -2.4,0 -4.4,-1.1 -7.2,-4.1 -7.2,-16.7 0,-3.6 -2.4,-5.7 -2.3,-2.1 -6.7,-2.1L592.23,45.5l0,19.4q0,2.7 -1.5,4.4 -1.4,1.7 -3.8,1.7 -2.9,0 -5.1,-1.7 -2.1,-1.8 -2.1,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l28.8,0q5.2,0 9.8,2.8 4.6,2.8 7.3,7.7 2.8,4.9 2.8,11 0,5 -2.7,9.8 -2.7,4.7 -7,7.5 6.3,4.4 6.9,11.8 0.3,1.6 0.3,3.1 0.4,3.1 0.8,4.5 0.4,1.3 1.8,2zM614.13,35.2q1.8,0 3.5,-1.7 1.7,-1.7 2.8,-4.5 1.1,-2.9 1.1,-6.2 0,-2.8 -1.1,-5.1 -1.1,-2.4 -2.8,-3.8 -1.7,-1.4 -3.5,-1.4l-21.9,0l0,22.7z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m688.08,59.5q2.6,0 4.3,1.8 1.8,1.7 1.8,4 0,2.5 -1.8,4.1 -1.7,1.6 -4.3,1.6l-33.5,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l33.5,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-27.1,0l0,17l22.6,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-22.6,0l0,18.5z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m735.71,1q9.4,0 16.1,4.7 6.8,4.6 10.3,12.6 3.6,7.9 3.6,17.7 0,9.8 -3.6,17.8 -3.5,7.9 -10.3,12.6 -6.7,4.6 -16.1,4.6l-23.9,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8zM734.71,59.5q9,0 13.5,-6.6 4.5,-6.7 4.5,-16.9 0,-10.2 -4.6,-16.8 -4.5,-6.7 -13.4,-6.7l-16.5,0l0,47z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m847.12,32.2q5.3,2.1 8.6,6.4 3.4,4.3 3.4,11.1 0,11.9 -6.8,16.6 -6.8,4.7 -16.2,4.7l-24.9,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l25.2,0q19,0 19,17.8 0,4.5 -2.2,8 -2.1,3.4 -6.1,5.4zM842.42,21q0,-4.1 -2.1,-6.1 -2,-2.1 -5.7,-2.1l-16.5,0l0,15.6l16.8,0q3,0 5.2,-2 2.3,-2 2.3,-5.4zM836.12,59.5q4.7,0 7.3,-2.5 2.7,-2.5 2.7,-7.3 0,-5.9 -3.1,-7.7 -3.1,-1.8 -7.6,-1.8l-17.3,0l0,19.3z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m918.81,6.9q0,2 -1.1,3.7l-20.9,29.9l0,24.4q0,2.6 -1.7,4.4 -1.7,1.7 -4.1,1.7 -2.5,0 -4.3,-1.7 -1.7,-1.8 -1.7,-4.4l0,-25.8l-20.8,-27.6q-1.8,-2.4 -1.8,-4.7 0,-2.6 2,-4.3 2.1,-1.8 4.4,-1.8 2.8,0 4.9,2.8l17.6,24.3 16.5,-24.1q2.1,-3 5,-3 2.4,0 4.2,1.8 1.8,1.8 1.8,4.4z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000016"
android:pathData="m246.94,173.65 l-11.5,-6.03 10.02,-5.24c3.41,-1.78 3.42,-4.71 0.01,-6.5l-10.76,-5.65 10.76,-5.62c3.41,-1.79 3.42,-4.71 0.01,-6.5L159.4,92.94c-3.41,-1.79 -9,-1.81 -12.42,-0.04L2.56,167.49c-3.42,1.77 -3.42,4.65 0.01,6.41l11.02,5.66 -11.02,5.69c-3.42,1.77 -3.42,4.65 0.01,6.41l11.76,6.04 -10.29,5.31c-3.42,1.77 -3.42,4.65 0.01,6.41l88.01,45.22c3.43,1.76 9.02,1.74 12.43,-0.04l142.46,-74.47c3.41,-1.78 3.41,-4.71 0,-6.5zM153.91,115.02 L132.31,139.92c-1.08,1.25 -0.76,2.88 0.7,3.64l17.18,8.83c1.47,0.75 1.47,1.99 0,2.75l-53.92,27.85c-1.47,0.76 -1.78,0.36 -0.7,-0.89l21.59,-24.9c1.08,-1.25 0.77,-2.88 -0.7,-3.64l-17.18,-8.83c-1.47,-0.75 -1.47,-1.99 0,-2.75l53.92,-27.85c1.47,-0.76 1.78,-0.36 0.7,0.89z" />
</vector>

View File

@@ -543,9 +543,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/go_to_chargeprice"
android:transitionName="@string/shared_element_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:icon="@drawable/ic_chargeprice" />
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}" />
<Button
android:id="@+id/btnChargerWebsite"

View File

@@ -1,141 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<variable
name="vm"
type="ChargepriceViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:transitionName="@string/shared_element_chargeprice">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize">
<ImageView
android:id="@+id/imgChargepriceLogo"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/powered_by_chargeprice"
android:focusable="true"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:layout_gravity="right"
app:srcCompat="@drawable/ic_powered_by_chargeprice"
app:tint="?android:textColorPrimary"
tools:ignore="RtlSymmetry" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/charge_prices_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
tools:itemCount="1"
tools:listitem="@layout/fragment_chargeprice_preview" />
<ProgressBar
android:id="@+id/progressBar5"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="280dp"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView8"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="280dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_tariffs_found"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView9"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="280dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_compatible_connectors"
app:goneUnless="@{vm.noCompatibleConnectors}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="280dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_select_car_first"
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS &amp;&amp; vm.vehicles.data.size() == 0}"
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
app:layout_constraintVertical_chainStyle="packed" />
<Button
android:id="@+id/btnSettings"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings"
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS &amp;&amp; vm.vehicles.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -1,122 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<variable
name="vm"
type="ChargepriceViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/chargeprice_select_connector"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vehicle_selection" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/connectors_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:data="@{vm.charger.chargepointsMerged}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
tools:itemCount="3"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_connector_button"
tools:orientation="horizontal" />
<TextView
android:id="@+id/tvChargeFromTo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:clickable="true"
android:focusable="true"
android:background="?selectableItemBackground"
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
tools:text="Charge from 20% to 80%" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging &amp;&amp; vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
app:layout_constraintStart_toStartOf="@+id/tvChargeFromTo"
app:layout_constraintTop_toBottomOf="@+id/tvChargeFromTo"
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
<TextView
android:id="@+id/tvVehicleHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/chargeprice_vehicle"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{vm.vehicles.data != null &amp;&amp; vm.vehicles.data.size() > 1}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/vehicle_selection"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvVehicleHeader"
app:data="@{vm.vehicles.data}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:goneUnless="@{vm.vehicles.data != null &amp;&amp; vm.vehicles.data.size() > 1}"
android:orientation="horizontal"
tools:listitem="@layout/item_chargeprice_vehicle_chip" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/battery_range"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:valueFrom="0.0"
android:valueTo="100.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView4"
app:values="@={vm.batteryRange}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/fragment_chargeprice_header" />
<include layout="@layout/item_chargeprice" />
<include layout="@layout/item_chargeprice" />
<include layout="@layout/item_chargeprice" />
<include layout="@layout/item_chargeprice" />
<include layout="@layout/item_chargeprice" />
</LinearLayout>

View File

@@ -1,173 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.api.chargeprice.ChargePrice" />
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="java.util.Set" />
<variable
name="item"
type="ChargePrice" />
<variable
name="meta"
type="ChargepriceChargepointMeta" />
<variable
name="myTariffs"
type="Set&lt;String>" />
<variable
name="myTariffsAll"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll &amp;&amp; myTariffs.contains(item.tariffId), item.branding.backgroundColor)}">
<TextView
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.tariffName}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="CheapCharge" />
<TextView
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.provider}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
app:layout_constraintBottom_toTopOf="@+id/rvTags"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
tools:text="Cheap Charging Co." />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:data="@{item.tags}"
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
tools:itemCount="1"
tools:listitem="@layout/item_chargeprice_tag" />
<TextView
android:id="@+id/txtProviderCustomerTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/chargeprice_provider_customer_tariff"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.providerCustomerTariff}"
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
<TextView
android:id="@+id/txtMonthlyFee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.formatMonthlyFees(context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
tools:text="Base fee 1 €/month" />
<TextView
android:id="@+id/txtPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="end"
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(@string/charge_price_format, item.chargepointPrices.get(0).price, BindingAdaptersKt.currency(item.currency)) : @string/chargeprice_price_not_available}"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="@+id/txtAveragePrice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline5"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="1,50 €" />
<TextView
android:id="@+id/txtAveragePrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="end"
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(item.chargepointPrices.get(0).priceDistribution.isOnlyKwh ? @string/charge_price_kwh_format : @string/charge_price_average_format, item.chargepointPrices.get(0).price / meta.energy, BindingAdaptersKt.currency(item.currency)) : item.chargepointPrices.get(0).noPriceReason}"
app:goneUnless="@{item.chargepointPrices.get(0).price > 0 || item.chargepointPrices.get(0).price == null}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintBottom_toTopOf="@id/txtPriceDetails"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline5"
app:layout_constraintTop_toBottomOf="@+id/txtPrice"
tools:text="⌀ 0,29 €/kWh" />
<TextView
android:id="@+id/txtPriceDetails"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="end"
android:text="@{item.chargepointPrices.get(0).formatDistribution(context)}"
app:goneUnless="@{!item.chargepointPrices.get(0).formatDistribution(context).empty}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline5"
app:layout_constraintTop_toBottomOf="@+id/txtAveragePrice"
tools:text="pro kWh + ab 4h Blockiergeb." />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.65" />
<ImageView
android:id="@+id/ivLogo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="8dp"
android:scaleType="fitCenter"
app:invisibleUnless="@{item.branding.logoUrl != null}"
app:imageUrl="@{item.branding.logoUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceTag" />
<variable
name="item"
type="ChargepriceTag" />
</data>
<net.vonforst.evmap.ui.BalancedBreakingTextView
android:id="@+id/rvTags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@drawable/rounded_rect_16dp"
android:maxLines="3"
android:text="@{item.text}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?android:textColorPrimary"
android:theme="@style/ThemeOverlay.Material3.Dark"
android:gravity="center_vertical"
android:drawablePadding="4dp"
android:paddingEnd="8dp"
android:paddingStart="3dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:drawableTint="?android:textColorPrimary"
android:breakStrategy="balanced"
app:chargepriceTagColor="@{item.kind}"
app:chargepriceTagIcon="@{item.kind}"
tools:backgroundTint="@color/chargeprice_alert"
tools:drawableLeft="@drawable/ic_chargeprice_alert"
tools:text="Only for drivers of blue cars" />
</layout>

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceCar" />
<variable
name="item"
type="ChargepriceCar" />
<variable
name="selectedItem"
type="ChargepriceCar" />
</data>
<com.google.android.material.chip.Chip
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:text="@{item.brand + ' ' + item.name}"
android:checked="@{item == selectedItem}"
tools:text="Tesla Model 2" />
</layout>

View File

@@ -27,9 +27,6 @@
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_chargepriceFragment"
app:destination="@id/chargeprice" />
<action
android:id="@+id/action_map_to_opensource_donations"
app:destination="@id/opensource_donations" />
@@ -88,16 +85,6 @@
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/settings_chargeprice"
android:name="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
android:label="@string/settings_chargeprice"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_android_auto"
android:name="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
android:label="@string/settings_android_auto"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_developer"
android:name="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
@@ -126,25 +113,6 @@
android:name="net.vonforst.evmap.fragment.FilterProfilesFragment"
android:label="@string/menu_manage_filter_profiles"
tools:layout="@layout/fragment_filter_profiles" />
<fragment
android:id="@+id/chargeprice"
android:name="net.vonforst.evmap.fragment.ChargepriceFragment"
android:label="@string/chargeprice_title"
tools:layout="@layout/fragment_chargeprice">
<action
android:id="@+id/action_chargeprice_to_chargepriceSettingsFragment"
app:destination="@id/settings_chargeprice"
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<action
android:id="@+id/action_chargeprice_to_donateFragment"
app:destination="@id/donate" />
<argument
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation" />
</fragment>
<fragment
android:id="@+id/donate"
android:name="net.vonforst.evmap.fragment.DonateFragment"

View File

@@ -381,4 +381,16 @@
<string name="data_source_openstreetmap_desc">Experimentální podpora v EVMap, nejsou dostupné všechny funkce.</string>
<string name="downloading_chargers_percent">Stahování… %.0f%%</string>
<string name="plug_type_2_tethered">Provázaný kabel typ 2</string>
<string name="no_email_app_found">Nejprve si nainstalujte e-mailovou aplikaci</string>
<string name="filter_accessibility">Přístupnost nabíječky</string>
<string name="data_source_nobil">NOBIL</string>
<string name="data_source_nobil_desc"><![CDATA[Otevřená data poskytovaná vládou a komunitou ve Švédsku a Norsku.]]></string>
<string name="accessibility_public">Veřejné</string>
<string name="accessibility_visitors">Návštěvníci</string>
<string name="accessibility_employees">Zaměstnanci</string>
<string name="accessibility_by_appointment">Po domluvě</string>
<string name="accessibility_residents">Obyvatelé</string>
<string name="chargeprice_removal_2025_dialog_title">Omlouváme se!</string>
<string name="chargeprice_removal_2025_dialog_detail">Náklady na přístup k údajům ze služby Chargeprice prudce vzrostly a nelze je pokrýt z darů, takže EVMap již nemůže tyto údaje přímo zobrazovat. Prozatím se otevře webová stránka Chargeprice. Alternativní řešení se vyvíjí, ale bude to nějakou dobu trvat a zpočátku bude mít omezené funkce. Děkujeme za trpělivost a podporu!</string>
<string name="auto_use_new_map_screen">Nová obrazovka mapy (beta)</string>
</resources>

View File

@@ -231,7 +231,7 @@
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="data_source_openstreetmap">OpenStreetMap</string>
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in den deutschsprachigen Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
<string name="data_source_nobil_desc"><![CDATA[Offizielles Verzeichnis der nordischen Länder]]></string>
<string name="data_source_nobil_desc"><![CDATA[Offizielles Verzeichnis in Schweden und Norwegen]]></string>
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt und offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
<string name="data_source_openstreetmap_desc">Experimentelle Unterstützung in EVMap, nicht alle Funktionen nutzbar.</string>
<string name="next">weiter</string>
@@ -380,10 +380,13 @@
<string name="pref_chargeprice_native_integration_on">Preise werden direkt in EVMap angezeigt</string>
<string name="pref_chargeprice_native_integration_off">Preisvergleich verlinkt auf die App oder Website von Chargeprice</string>
<string name="auto_zoom_for_details">Für Details hineinzoomen</string>
<string name="plug_type_2_tethered">Typ 2 Kabel mit Stecker</string>
<string name="plug_type_2_tethered">Typ-2-Kabel mit Stecker</string>
<string name="accessibility_public">Öffentlich</string>
<string name="accessibility_visitors">Besucher</string>
<string name="accessibility_employees">Mitarbeiter</string>
<string name="accessibility_by_appointment">Nach Vereinbarung</string>
<string name="accessibility_residents">Bewohner</string>
<string name="chargeprice_removal_2025_dialog_title">Sorry!</string>
<string name="chargeprice_removal_2025_dialog_detail">Die Kosten für den Zugriff auf Chargeprice-Daten sind stark gestiegen und können nicht mehr durch Spenden gedeckt werden. Daher kann EVMap diese Daten nicht mehr direkt anzeigen. Hier öffnet sich nun die Chargeprice-Website. Eine alternative Lösung ist in Arbeit, wird aber Zeit brauchen und anfangs nur eingeschränkt funktionieren. Danke für eure Geduld und Unterstützung!</string>
<string name="auto_use_new_map_screen">Neue Kartendarstellung (beta)</string>
</resources>

View File

@@ -377,4 +377,16 @@
<string name="data_source_openstreetmap_desc">Katseline tugi EVMapis - kõik funktsionaalsused pole saadaval.</string>
<string name="downloading_chargers_percent">Laadin alla… %.0f%%</string>
<string name="plug_type_2_tethered">Tüüp 2 lõimitud kaabel</string>
<string name="no_email_app_found">Esmalt paigalda e-posti rakendus</string>
<string name="filter_accessibility">Laadija ligipääsetavus</string>
<string name="data_source_nobil">NOBIL</string>
<string name="data_source_nobil_desc"><![CDATA[Kogukonna poolt täiendatud riikide avaandmed Norrast ja Rootsist.]]></string>
<string name="accessibility_public">Avalik</string>
<string name="accessibility_visitors">Külastajatele</string>
<string name="accessibility_employees">Töötajatele</string>
<string name="accessibility_by_appointment">Broneeringu alusel</string>
<string name="accessibility_residents">Elanikele</string>
<string name="chargeprice_removal_2025_dialog_title">Vabandust!</string>
<string name="chargeprice_removal_2025_dialog_detail">Chargeprice\'i andmete maksumus on 2025. aastast järsult kasvanud ja rahalistest toetustest meile pole võimalik seda enam rahastada. Seega EVMap ei saa neid andmeid enam otse näidata. Asendusena on esialgu kasutusel link Chargeprice\'i veebisaiti. Oleme arendamas ka alternatiivset lahendust, aga selleks kulub aega ning ta võib kasutusele tulla piiratud funktsionaalsuses. Suur tänu teie toe eest!</string>
<string name="auto_use_new_map_screen">Uus kaardivaade (beetaversioon)</string>
</resources>

View File

@@ -9,14 +9,14 @@
<string name="operator">Opérateur</string>
<string name="network">Réseau</string>
<string name="hours">Heures d\'ouverture</string>
<string name="open_247"><b>Ouvert 24h/24 et 7j/7</b></string>
<string name="open_closesat"><b>Ouvert</b> · Ferme à %s</string>
<string name="open_247"><![CDATA[<b>Ouvert 24h/24 et 7j/7</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Ouvert</b> · Ferme à %s]]></string>
<string name="closed_unfmt">Fermé</string>
<string name="cost">Coût</string>
<string name="closed"><b>Fermé</b></string>
<string name="closed_opensat"><b>Fermé</b> · Ouvre à %s</string>
<string name="closed"><![CDATA[<b>Fermé</b>]]></string>
<string name="closed_opensat"><![CDATA[<b>Fermé</b> · Ouvre à %s]]></string>
<string name="holiday">Jour férié</string>
<string name="cost_detail"><b>Recharge :</b> %1$s · <b>Stationnement :</b> %2$s</string>
<string name="cost_detail"><![CDATA[<b>Recharge :</b> %1$s · <b>Stationnement :</b> %2$s]]></string>
<string name="realtime_data_unavailable">Statut en temps réel non disponible</string>
<string name="source">Source : %s</string>
<string name="menu_favs">Favoris</string>
@@ -235,17 +235,17 @@
<string name="crash_report_text">EVMap a planté. Veuillez envoyer un rapport de plantage au développeur.</string>
<string name="unknown_operator">Opérateur inconnu</string>
<string name="data_source_goingelectric_desc">Idéal dans les pays germanophones. Descriptions en allemand. Maintenu par la communauté.</string>
<string name="data_source_openchargemap_desc">Couverture mondiale avec une qualité variable. Descriptions en anglais ou dans la langue locale. Données ouvertes maintenues par la communauté et provenant de sources gouvernementales dans certains pays (par exemple, Amérique du Nord, Royaume-Uni, France, Norvège).</string>
<string name="data_source_openchargemap_desc"><![CDATA[Couverture mondiale avec une qualité variable. Descriptions en anglais ou dans la langue locale. Données ouvertes maintenues par la communauté et provenant de sources gouvernementales dans certains pays (par exemple, Amérique du Nord, Royaume-Uni, France, Norvège).]]></string>
<string name="faq_link">https://ev-map.app/faq/</string>
<string name="chargeprice_faq_link">https://ev-map.app/faq/#price-comparison-feature</string>
<string name="settings_data_sources">Sources de données</string>
<string name="data_sources_description">Veuillez choisir une source de données pour les stations de recharge. Vous pourrez la modifier ultérieurement dans les paramètres de l\'application.</string>
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses à récupérer. Veuillez envisager de faire un don via \"À propos\" → \"Faire un don\".</string>
<string name="pref_search_provider_info"><![CDATA[Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses à récupérer. Veuillez envisager de faire un don via "À propos" → "Faire un don".]]></string>
<string name="help">Aide</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge en courant alternatif monophasé de plus de 4,5 kW</string>
<string name="pref_map_rotate_gestures_on">Utilisez deux doigts pour faire pivoter la carte</string>
<string name="cost_detail_charging"><b>Recharge %s</b></string>
<string name="cost_detail_parking"><b>Stationnement %s</b></string>
<string name="cost_detail_charging"><![CDATA[<b>Recharge %s</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>Stationnement %s</b>]]></string>
<string name="navigate">Naviguer vers</string>
<string name="charge_price_format">%1$.2f %2$s</string>
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>

View File

@@ -380,4 +380,14 @@
<string name="data_source_openstreetmap">OpenStreetMap</string>
<string name="data_source_openstreetmap_desc">Supporto sperimentale in EVMap, non tutte le funzionalità sono disponibili.</string>
<string name="downloading_chargers_percent">Scaricamento… %.0f%%</string>
<string name="no_email_app_found">Prima installa una app per le email</string>
<string name="plug_type_2_tethered">Cavo fissato di tipo 2</string>
<string name="filter_accessibility">Accessibilità colonnina</string>
<string name="data_source_nobil">NOBIL</string>
<string name="data_source_nobil_desc"><![CDATA[Dati forniti liberamente dal governo e dalla comunità in Svezia e Norvegia.]]></string>
<string name="accessibility_public">Pubblico</string>
<string name="accessibility_visitors">Visitatori</string>
<string name="accessibility_employees">Impiegati</string>
<string name="accessibility_by_appointment">Per appuntamento</string>
<string name="accessibility_residents">Residenti</string>
</resources>

View File

@@ -2,8 +2,8 @@
<resources>
<string name="app_name">EVMap</string>
<string name="no_maps_app_found">Installer et navigeringsprogram først</string>
<string name="closed"><b>Stengt</b></string>
<string name="open_closesat"><b>Åpen</b> · Stenger %s</string>
<string name="closed"><![CDATA[<b>Stengt</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Åpen</b> · Stenger %s]]></string>
<string name="holiday">Ferie</string>
<string name="cost">Kostnad</string>
<string name="general_info">Generell info</string>
@@ -40,18 +40,18 @@
<string name="edit_filter_profile">Rediger «%s»</string>
<string name="help">Hjelp</string>
<string name="hours">Åpningstider</string>
<string name="open_247"><b>Døgnåpen</b></string>
<string name="open_247"><![CDATA[<b>Døgnåpen</b>]]></string>
<string name="settings_ui">Grensesnitt</string>
<string name="title_activity_maps">EVMap</string>
<string name="no_browser_app_found">Installer en nettleser først</string>
<string name="address">Adresse</string>
<string name="network">Nettverk</string>
<string name="closed_unfmt">Stengt</string>
<string name="cost_detail_charging"><b>%s-lading</b></string>
<string name="cost_detail_parking"><b>%s-parkering</b></string>
<string name="cost_detail_charging"><![CDATA[<b>%s-lading</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s-parkering</b>]]></string>
<string name="menu_map">Kart</string>
<string name="category_petrol_station">Bensinstasjon</string>
<string name="closed_opensat"><b>Stengt</b> · Åpner %s</string>
<string name="closed_opensat"><![CDATA[<b>Stengt</b> · Åpner %s]]></string>
<string name="retry">Prøv igjen</string>
<string name="source">Kilde: %s</string>
<string name="menu_favs">Favoritter</string>
@@ -88,7 +88,7 @@
<string name="realtime_data_source">Kilde for sanntidsstatus (beta): %s</string>
<string name="realtime_data_unavailable">Sanntidsstatus utilgjengelig</string>
<string name="other">Andre</string>
<string name="cost_detail"><b>Lading:</b> %1$s · <b>Parkering:</b> %2$s</string>
<string name="cost_detail"><![CDATA[<b>Lading:</b> %1$s · <b>Parkering:</b> %2$s]]></string>
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter ruteveiledning på Google Maps</string>
<string name="filter_free_parking">Kun ladere med gratis parkering</string>
<string name="filter_min_power">Min. effekt</string>
@@ -239,8 +239,8 @@
</plurals>
<string name="data_source_goingelectric_desc">Storartet i tyskspråklige land. Beskrivelser på tysk. Gemenskapsdrevet.</string>
<string name="powered_by_mapbox">tilbudt av Mapbox</string>
<string name="pref_search_provider_info">Data for søk er dyre å hente, spesielt fra Google Maps. Overvei å donere gjennom «Om» → «Doner».</string>
<string name="data_source_openchargemap_desc">Verdensomspennende, med varierende kvalitet. Beskrivelser på engelsk eller det lokale språket. Gemenskapsdrevet og åpen myndighetsdata i noen land (f.eks. Nord-Amerika, Storbritannia, Frankrike, og Norge.)</string>
<string name="pref_search_provider_info"><![CDATA[Data for søk er dyre å hente, spesielt fra Google Maps. Overvei å donere gjennom «Om» → «Doner».]]></string>
<string name="data_source_openchargemap_desc"><![CDATA[Verdensomspennende, med varierende kvalitet. Beskrivelser på engelsk eller det lokale språket. Gemenskapsdrevet og åpen myndighetsdata i noen land (f.eks. Nord-Amerika, Storbritannia, Frankrike, og Norge.)]]></string>
<string name="lets_go">Begynn</string>
<string name="crash_report_text">EVMap krasjet. Send en rapport til utvikleren.</string>
<string name="chargeprice_all_tariffs_selected">alle planer valgt</string>

View File

@@ -2,8 +2,8 @@
<resources>
<string name="data_source_goingelectric_desc">Ideaal in Duitstalige landen. Beschrijvingen in het Duits. Onderhouden door de gebruikersgemeenschap.</string>
<string name="crash_report_text">EVMap is afgebroken. Stuur een crash rapport naar de ontwikkelaar.</string>
<string name="pref_search_provider_info">Gegevens opzoeken is duur, vooral via Google Maps. Overweeg aub om een donatie te doen via “Over” → “Doneer”.</string>
<string name="data_source_openchargemap_desc">Werelddekkend, met variabele kwaliteit. Beschrijving in Engels of lokale taal. Onderhouden door de gebruikers. Ook open overheidswege eens in sommige landen (bv. Noord-Amerika, UK, Frankrijk, Noorwegen).</string>
<string name="pref_search_provider_info"><![CDATA[Gegevens opzoeken is duur, vooral via Google Maps. Overweeg aub om een donatie te doen via “Over” → “Doneer”.]]></string>
<string name="data_source_openchargemap_desc"><![CDATA[Werelddekkend, met variabele kwaliteit. Beschrijving in Engels of lokale taal. Onderhouden door de gebruikers. Ook open overheidswege eens in sommige landen (bv. Noord-Amerika, UK, Frankrijk, Noorwegen).]]></string>
<string name="pref_darkmode_always_off">altijd uit</string>
<string name="chargeprice_select_car_first">Kiest eerst je voertuig model in de instellingen</string>
<string name="chargeprice_no_compatible_connectors">Geen compatibele connectoren aan dit laadstation</string>
@@ -22,16 +22,16 @@
<string name="address">Adres</string>
<string name="operator">Operator</string>
<string name="network">Netwerk</string>
<string name="open_247"><b>24/7 open</b></string>
<string name="closed"><b>Gesloten</b></string>
<string name="open_closesat"><b>Open</b> · Sluit om %s</string>
<string name="closed_opensat"><b>Gesloten</b> · Opent om %s</string>
<string name="open_247"><![CDATA[<b>24/7 open</b>]]></string>
<string name="closed"><![CDATA[<b>Gesloten</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Open</b> · Sluit om %s]]></string>
<string name="closed_opensat"><![CDATA[<b>Gesloten</b> · Opent om %s]]></string>
<string name="closed_unfmt">Gesloten</string>
<string name="holiday">Feestdag</string>
<string name="cost">Kostprijs</string>
<string name="cost_detail"><b>Laden:</b> %1$s · <b>Parkeren:</b> %2$s</string>
<string name="cost_detail_charging"><b>%s laden</b></string>
<string name="cost_detail_parking"><b>%s parkeren</b></string>
<string name="cost_detail"><![CDATA[<b>Laden:</b> %1$s · <b>Parkeren:</b> %2$s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s laden</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parkeren</b>]]></string>
<string name="charging_free">Gratis</string>
<string name="parking_free">Gratis</string>
<string name="amenities">Voorzieningen</string>

View File

@@ -10,11 +10,11 @@
<string name="operator">Operatör</string>
<string name="network">Nätverk</string>
<string name="hours">Öppettider</string>
<string name="open_247"><![CDATA[<b>Öppen 24/7</b>]]></string>
<string name="closed"><![CDATA[<b>Stängd</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Öppen</b> · Stänger %s]]></string>
<string name="closed_opensat"><![CDATA[<b>Stängd</b> · Öppnar %s]]></string>
<string name="closed_unfmt">Stängd</string>
<string name="open_247"><![CDATA[<b>Öppet 24/7</b>]]></string>
<string name="closed"><![CDATA[<b>Stängt</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Öppet</b> · Stänger %s]]></string>
<string name="closed_opensat"><![CDATA[<b>Stängt</b> · Öppnar %s]]></string>
<string name="closed_unfmt">Stängt</string>
<string name="holiday">helgdag</string>
<string name="cost">Kostnad</string>
<string name="cost_detail"><![CDATA[<b>Laddning:</b> %1$s · <b>Parkering:</b> %2$s]]></string>
@@ -54,7 +54,7 @@
<string name="coordinates">Koordinater</string>
<string name="share">Dela</string>
<string name="filter_free">Endast gratis laddare</string>
<string name="filter_min_power">Lägst effekt</string>
<string name="filter_min_power">Lägsta effekt</string>
<string name="filter_free_parking">Endast laddare med gratis parkering</string>
<string name="filter_min_connectors">Lägst antal laddkontakter</string>
<string name="filter_connectors">Laddkontakter</string>
@@ -231,7 +231,7 @@
<string name="data_source_openstreetmap">OpenStreetMap</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="data_source_goingelectric_desc">Mycket bra i tysktalande länder. Beskrivningar på tyska. Underhålls av frivilliga.</string>
<string name="data_source_nobil_desc"><![CDATA[Öppen data från myndigheter och allmänheten för de nordiska länderna.]]></string>
<string name="data_source_nobil_desc"><![CDATA[Öppen data från myndigheter och allmänheten i Sverige och Norge.]]></string>
<string name="data_source_openchargemap_desc"><![CDATA[Världsomspännande med varierande kvalitet. Beskrivningar på engelska eller på det lokala språket. Underhålls av frivilliga och har öppen data från myndigheter i några länder (t.ex. Nordamerika, Storbritannien, Frankrike och Norge).]]></string>
<string name="data_source_openstreetmap_desc">Experimentellt stöd i EVMap, inte alla funktioner är tillgängliga.</string>
<string name="next">nästa</string>
@@ -385,4 +385,8 @@
<string name="accessibility_employees">Anställda</string>
<string name="accessibility_by_appointment">Efter överenskommelse</string>
<string name="accessibility_residents">Boende</string>
<string name="chargeprice_removal_2025_dialog_detail">Kostnaderna för Chargeprice-data har stigit kraftigt och täcks inte av längre av donationer. Därför kan inte EVMap längre visa denna data direkt i appen. Tillsvidare öppnar detta Chargeprices webbsida. En alternativ lösning är under utveckling, men kommer dröja något och kan introduceras med begränsad funktionalitet. Tack för ditt tålamod och stöd!</string>
<string name="chargeprice_removal_2025_dialog_title">Ursäkta!</string>
<string name="auto_use_new_map_screen">Ny kartvy (beta)</string>
<string name="welcome_2_title">Full koll på hastigheten</string>
</resources>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="shared_element_picture">picture</string>
<string name="shared_element_chargeprice">chargeprice</string>
<string name="github_link">https://github.com/ev-map/EVMap</string>
<string name="twitter_handle">\@ev_map</string>
<string name="twitter_url">https://twitter.com/ev_map</string>
@@ -10,7 +9,6 @@
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
<string name="tff_forum_url"><![CDATA[https://tff-forum.de/t/283834]]></string>
<string name="github_sponsors_link">https://github.com/sponsors/johan12345/</string>
<string name="chargeprice_api_url">https://api.chargeprice.app/v1/</string>
<string name="fronyx_url">https://fronyx.io/</string>
<string name="website_url">https://ev-map.app</string>
<string name="about_contributors_list">

View File

@@ -232,7 +232,7 @@
<string name="data_source_openstreetmap">OpenStreetMap</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="data_source_goingelectric_desc">Great in the German-speaking countries. Descriptions in German. Community-maintained.</string>
<string name="data_source_nobil_desc"><![CDATA[Open government and community provided data in the Nordic countries.]]></string>
<string name="data_source_nobil_desc"><![CDATA[Open government and community provided data in Sweden and Norway.]]></string>
<string name="data_source_openchargemap_desc"><![CDATA[Worldwide, with varying quality. Descriptions in English or the local language. Community-maintained and open government data in some countries (e.g. North America, UK, France, Norway).]]></string>
<string name="data_source_openstreetmap_desc">Experimental support in EVMap, not all features available.</string>
<string name="next">next</string>
@@ -386,4 +386,7 @@
<string name="accessibility_employees">Employees</string>
<string name="accessibility_by_appointment">By appointment</string>
<string name="accessibility_residents">Residents</string>
<string name="chargeprice_removal_2025_dialog_title">Sorry!</string>
<string name="chargeprice_removal_2025_dialog_detail">Costs for Chargeprice data access have risen sharply and cant be covered by donations, so EVMap can no longer show this data directly. For now, this opens the Chargeprice website. An alternative solution is being developed, but itll take time and may start with limited features. Thanks for your patience and support!</string>
<string name="auto_use_new_map_screen">New map screen (beta)</string>
</resources>

View File

@@ -9,14 +9,6 @@
android:fragment="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
android:title="@string/settings_data_sources"
android:icon="@drawable/ic_settings_data_source" />
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
android:title="@string/settings_chargeprice"
android:icon="@drawable/ic_chargeprice" />
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
android:title="@string/settings_android_auto"
android:icon="@drawable/ic_android_auto" />
<Preference
android:key="developer_options"
android:fragment="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<net.vonforst.evmap.ui.RangeSliderPreference
android:key="chargeprice_battery_range_android_auto"
android:title="@string/settings_android_auto_chargeprice_range"
android:valueFrom="0.0"
android:valueTo="100.0"
app:updatesContinuously="true"
android:defaultValue="20.0,80.0"
android:layout="@layout/preference_widget_rangeslider"
tools:summary="@string/chargeprice_battery_range" />
</PreferenceScreen>

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<CheckBoxPreference
android:key="chargeprice_native_integration"
android:title="@string/pref_chargeprice_native_integration"
android:summaryOn="@string/pref_chargeprice_native_integration_on"
android:summaryOff="@string/pref_chargeprice_native_integration_off"
app:defaultValue="true" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle"
app:showAllButton="false"
app:defaultToAll="false" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entryValues="@array/pref_chargeprice_currencies"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"
android:defaultValue="false"
app:singleLineTitle="false" />
<CheckBoxPreference
android:key="chargeprice_show_provider_customer_tariffs"
android:title="@string/pref_chargeprice_show_provider_customer_tariffs"
android:summary="@string/pref_chargeprice_show_provider_customer_tariffs_summary"
android:defaultValue="false"
app:singleLineTitle="false" />
<CheckBoxPreference
android:key="chargeprice_allow_unbalanced_load"
android:title="@string/pref_chargeprice_allow_unbalanced_load"
android:summary="@string/pref_chargeprice_allow_unbalanced_load_summary"
android:defaultValue="false"
app:singleLineTitle="false" />
</PreferenceScreen>

View File

@@ -1,79 +0,0 @@
package net.vonforst.evmap.api.chargeprice
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.okResponse
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.HttpURLConnection
class ChargepriceApiTest {
val ge: GoingElectricApi
val webServer = MockWebServer()
val chargeprice: ChargepriceApi
init {
webServer.start()
val apikey = ""
val baseurl = webServer.url("/ge/").toString()
ge = GoingElectricApi.create(apikey, baseurl)
chargeprice = ChargepriceApi.create(
apikey,
webServer.url("/cp/").toString()
)
webServer.dispatcher = object : Dispatcher() {
val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
override fun dispatch(request: RecordedRequest): MockResponse {
val segments = request.requestUrl!!.pathSegments
val urlHead = segments.subList(0, 2).joinToString("/")
return when (urlHead) {
"ge/chargepoints" -> {
val id = request.requestUrl!!.queryParameter("ge_id")
okResponse("/chargers/$id.json")
}
"cp/charge_prices" -> {
val body = request.body.readUtf8()
okResponse("/chargeprice/2105.json")
}
else -> notFoundResponse
}
}
}
}
private fun readResource(s: String) =
ChargepriceApiTest::class.java.getResource(s)?.readText()
@ExperimentalCoroutinesApi
@Test
fun apiTest() {
for (chargepoint in listOf(2105L, 18284L)) {
val charger = runBlocking { ge.getChargepointDetail(chargepoint).body()!! }
.chargelocations!![0].convert("", true) as ChargeLocation
println(charger)
runBlocking {
val result = chargeprice.getChargePrices(
ChargepriceRequest(
dataAdapter = "going_electric",
station =
ChargepriceStation.fromEvmap(charger, listOf("Typ2", "Schuko")),
options = ChargepriceOptions(energy = 22.0, duration = 60)
), "en"
)
assertEquals(25, result.data!!.size)
}
}
}
}

View File

@@ -114,5 +114,13 @@ class OpenStreetMapModelTest {
assertEquals(22.0, OSMChargingStation.parseOutputPower("22,0 kW"))
assertEquals(22.0, OSMChargingStation.parseOutputPower("22kW"))
assertEquals(22.0, OSMChargingStation.parseOutputPower("22 kW"))
// number without unit, assume kW or W depending on the number's magnitude
assertEquals(22.0, OSMChargingStation.parseOutputPower("22"))
assertEquals(22.0, OSMChargingStation.parseOutputPower("22.0"))
assertEquals(22.0, OSMChargingStation.parseOutputPower("22,0"))
assertEquals(22.0, OSMChargingStation.parseOutputPower("22000"))
assertEquals(22.0, OSMChargingStation.parseOutputPower("22000.0"))
assertEquals(22.0, OSMChargingStation.parseOutputPower("22000,0"))
}
}

View File

@@ -26,9 +26,6 @@ be put into the app in the form of a resource file called `apikeys.xml` under
<string name="goingelectric_key" translatable="false">
insert your GoingElectric key here
</string>
<string name="chargeprice_key" translatable="false">
insert your Chargeprice key here
</string>
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
@@ -41,6 +38,9 @@ be put into the app in the form of a resource file called `apikeys.xml` under
<string name="nobil_key" translatable="false">
insert your nobil key here
</string>
<string name="evmap_key" translatable="false">
insert your EVMap key here
</string>
</resources>
```
@@ -172,8 +172,8 @@ in German.
### **NOBIL**
NOBIL lists charging stations in the Nordic countries (Denmark, Finland, Iceland, Norway, Sweden)
and provides an open [API](https://info.nobil.no/api) to access the data.
NOBIL lists charging stations in Norway and Sweden and provides an open
[API](https://info.nobil.no/api) to access the data.
To get a NOBIL API key, fill in and submit the form on [this page](https://info.nobil.no/api).
Then, wait for an an e-mail with your API key.
@@ -239,6 +239,13 @@ key and documentation.
If you don't want to test this functionality, simply leave the API key blank.
</details>
### EVMap
EVMap provides APIs to fetch Nobil real-time data.
Contact [EVMap](mailto:evmap@vonforst.net) to get an API key.
Crash reporting
---------------

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Absturz behoben

View File

@@ -0,0 +1,8 @@
Native Chargeprice-Integration durch Webansicht ersetzt (Details siehe Meldung in der App)
Verbesserungen:
- Verbesserungen an den neuen Datenquellen Nobil und OpenStreetMap
- Neue Kartenansicht in Android Auto und AAOS (Beta): Kartenrotation nach Kompass
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed crash

View File

@@ -0,0 +1,8 @@
Replaced native Chargeprice integration with link to web view (see details in the app)
Improvements:
- Minor improvements to Nobil and OpenStreetMap data sources
- Added compass-based map rotation in new map view on Android Auto and AAOS
Bugfixes:
- Fixed crashes

View File

@@ -0,0 +1,18 @@
Med EVMap hittar du enkelt laddare för elfordon med hjälp av din Android-telefon. Du får tillgång till data från GoingElectric.de och Open Charge Map som innehåller information om laddstationer över hela världen. För många laddstationer i Europa finns realtidsstatus.
Funktioner:
- Material Design
- Visar laddstationer från databaserna GoingElectric.de och Open Charge Map
- Realtidsstatus om en laddstations tillgänglighet (bara i Europa)
- Integrerad prisjämförelse genom Chargeprice.app (bara i Europa)
- Kartdata från OpenStreetMap
- Sök efter platser
- Avancerad filterfunktion, med möjlighet att spara filterprofiler
- Favoritlista, med tillgänglighetsstatus
- Inga annonser, helt öppen källkod
EVMap är fri programvara och hittas på https://github.com/ev-map/EVMap.
Denna app är inte en officiell produkt från GoingElectric.de eller Open Charge Map. Appen använder endast deras öppna API:er.
En lista över nödvändiga behörigheter med förklaringar finns här: https://ev-map.app/faq/#permissions

View File

@@ -1 +1 @@
Hitta laddstationer för elbilar
Hitta laddstationer för elfordon

View File

@@ -1 +1 @@
EVMap - elbilsladdare
EVMap - EV-laddare