Compare commits

...

191 Commits
1.9.2 ... flows

Author SHA1 Message Date
Johan von Forstner
0685f14d06 WIP: refactor LiveData to Flows 2025-08-24 16:26:35 +02:00
johan12345
7e96c9e5a7 dependency upgrades & replacements 2025-08-23 21:48:02 +02:00
johan12345
44bd2c6159 upgrade MapLibre - 16 KB page size support
https://github.com/maplibre/maplibre-native/pull/3728
2025-08-19 20:38:34 +02:00
johan12345
7d2a19b0a3 upload AboutLibraries file for release builds 2025-08-18 18:02:08 +02:00
johan12345
3414a7581c remove FlipperDiagnosticActivity from manifest 2025-08-17 21:12:38 +02:00
johan12345
df47f7b4c1 upgrade dependencies 2025-08-17 20:31:03 +02:00
johan12345
a08e2ab7e9 upgrade to Java 21 2025-08-17 19:40:34 +02:00
johan12345
c1351ce935 update AGP 2025-08-17 19:36:34 +02:00
johan12345
b4a1a8b546 remove Flipper 2025-08-17 19:31:37 +02:00
johan12345
3865e6c33d update android-spatialite 2025-08-17 19:21:59 +02:00
johan12345
091b0f5ac3 further insets handling in MapFragment 2025-08-17 16:37:15 +02:00
johan12345
1148200f37 Upgrade Robolectric, re-enable CarAppTest 2025-08-16 15:23:07 +02:00
Johan von Forstner
1847e8b771 Rework MapFragment insets handling
fixes gallery height
2025-08-10 21:02:45 +02:00
Johan von Forstner
bbfe8e2bb2 fix detailAppBar popupTheme
commented out in 104913b3
2025-08-10 19:48:31 +02:00
Johan von Forstner
983d368a78 Tesla login fixes
refs 104913b3
2025-08-10 19:40:39 +02:00
johan12345
4a6a34db3a disable CarAppTest due to Robolectric incompatibility 2025-08-10 16:08:48 +02:00
Johan von Forstner
35ddece698 handle navigation bar insets for more fragments
fixes #382
2025-08-10 15:56:29 +02:00
Johan von Forstner
36c6a4053d fix location of ksp in build.gradle.kts 2025-08-10 15:08:08 +02:00
Johan von Forstner
104913b3c4 targetSdk 36, library upgrade, replace LocalBroadcastReceiver 2025-08-10 15:03:41 +02:00
Hosted Weblate
5cc510fe22 Translated using Weblate (Italian)
Currently translated at 100.0% (364 of 364 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-08-10 12:41:07 +02:00
Hosted Weblate
4250eb2ba8 Translated using Weblate (Portuguese)
Currently translated at 100.0% (364 of 364 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2025-08-10 12:41:07 +02:00
Johan von Forstner
1db82db066 fix location of CarInfo.kt 2025-08-10 12:39:02 +02:00
Johan von Forstner
d6a8fbee7d update Gradle & AGP 2025-07-27 20:41:58 +02:00
johan12345
23e2f0baad fix endless loading with filters that do not support local SQL queries 2025-07-27 17:43:09 +02:00
Johan von Forstner
ea4fb37f30 Merge pull request #381 from weblate/weblate-evmap-android
Translations update from Hosted Weblate
2025-07-17 21:29:36 +02:00
Hosted Weblate
094f38ac87 Translated using Weblate (Czech)
Currently translated at 100.0% (364 of 364 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-07-14 22:02:00 +00:00
Hosted Weblate
b84d13d42b Translated using Weblate (Estonian)
Currently translated at 100.0% (364 of 364 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-07-14 22:01:59 +00:00
johan12345
845bd2e5ca API 35 compat: handle bottom nav bar insets 2025-07-14 00:07:46 +02:00
Johan von Forstner
0b68ddb939 Merge pull request #290 from ev-map/openstreetmap
Implement OpenStreetMap data source
2025-07-13 23:23:45 +02:00
johan12345
93ff5592e6 Auto: Fallback to coordinates if address not available 2025-07-13 23:09:03 +02:00
johan12345
fbdda89219 automatically update OSM data 2025-07-13 22:56:14 +02:00
johan12345
f409ded73a fix fullDownload not being called 2025-07-13 22:30:41 +02:00
johan12345
d5a0e16675 run full download on IO thread 2025-07-13 18:56:40 +02:00
johan12345
99a93b202b OSM: fix min connectors SQL query 2025-07-13 18:48:47 +02:00
johan12345
32726993de Implement clustering DB queries with filters 2025-07-13 18:48:33 +02:00
Johan von Forstner
2218cc6b3a Spatialite clustering: calculate cluster center based on projected coordinates 2025-07-12 18:25:05 +02:00
Johan von Forstner
66564d81d7 add test to ensure consistent clustering in DB and memory 2025-07-12 18:25:05 +02:00
Johan von Forstner
eb819a793e fix package name for SavedRegionDaoTest 2025-07-12 18:25:05 +02:00
Johan von Forstner
deb101bcee make local (in-memory) clustering consistent with DB clustering 2025-07-12 18:25:05 +02:00
johan12345
176664d7ab clustering on projected coordinates 2025-07-12 18:25:05 +02:00
johan12345
7327943ec4 stabilize clusters 2025-07-12 18:25:05 +02:00
johan12345
61a7b358c0 continue implementation of DB-based clustering 2025-07-12 18:25:05 +02:00
johan12345
1845508512 add Room schema 23 2025-07-12 18:25:05 +02:00
johan12345
5e13134ee7 use spatial index 2025-07-12 18:25:05 +02:00
Johan von Forstner
b193e27022 fix imports 2025-07-12 18:25:05 +02:00
Altonss
ba601a8027 Increase margin to data source text (#361)
* Update fragment_onboarding_data_source.xml

Increase margin to data source text

* Update fragment_onboarding_data_source.xml

Increase margin to data source text

* use margin instead of padding

---------

Co-authored-by: johan12345 <johan.forstner@gmail.com>
2025-07-12 18:25:05 +02:00
johan12345
26de099baa WIP: clustering in DB 2025-07-12 18:25:05 +02:00
johan12345
4cc54bd376 OSM: try to speed up download process 2025-07-12 18:25:05 +02:00
johan12345
4c5cc187cd OSM: reduce flashing progress bar when moving map during download 2025-07-12 18:25:05 +02:00
Johan von Forstner
5a6c185a31 Consistency: add "." to strings
Co-authored-by: Danilo Bargen <mail@dbrgn.ch>
2025-07-12 18:25:05 +02:00
johan12345
4f02935b45 move to production webserver 2025-07-12 18:25:05 +02:00
johan12345
e7ecb37040 OSM: progress indicator improvements 2025-07-12 18:25:05 +02:00
johan12345
cc40b7e988 OSM: adjustments for AA/AAOS app 2025-07-12 18:25:05 +02:00
johan12345
8b02660c34 TeslaAvailabilityDetector: raise exception if chargepoints not known 2025-07-12 18:24:58 +02:00
johan12345
6e5f894c8a OSM: implement ReferenceData and network filter 2025-07-12 18:24:58 +02:00
johan12345
2d8327f6a6 fix OnboardingFragment for OSM 2025-07-12 18:24:58 +02:00
johan12345
964a65611b OSM: implement progress indicator for full download 2025-07-12 18:24:58 +02:00
johan12345
1f804efc04 add total count in OSMDocument
5d7b07b243
2025-07-12 18:24:58 +02:00
johan12345
44c7e1a705 detail_view layout fixes for missing values 2025-07-12 18:24:58 +02:00
johan12345
528454cd2c OSM: add tesla_supercharger_ccs 2025-07-12 18:24:49 +02:00
johan12345
868901d592 OSM: enable realtime data 2025-07-12 18:24:49 +02:00
johan12345
e934c2b7d9 lazy loading for fullDownload 2025-07-12 18:24:49 +02:00
johan12345
08ccd593e6 OSM: add charger website 2025-07-12 18:24:49 +02:00
johan12345
9b924af4ff OSM: implement getPhotos for images hosted at imgur 2025-07-12 18:24:49 +02:00
johan12345
3d16a90579 OSM: implement address 2025-07-12 18:24:49 +02:00
johan12345
36916fa8e3 OSM: implement cost description 2025-07-12 18:24:49 +02:00
johan12345
3ef69ebb02 DB: make getChargeLocationById nullable 2025-07-12 18:24:49 +02:00
johan12345
33fe7c0da3 improve formatting when address or connectors are missing 2025-07-12 18:24:49 +02:00
johan12345
1fee260d1c OpenStreetMap API: implement some first filters 2025-07-12 18:24:42 +02:00
johan12345
f1bb37ea68 fix getChargepointDetail for offline APIs 2025-07-12 18:24:42 +02:00
johan12345
0680bdab21 basic implementation of OpenStreetMapApi
#97
2025-07-12 18:24:42 +02:00
johan12345
d044de1231 Big toolchain update
- Gradle + AGP
- Java 17
- compile/targetSdk 35
- Room & Moshi use KSP, not KAPT
2025-07-12 18:14:11 +02:00
Weblate (bot)
667914f9d7 Translated using Weblate (Italian) (#380)
Currently translated at 100.0% (361 of 361 strings)



Translate-URL: https://hosted.weblate.org/projects/evmap/android/it/
Translation: EVMap/Android

Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
2025-07-10 19:17:34 +02:00
johan12345
8de16d1130 fix lint error 2025-06-14 17:49:25 +02:00
johan12345
68ca30ef4e fix lint error 2025-06-14 17:42:22 +02:00
johan12345
a0c4a8d4c1 TeslaAvailabilityDetector: Fix nullability bug 2025-06-14 17:40:26 +02:00
johan12345
18b7127034 always show current location on start, even if we were not following the location before 2025-06-14 17:32:36 +02:00
Weblate (bot)
5913d6a912 Translations update from Hosted Weblate (#227)
* Translated using Weblate (German)

Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (German)

Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (German)

Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (German)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (German)

Currently translated at 100.0% (361 of 361 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/de/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/de/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/de/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/de/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)
Translation: EVMap/App Store metadata

* Translated using Weblate (Estonian)

Currently translated at 100.0% (3 of 3 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/et/
Translation: EVMap/Android (strings specific to F-Droid variant)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Romanian)

Currently translated at 0.0% (0 of 2 strings)

Translated using Weblate (Romanian)

Currently translated at 0.0% (0 of 2 strings)

Translated using Weblate (Romanian)

Currently translated at 75.0% (271 of 361 strings)

Translated using Weblate (Romanian)

Currently translated at 75.0% (271 of 361 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/ro/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/ro/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/ro/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)

* Translated using Weblate (Czech)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (361 of 361 strings)

Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/cs/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/cs/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/cs/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (361 of 361 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/pt/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/pt/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/pt/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Dutch)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Dutch)

Currently translated at 81.9% (296 of 361 strings)

Translated using Weblate (Dutch)

Currently translated at 81.9% (296 of 361 strings)

Translated using Weblate (Dutch)

Currently translated at 81.9% (296 of 361 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Wim Lamotte <wim.lamotte@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/nl/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/nl/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/nl/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nl/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (French)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (French)

Currently translated at 92.5% (334 of 361 strings)

Translated using Weblate (French)

Currently translated at 92.5% (334 of 361 strings)

Co-authored-by: Altons <marsupilami450@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Norwegian Bokmål)

Currently translated at 74.5% (269 of 361 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 74.5% (269 of 361 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (3 of 3 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)

* Update translation files

Updated by "Squash Git commits" hook in Weblate.

Translation: EVMap/Android
Translate-URL: https://hosted.weblate.org/projects/evmap/android/

---------

Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Co-authored-by: Wim Lamotte <wim.lamotte@gmail.com>
Co-authored-by: Altons <marsupilami450@gmail.com>
2025-06-14 17:09:53 +02:00
johan12345
0615d06680 FilterScreen: remove unnecessary invalidate() calls
we are already observing filterProfiles
2025-06-14 16:47:09 +02:00
Licaon_Kter
463de7365d Remove some non-determinism 2025-05-30 12:02:08 +02:00
johan12345
aa00f8b7f0 update TeslaOwnerApi 2025-05-29 00:38:24 +02:00
johan12345
31754eca0c capture (but ignore) clicks on searchResultMarker 2025-05-17 19:37:50 +02:00
johan12345
ac8fc813f8 RTL and Arabic locale fixes 2025-05-17 19:27:52 +02:00
johan12345
4c202d7ff2 disable extendBounds if map is zoomed out far 2025-05-17 19:25:27 +02:00
johan12345
2e29e1f108 Rework showLocation function
avoid opening within EVMap itself
2025-05-15 23:16:10 +02:00
johan12345
363345f9c2 make Lint more happy 2025-05-07 23:45:28 +02:00
johan12345
2074c73bf7 make Lint happy 2025-05-07 23:37:26 +02:00
johan12345
fae373d125 Use CarAppService for startActivity instead of CarContext
fixes #375 for startActivity and openUrl

https://issuetracker.google.com/issues/372055514
Warning: You must update to androidx.car.app:1.7.0-alpha01 or later for the permissions dialog to show up on the phone screen when your app is used on a device running Android 14 or higher.
2025-05-07 23:06:20 +02:00
johan12345
ff388a0d9b fix missing escape in Italian translation 2025-04-27 12:43:14 +02:00
Hosted Weblate
002a6c4b3b Translated using Weblate (Norwegian Bokmål)
Currently translated at 74.9% (269 of 359 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2025-04-27 12:37:25 +02:00
Hosted Weblate
911358eca0 Translated using Weblate (French)
Currently translated at 93.0% (334 of 359 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2025-04-27 12:37:25 +02:00
Hosted Weblate
a43a8c8e60 Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (361 of 361 strings)

Added translation using Weblate (Italian)

Added translation using Weblate (Italian)

Added translation using Weblate (Italian)

Added translation using Weblate (Italian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/it/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/it/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/it/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/it/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/it/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)
Translation: EVMap/App Store metadata
2025-04-27 12:37:24 +02:00
Hosted Weblate
380150c851 Translated using Weblate (Portuguese)
Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (358 of 359 strings)

Translated using Weblate (Portuguese)

Currently translated at 98.6% (354 of 359 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2025-04-27 12:37:24 +02:00
Hosted Weblate
e6c3ebdcde Translated using Weblate (Czech)
Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (359 of 359 strings)

Translated using Weblate (Czech)

Currently translated at 98.6% (354 of 359 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2025-04-27 12:37:23 +02:00
Hosted Weblate
5fb58d25bc Translated using Weblate (Estonian)
Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (359 of 359 strings)

Translated using Weblate (Estonian)

Currently translated at 99.1% (356 of 359 strings)

Translated using Weblate (Estonian)

Currently translated at 50.0% (179 of 358 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Estonian)

Currently translated at 6.4% (23 of 358 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (2 of 2 strings)

Added translation using Weblate (Estonian)

Deleted translation using Weblate (Estonian)

Added translation using Weblate (Estonian)

Added translation using Weblate (Estonian)

Translated using Weblate (Estonian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Estonian)

Currently translated at 98.7% (81 of 82 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (3 of 3 strings)

Added translation using Weblate (Estonian)

Added translation using Weblate (Estonian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/et/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/et/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/et/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/et/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/et/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)
Translation: EVMap/App Store metadata
2025-04-27 12:37:23 +02:00
Hosted Weblate
948fb137c1 Translated using Weblate (German)
Currently translated at 100.0% (361 of 361 strings)

Translated using Weblate (German)

Currently translated at 100.0% (359 of 359 strings)

Translated using Weblate (German)

Currently translated at 99.7% (358 of 359 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Co-authored-by: Maschga <delphi@freenet.de>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2025-04-27 12:37:23 +02:00
johan12345
4578311d1f Fix touch targets for privacy policy link on API < 34
https://github.com/material-components/material-components-android/issues/2100#issuecomment-2234437889

fixes #374
2025-04-26 22:36:11 +02:00
Johan von Forstner
7a9cdcac21 try to better handle situations where map bounds cross the Antimeridian
ports 890af2ddef and e9b70a2f00 to 2.0.x
2025-04-21 10:43:47 +02:00
johan12345
9df61042af upgrade maplibre 2025-03-15 15:49:43 +01:00
johan12345
7ff21d9b5f Chargeprice: reset to default charging range when tapping title 2025-03-09 23:07:54 +01:00
johan12345
c6490471de CI: run checksec on release APKs 2025-03-04 22:23:20 +01:00
johan12345
75ba9d9978 CI: move apikeys-ci.xml to _ci folder 2025-03-04 22:23:20 +01:00
johan12345
1637d20806 upgrade android-spatialite 2025-03-04 22:23:20 +01:00
johan12345
5f60b73712 improve setLinkify BindingAdapter
fixes #371
2025-02-26 21:20:57 +01:00
johan12345
60871904be CarAppService: ignore if starting foreground service fails
this happens on AAOS API 34+ due to https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start. However, the app still works even without the foreground service.
2025-02-26 20:08:39 +01:00
johan12345
bb240afd21 FusionEngine: change log level 2025-02-26 20:08:38 +01:00
johan12345
8c6c27fd28 fix cherry-picked commit 03728f
fixes #369
2025-02-23 17:28:22 +01:00
johan12345
03728f0677 fix disappearing markers
fixes #368
2025-02-18 21:54:32 +01:00
johan12345
cfe40e1972 update social links 2025-02-07 22:18:19 +01:00
johan12345
a51052d9a3 increase heap space 2025-02-07 19:35:11 +01:00
johan12345
36af713252 embed referral links as webpage instead of native Android buttons 2025-02-07 19:13:15 +01:00
johan12345
77245c1e10 aboutLibraries: add allowed license "Unicode-3.0" 2025-01-29 23:15:53 +01:00
johan12345
bc90d23bd3 update car app library 2025-01-29 22:53:13 +01:00
johan12345
a6c482d5d3 remove setting for fronyx predictions in AAOS app
see also e7c9432191
2025-01-28 21:03:43 +01:00
johan12345
ca829be72b SearchSelectScreen: fix displaying selectAll buttons 2025-01-27 23:26:58 +01:00
johan12345
09980598f7 Room: export schema 2025-01-26 17:29:37 +01:00
johan12345
08f617d23b Onboarding Android Auto page: fix icon disappearing 2025-01-18 23:44:25 +01:00
johan12345
968bffd3aa fix more possible memory leak issues 2025-01-18 23:44:25 +01:00
johan12345
0ea0a2454e update car app library 2025-01-06 12:18:33 +01:00
Johan von Forstner
6fc7652079 README.md: Add JetBrains to sponsors 2024-12-03 20:53:34 +01:00
Robert Högberg
eacdb7294f Translated using Weblate (Swedish)
Currently translated at 98.7% (81 of 82 strings)

Translation: EVMap/App Store metadata
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/sv/
2024-11-24 18:39:57 +01:00
bittin1ddc447d824349b2
9e63c0dcf8 Translated using Weblate (Swedish)
Currently translated at 98.7% (81 of 82 strings)

Translation: EVMap/App Store metadata
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/sv/
2024-11-24 18:39:56 +01:00
johan12345
cf20dc4089 fix crash on first start 2024-11-24 18:25:11 +01:00
johan12345
bfe1667a69 add @nhoeher to contributors 2024-11-19 22:21:18 +01:00
johan12345
069b825ee7 monochrome app icon: new design
filled instead of outlined style
2024-11-19 22:20:02 +01:00
Niklas Höher
b058b3399c Add support for Android 13+ themed icons 2024-11-19 22:20:02 +01:00
johan12345
fcad13758a fix crash on first start 2024-11-19 21:51:36 +01:00
Robert Högberg
5f2bf26bc2 Fix current/voltage mixup for merged Chargepoints (#365) 2024-11-13 00:17:10 +01:00
johan12345
1e7bcd73a9 fix naming of OSM map provider in foss variant 2024-11-09 16:01:23 +01:00
johan12345
5ebd0a8abe fix possible memory leak issues 2024-11-08 20:52:53 +01:00
johan12345
a1fb480ff0 update MapLibre & AnyMaps 2024-10-26 22:36:36 +02:00
johan12345
cafc477964 Databinding: use viewLifecycleOwner instead of Fragment as lifecycle owner 2024-10-26 22:36:36 +02:00
Hosted Weblate
c90f32f1ff Translated using Weblate (Norwegian Bokmål)
Currently translated at 74.8% (268 of 358 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2024-10-25 22:03:36 +02:00
johan12345
0b266ee0aa translations: move nb-rNO to nb 2024-10-25 21:55:57 +02:00
johan12345
d0e8a24e5c fix unnecessary recreation of MapFragment 2024-10-23 22:58:10 +02:00
johan12345
8cda9ee469 upgrade AnyMaps 2024-10-23 22:58:08 +02:00
Hosted Weblate
ea3c552bca Translated using Weblate (Portuguese)
Currently translated at 100.0% (358 of 358 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2024-10-17 18:39:44 +02:00
Hosted Weblate
ffdcecd6ab Translated using Weblate (Czech)
Currently translated at 100.0% (358 of 358 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (356 of 356 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (355 of 355 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
2024-10-17 18:39:43 +02:00
Hosted Weblate
2650086594 Translated using Weblate (German)
Currently translated at 100.0% (358 of 358 strings)

Co-authored-by: mcliquid <info@mcliquid.de>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2024-10-17 18:39:43 +02:00
johan12345
aa07da1002 fix crash when trying to rename a filter profile to a name that already exists 2024-10-15 20:11:56 +02:00
Johan von Forstner
ce618f81e4 Fix comma at the end of address field
fixes #359
2024-09-14 12:56:27 +02:00
Johan von Forstner
9d196f645b OCM: fix blank cost field
fixes #358
2024-09-14 12:41:28 +02:00
Hosted Weblate
d28f14b932 Translated using Weblate (Portuguese)
Currently translated at 100.0% (357 of 357 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2024-09-05 19:01:21 +02:00
johan12345
35002aad86 Revert "hide Tesla referral link"
This reverts commit a55e4df62d.
2024-09-05 18:53:06 +02:00
johan12345
7601140bb8 fix another nullability issue in Tesla API 2024-08-23 21:07:21 +02:00
johan12345
dde3ff26ba translation fixes 2024-08-17 00:15:23 +02:00
Hosted Weblate
8cbf0a0589 Translated using Weblate (Czech)
Currently translated at 100.0% (356 of 356 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (355 of 355 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
2024-08-17 00:13:40 +02:00
Hosted Weblate
0382f63e72 Translated using Weblate (German)
Currently translated at 100.0% (355 of 355 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
2024-08-17 00:12:19 +02:00
johan12345
23d47142c0 Add fallback icon for unknown connectors
fixes #357
refs #6
2024-08-17 00:10:04 +02:00
johan12345
5130b93e99 UI fixes for chargers with unknown power
#357
2024-08-16 23:52:02 +02:00
johan12345
520d753b16 fix separate treatment of Type 3A and Type 3C connectors
fixes #356
2024-08-16 23:40:55 +02:00
johan12345
be4115af33 AAOS: more navigateToCharger fixes 2024-08-16 19:09:54 +02:00
johan12345
17efe716d5 Rework openUrl function
- use preferBrowser only when needed (when opening charger URLs, which might otherwise open in EVMap itself)
- make preferBrowser work even if the default browser does not support Custom Tabs

#313
2024-08-13 22:33:26 +02:00
johan12345
32c32ed934 AA/AAOS: disable unused map features
so they don't show up accidentally
2024-08-13 20:35:51 +02:00
johan12345
e911eb78c4 Google Maps: disable "map toolbar" 2024-08-13 20:34:29 +02:00
johan12345
efacd462db re-add PredictionRepository so that Tesla congestion data is displayed
ref 17a82591a5
2024-08-12 23:02:16 +02:00
johan12345
41be3e0180 update TeslaOwnerApi 2024-08-12 22:58:16 +02:00
johan12345
8a5ad2b1e8 make fossNormalDebug default build variant 2024-08-12 22:39:44 +02:00
johan12345
b723556dd9 Gradle upgrade 2024-08-12 22:33:39 +02:00
johan12345
17a82591a5 Disable fronyx predictions
API has been broken for >4 months now
2024-08-11 22:26:46 +02:00
johan12345
fa040928e8 Implement new MapScreen using MapWithContentTemplate 2024-08-11 22:10:38 +02:00
johan12345
c11178810e refactor map marker handling into MarkerManager class 2024-08-11 22:10:38 +02:00
johan12345
ef8bce5197 update Car App Library to 1.7.0-beta01 2024-08-11 22:10:38 +02:00
Johan von Forstner
398f159e27 Release 1.9.6 2024-07-31 16:37:23 +02:00
Johan von Forstner
6ab3ba2ed2 Mapbox Autocomplete: catch HttpExceptions 2024-07-31 16:10:36 +02:00
Johan von Forstner
f59fd9b3aa OpenChargeMap API: fix nullability 2024-07-31 16:04:14 +02:00
Johan von Forstner
9e18c62d9d remove unused method 2024-07-31 16:03:07 +02:00
Johan von Forstner
3626c9a72f update spatia-room 2024-07-31 15:56:48 +02:00
Johan von Forstner
36805d8224 AAOS: fallback to regular navigation intent if car app intent fails 2024-07-30 21:21:41 +02:00
johan12345
b1dee90068 docs improvements 2024-07-13 14:11:51 +02:00
johan12345
dfc7de75ad Release 1.9.5 2024-06-30 17:37:53 +02:00
johan12345
32c7774a3a update MapLibre
may help with #351
2024-06-30 16:34:03 +02:00
johan12345
02ef25b961 rework navigation handling to avoid changing start destination
fixes the following issue:
- start app for the first time, go through onboarding
- go to settings, change to dark mode
- try to go back to the map using the drawer
-> stuck, only back button helps
2024-06-30 16:22:35 +02:00
johan12345
e535e77b7a update build tools 2024-06-30 15:08:30 +02:00
johan12345
5b0b4e4337 remove "noinspection JCenterRepositoryObsolete" 2024-06-30 15:07:50 +02:00
johan12345
a6bbf635c5 Update AnyMaps & Google Maps
uses new Google Maps dark mode
2024-06-30 14:08:24 +02:00
johan12345
591f99dea4 update to released locale-config-x version 2024-06-22 11:53:52 +02:00
johan12345
0c5bd69205 Release 1.9.4 2024-06-21 00:25:13 +02:00
johan12345
72e98cf611 fix NoSuchElementExceptions in intent handling 2024-06-21 00:23:37 +02:00
johan12345
0fefffda2f update MapLibre
may help with #351
2024-06-21 00:19:38 +02:00
johan12345
49e555ef04 switch to fork of locale-config-x
fixes crash #352 until https://github.com/erfansn/locale-config-x/pull/2 is merged
2024-06-21 00:00:42 +02:00
johan12345
d6d1e915ee updated TeslaGuestApi 2024-06-19 00:19:36 +02:00
johan12345
546d7a11ce maybe fix rare NPE in GoingElectricAPI 2024-06-16 17:54:59 +02:00
johan12345
4849944c23 Release 1.9.3 2024-06-05 20:32:10 +02:00
johan12345
77b38661dd fix b9354e77: use correct exception type 2024-06-05 19:05:57 +02:00
johan12345
3723ee161b update AnyMaps
fixes issue where satellite map would not load correctly under some conditions
2024-06-04 21:41:26 +02:00
johan12345
1d3efe5295 update AnyMaps
fixes #346 through a5b09b5fda
2024-06-04 21:17:53 +02:00
johan12345
f011944135 fix lint error 2024-05-30 17:28:38 +02:00
johan12345
1d81bb5d37 Simplify currency handling
removes the need to translate all the currency names
2024-05-30 17:15:06 +02:00
johan12345
e8adb759a6 Simplify locale handling
using Locale Config X library
https://github.com/erfansn/locale-config-x
2024-05-30 16:52:02 +02:00
johan12345
f4384b4b60 update Android Gradle plugin 2024-05-30 13:10:20 +02:00
274 changed files with 8949 additions and 3252 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Java environment
uses: actions/setup-java@v4
with:
java-version: 17
java-version: 21
distribution: 'zulu'
cache: 'gradle'
- name: Decrypt keystore
@@ -24,7 +24,7 @@ jobs:
- name: Extract version code
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
- name: Build app release
- name: Build app release & export libraries
env:
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
@@ -38,7 +38,7 @@ jobs:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
run: ./gradlew assembleRelease --no-daemon
run: ./gradlew exportLibraryDefinitions assembleRelease --no-daemon
- name: release
uses: actions/create-release@v1
@@ -88,3 +88,12 @@ jobs:
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
asset_name: app-foss-automotive-release.apk
asset_content_type: application/vnd.android.package-archive
- name: upload Licenses
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/generated/aboutLibraries/aboutlibraries.json
asset_name: aboutlibraries.json
asset_content_type: application/json

View File

@@ -21,12 +21,12 @@ jobs:
- name: Set up Java environment
uses: actions/setup-java@v4
with:
java-version: 17
java-version: 21
distribution: 'zulu'
cache: 'gradle'
- name: Copy apikeys.xml
run: cp .github/workflows/apikeys-ci.xml app/src/main/res/values/apikeys.xml
run: cp _ci/apikeys-ci.xml app/src/main/res/values/apikeys.xml
- name: Build app
run: ./gradlew assemble${{ matrix.buildvariant }}Debug --no-daemon
@@ -36,3 +36,53 @@ jobs:
run: ./gradlew lint${{ matrix.buildvariant }}Debug --no-daemon
- name: Check licenses
run: ./gradlew exportLibraryDefinitions --no-daemon
apk_check:
name: Release APK checks (${{ matrix.buildvariant }})
runs-on: ubuntu-latest
strategy:
matrix:
buildvariant: [ FossNormal, FossAutomotive, GoogleNormal, GoogleAutomotive ]
steps:
- name: Install checksec
run: sudo apt install -y checksec
- name: Check out code
uses: actions/checkout@v4
- name: Set up Java environment
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Copy apikeys.xml
run: cp _ci/apikeys-ci.xml app/src/main/res/values/apikeys.xml
- name: Build app
run: ./gradlew assemble${{ matrix.buildvariant }}Release --no-daemon
- name: Unpack native libraries from APK
run: |
VARIANT_FILENAME=$(echo ${{ matrix.buildvariant }} | sed -E 's/([a-z])([A-Z])/\1-\2/g' | tr 'A-Z' 'a-z')
VARIANT_FOLDER=$(echo ${{ matrix.buildvariant }} | sed -E 's/^([A-Z])/\L\1/')
APK_FILE="app/build/outputs/apk/$VARIANT_FOLDER/release/app-$VARIANT_FILENAME-release-unsigned.apk"
unzip $APK_FILE "lib/*"
- name: Run checksec on native libraries
run: |
checksec --output=json --dir=lib > checksec_output.json
jq --argjson exceptions '[
"lib/armeabi-v7a/libc++_shared.so",
"lib/x86/libc++_shared.so"
]' '
to_entries
| map(select(.value.fortify_source == "no" and (.key as $lib | $exceptions | index($lib) | not)))
| if length > 0 then
error("The following libraries do not have fortify enabled (and are not in the exception list): " + (map(.key) | join(", ")))
else
"All libraries have fortify enabled or are in the exception list."
end
' checksec_output.json

View File

@@ -46,8 +46,7 @@ features and how they can be obtained in our [documentation page](doc/api_keys.m
There are four different build flavors, `googleNormal`, `fossNormal`, `googleAutomotive`, and
`fossAutomotive`.
- The `foss` variants only use OSM data and should run on most Android devices, even without
Google Play Services.
- The `foss` variants only use OSM data for the base map and place search. They should run on most Android devices, even those without Google Play Services.
- `fossNormal` is intended to run on smartphones and tablets, and also includes the Android
Auto app for use on the car display (however Android Auto may not work if the app is not
installed from Google Play, see https://github.com/ev-map/EVMap/issues/319).
@@ -87,14 +86,13 @@ Sponsors
Many users currently support the development EVMap with their donations. You can find more
information on the [Donate page](https://ev-map.app/donate/) on the EVMap website.
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="58"/></a><br>
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="38"/></a><br>
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="58"/></a><br>
<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://fronyx.io/"><img src="https://github.com/ev-map/EVMap/blob/master/_img/powered_by_fronyx.svg" alt="Powered by Fronyx" height="68"/></a><br>
Since September 2022, for certain charging stations, **Fronyx** provide us free access to their API
for availability predictions.
<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

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" version="1.1"
viewBox="0 0 108 108">
<defs>
<style>
.cls-1 {
fill: #000;
stroke-width: 0px;
}
</style>
</defs>
<path class="cls-1"
d="M53.9,28c-8.8,0-15.9,7.1-15.9,15.9s13.4,18.2,15,35.3c0,.5.5.9,1,.9s.9-.4,1-.9c1.6-17.1,15-23.3,15-35.3-.1-8.8-7.2-15.9-16-15.9ZM59,43.1l-6.1,10.5v-7.9h-2.6v-9.6s8.8,0,8.7,0l-3.5,7h3.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z" />
</svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@@ -1,11 +1,12 @@
import java.util.Base64
plugins {
id("com.adarshr.test-logger") version "3.1.0"
id("com.adarshr.test-logger") version "4.0.0"
id("com.android.application")
id("kotlin-android")
id("kotlin-parcelize")
id("kotlin-kapt")
id("com.google.devtools.ksp").version("2.0.21-1.0.28")
id("androidx.navigation.safeargs.kotlin")
id("com.mikepenz.aboutlibraries.plugin")
}
@@ -16,20 +17,26 @@ android {
defaultConfig {
applicationId = "net.vonforst.evmap"
compileSdk = 34
compileSdk = 36
minSdk = 21
targetSdk = 34
targetSdk = 36
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 222
versionName = "1.9.2"
versionCode = 230
versionName = "1.9.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
val isRunningOnCI = System.getenv("CI") == "true"
val isCIKeystoreAvailable = System.getenv("KEYSTORE_PASSWORD") != null
signingConfigs {
create("release") {
val isRunningOnCI = System.getenv("CI") == "true"
if (isRunningOnCI) {
if (isRunningOnCI && isCIKeystoreAvailable) {
// configure keystore
storeFile = file("../_ci/keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
@@ -46,7 +53,11 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
signingConfig = if (isRunningOnCI && !isCIKeystoreAvailable) {
null
} else {
signingConfigs.getByName("release")
}
}
create("releaseAutomotivePackageName") {
// Faurecia Aptoide requires the automotive variant to use a separate package name
@@ -67,6 +78,7 @@ android {
productFlavors {
create("foss") {
dimension = "dependencies"
isDefault = true
}
create("google") {
dimension = "dependencies"
@@ -74,6 +86,7 @@ android {
}
create("normal") {
dimension = "automotive"
isDefault = true
}
create("automotive") {
dimension = "automotive"
@@ -85,18 +98,12 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures {
@@ -251,17 +258,21 @@ configurations {
}
aboutLibraries {
allowedLicenses = arrayOf(
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
"asdkl", // Android SDK
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
"Google Maps Platform Terms of Service", // Google Maps SDK
"provided without support or warranty", // org.json
"Unicode/ICU License", // icu4j
"Bouncy Castle Licence", // bcprov
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
)
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
license {
allowedLicenses = setOf(
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
"asdkl", // Android SDK
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
"Google Maps Platform Terms of Service", // Google Maps SDK
"Unicode/ICU License", "Unicode-3.0", // icu4j
"Bouncy Castle Licence", // bcprov
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
)
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
}
export {
excludeFields = setOf("generated")
}
}
dependencies {
@@ -275,114 +286,118 @@ dependencies {
val testGoogleImplementation by configurations
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.core:core-ktx:1.13.1")
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.9.0")
implementation("androidx.fragment:fragment-ktx:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
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.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.browser:browser:1.8.0")
implementation("com.google.android.material:material:1.13.0-rc01")
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.security:security-crypto:1.1.0-alpha06")
implementation("androidx.work:work-runtime-ktx:2.9.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("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
implementation("com.squareup.moshi:moshi-adapters:1.15.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
implementation("io.coil-kt:coil:2.6.0")
implementation("io.coil-kt:coil:2.7.0")
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
implementation("com.airbnb.android:lottie:4.1.0")
implementation("com.airbnb.android:lottie:6.6.7")
implementation("io.michaelrocks.bimap:bimap:1.1.0")
implementation("com.google.guava:guava:29.0-android")
implementation("com.github.pengrad:mapscaleview:1.6.0")
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
implementation("com.github.erfansn:locale-config-x:1.0.1")
// Android Auto
val carAppVersion = "1.4.0"
val carAppVersion = "1.7.0"
implementation("androidx.car.app:app:$carAppVersion")
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
// AnyMaps
val anyMapsVersion = "a5b9abca40"
val anyMapsVersion = "1174ef9375"
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:18.2.0")
googleImplementation("com.google.android.gms:play-services-maps:19.2.0")
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
// duplicates classes from mapbox-sdk-services
exclude("org.maplibre.gl", "android-sdk-geojson")
}
implementation("org.maplibre.gl:android-sdk:10.3.5") {
exclude("org.maplibre.gl", "android-sdk-geojson")
}
// Google Places
googleImplementation("com.google.android.libraries.places:places:3.5.0")
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
// Mapbox Geocoding
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0")
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.8.0")
// navigation library
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
// viewmodel library
val lifecycle_version = "2.8.1"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
val lifecycleVersion = "2.9.2"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
// room library
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("com.github.anboralabs:spatia-room:0.2.9") {
exclude(group = "com.github.dalgarins", module = "android-spatialite")
val roomVersion = "2.7.2"
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")
}
implementation("com.github.EV-map:android-spatialite:e5495c83ad") // version with minSdk increased to 21 & FORTIFY_SOURCE enabled
// 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")
// billing library
val billing_version = "7.0.0"
googleImplementation("com.android.billingclient:billing:$billing_version")
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
val billingVersion = "7.0.0"
googleImplementation("com.android.billingclient:billing:$billingVersion")
googleImplementation("com.android.billingclient:billing-ktx:$billingVersion")
// ACRA (crash reporting)
val acraVersion = "5.11.1"
val acraVersion = "5.12.0"
implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-dialog:$acraVersion")
implementation("ch.acra:acra-limiter:$acraVersion")
// debug tools
debugImplementation("com.facebook.flipper:flipper:0.238.0")
debugImplementation("com.facebook.soloader:soloader:0.10.5")
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0")
debugImplementation("com.jakewharton.timber:timber:5.0.1")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
// testing
testImplementation("junit:junit:4.13.2")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
//noinspection GradleDependency
testImplementation("org.json:json:20080701")
testImplementation("org.robolectric:robolectric:4.11.1")
testImplementation("androidx.test:core:1.5.0")
testImplementation("org.robolectric:robolectric:4.16-beta-1")
testImplementation("androidx.test:core:1.7.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("androidx.car.app:app-testing:$carAppVersion")
testImplementation("androidx.test:core:1.5.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}
fun decode(s: String, key: String): String {

View File

@@ -0,0 +1,904 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "5dbaaa5adf8cb9b6e8a8314bb7766447",
"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, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER 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": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"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": "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": "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": "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, '5dbaaa5adf8cb9b6e8a8314bb7766447')"
]
}
}

View File

@@ -0,0 +1,997 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "e9e169ba4257824c82e4acb030730e97",
"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, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER 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",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "editUrl",
"columnName": "editUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "verified",
"columnName": "verified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "barrierFree",
"columnName": "barrierFree",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "operator",
"columnName": "operator",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "generalInformation",
"columnName": "generalInformation",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "amenities",
"columnName": "amenities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "locationDescription",
"columnName": "locationDescription",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photos",
"columnName": "photos",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chargecards",
"columnName": "chargecards",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "license",
"columnName": "license",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "networkUrl",
"columnName": "networkUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chargerUrl",
"columnName": "chargerUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timeRetrieved",
"columnName": "timeRetrieved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "address.city",
"columnName": "city",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "address.country",
"columnName": "country",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "address.postcode",
"columnName": "postcode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "address.street",
"columnName": "street",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "faultReport.created",
"columnName": "fault_report_created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "faultReport.description",
"columnName": "fault_report_description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.twentyfourSeven",
"columnName": "twentyfourSeven",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "openinghours.description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.monday.start",
"columnName": "mostart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.monday.end",
"columnName": "moend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.tuesday.start",
"columnName": "tustart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.tuesday.end",
"columnName": "tuend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.wednesday.start",
"columnName": "westart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.wednesday.end",
"columnName": "weend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.thursday.start",
"columnName": "thstart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.thursday.end",
"columnName": "thend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.friday.start",
"columnName": "frstart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.friday.end",
"columnName": "frend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.saturday.start",
"columnName": "sastart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.saturday.end",
"columnName": "saend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.sunday.start",
"columnName": "sustart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.sunday.end",
"columnName": "suend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.holiday.start",
"columnName": "hostart",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "openinghours.days.holiday.end",
"columnName": "hoend",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cost.freecharging",
"columnName": "freecharging",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "cost.freeparking",
"columnName": "freeparking",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "cost.descriptionShort",
"columnName": "descriptionShort",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cost.descriptionLong",
"columnName": "descriptionLong",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chargepriceData.country",
"columnName": "chargepricecountry",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chargepriceData.network",
"columnName": "chargepricenetwork",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chargepriceData.plugTypes",
"columnName": "chargepriceplugTypes",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
},
"indices": [],
"foreignKeys": []
},
{
"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`)"
}
],
"foreignKeys": []
},
{
"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",
"notNull": false
},
{
"fieldPath": "types",
"columnName": "types",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
},
{
"fieldPath": "discontinued",
"columnName": "discontinued",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "obsolete",
"columnName": "obsolete",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contactEmail",
"columnName": "contactEmail",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contactTelephone1",
"columnName": "contactTelephone1",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contactTelephone2",
"columnName": "contactTelephone2",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
}
],
"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`)"
}
],
"foreignKeys": []
}
],
"views": [],
"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, 'e9e169ba4257824c82e4acb030730e97')"
]
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
package net.vonforst.evmap.storage
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.ChargeLocationCluster
import net.vonforst.evmap.model.ChargepointListItem
import net.vonforst.evmap.model.Coordinate
import net.vonforst.evmap.ui.cluster
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.time.Instant
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
class ChargeLocationsDaoTest {
private lateinit var database: AppDatabase
private lateinit var dao: ChargeLocationsDao
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = AppDatabase.createInMemory(context)
dao = database.chargeLocationsDao()
}
@After
fun tearDown() {
database.close()
}
@Test
fun testClustering() {
val lat1 = 53.0
val lng1 = 9.0
val lat2 = 54.0
val lng2 = 10.0
val chargeLocations = (0..100).map { i ->
val lat = Random.nextDouble(lat1, lat2)
val lng = Random.nextDouble(lng1, lng2)
ChargeLocation(
i.toLong(),
"test",
"test",
Coordinate(lat, lng),
null,
emptyList(),
null,
"https://google.com",
null,
null,
false,
null,
null,
null,
null,
null,
null,
null, null, null, null, null, null, null, Instant.now(), true
)
}
runBlocking {
dao.insert(*chargeLocations.toTypedArray())
}
val zoom = 10f
val clusteredInMemory = cluster(chargeLocations, zoom).sorted()
val clusteredInDB = runBlocking {
dao.getChargeLocationsClustered(lat1, lat2, lng1, lng2, "test", 0L, zoom)
}.sorted()
assertEquals(clusteredInMemory.size, clusteredInDB.size)
clusteredInDB.zip(clusteredInMemory).forEach { (a, b) ->
when (a) {
is ChargeLocation -> {
assertTrue(b is ChargeLocation)
assertEquals(a, b)
}
is ChargeLocationCluster -> {
assertTrue(b is ChargeLocationCluster)
assertEquals(a.clusterCount, (b as ChargeLocationCluster).clusterCount)
assertEquals(a.coordinates.lat, b.coordinates.lat, 1e-5)
assertEquals(a.coordinates.lng, b.coordinates.lng, 1e-5)
}
}
}
}
private fun List<ChargepointListItem>.sorted() = sortedBy {
when (it) {
is ChargeLocationCluster -> it.coordinates.lat
is ChargeLocation -> it.coordinates.lat
else -> 0.0
}
}.sortedBy {
when (it) {
is ChargeLocationCluster -> it.coordinates.lng
is ChargeLocation -> it.coordinates.lng
else -> 0.0
}
}
}

View File

@@ -1,4 +1,4 @@
package com.johan.evmap.storage
package net.vonforst.evmap.storage
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule

View File

@@ -2,4 +2,4 @@
<resources>
<string name="grant_on_phone">Povolit</string>
<string name="auto_location_permission_needed">Pro spuštění aplikace EVMap ve vašem autě musíte povolit přístup k vaší poloze.</string>
</resources>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<string name="grant_on_phone">Zulassen</string>
<string name="auto_location_permission_needed">Um EVMap auf deinem Fahrzeug zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
</resources>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="grant_on_phone">Luba</string>
<string name="auto_location_permission_needed">Et EVMap toimiks sinu autos, palun luba tal asukohta tuvastada.</string>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<string name="grant_on_phone">Autoriser</string>
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
</resources>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="grant_on_phone">Consenti</string>
<string name="auto_location_permission_needed">Per eseguire EVMap sulla propria auto, è necessario concedere l\'accesso alla propria posizione.</string>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<string name="auto_location_permission_needed">Du må du innvilge posisjonstilgang for å kjøre EVMap i bilen din.</string>
<string name="grant_on_phone">Tillat</string>
</resources>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<string name="grant_on_phone">Toestaan</string>
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
</resources>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<string name="grant_on_phone">Permitir</string>
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
</resources>
</resources>

View File

@@ -1,2 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
</resources>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
</application>
</manifest>

View File

@@ -2,41 +2,15 @@ package net.vonforst.evmap
import android.content.Context
import android.os.Build
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader
import okhttp3.OkHttpClient
private val networkFlipperPlugin = NetworkFlipperPlugin()
import timber.log.Timber
fun addDebugInterceptors(context: Context) {
if (Build.FINGERPRINT == "robolectric") return
SoLoader.init(context, false)
val client = AndroidFlipperClient.getInstance(context)
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
client.addPlugin(networkFlipperPlugin)
client.addPlugin(DatabasesFlipperPlugin(context))
client.addPlugin(SharedPreferencesFlipperPlugin(context))
client.start()
Timber.plant(Timber.DebugTree())
}
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
// Flipper does not work during unit tests - so check whether we are running tests first
var isRunningTest = true
try {
Class.forName("org.junit.Test")
} catch (e: ClassNotFoundException) {
isRunningTest = false
}
if (!isRunningTest) {
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
}
return this
}

View File

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

View File

@@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.transition.MaterialSharedAxis
@@ -43,7 +42,7 @@ class DonateFragment : DonateFragmentBase() {
)
binding.btnDonate.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link), binding.root)
}
setupReferrals(referrals)

View File

@@ -3,4 +3,4 @@
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj zasláním finančního daru vývojáři.</string>
<string name="donate_paypal">Přispět pomocí PayPalu</string>
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
</resources>
</resources>

View File

@@ -3,4 +3,4 @@
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
<string name="donate_paypal">Mit PayPal spenden</string>
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
</resources>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Kas EVMap on sulle kasulik? Oma arendajale saadetava rahalise toetusega edendad ka arendustegevust.</string>
<string name="donate_paypal">Toeta PayPali abil</string>
<string name="data_sources_hint">Selles rakenduses näidatavad kaardiandmed on pärit OpenStreetMapist.</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.</string>
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile ? Soutenez son développement en envoyant un don au développeur.</string>
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</string>
<string name="donate_paypal">Faire un don avec PayPal</string>
</resources>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Trovi utile EVMap? Sostieni il suo sviluppo inviando una donazione allo sviluppatore.</string>
<string name="donate_paypal">Dona attraverso PayPal</string>
<string name="data_sources_hint">I dati cartografici dell\'applicazione sono forniti da OpenStreetMap.</string>
</resources>

View File

@@ -2,5 +2,5 @@
<resources>
<string name="donate_paypal">Doner med PayPal</string>
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap.</string>
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
</resources>
<string name="donations_info" formatted="false">Synes du EVMap er nyttig? Støtt utviklingen ved å sende en slant til utvikleren.</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
<string name="donations_info" formatted="false">Vond je EVMap nuttig? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
<string name="donate_paypal">Doneer via PayPal</string>
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
</resources>
</resources>

View File

@@ -2,5 +2,5 @@
<resources>
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap.</string>
<string name="donate_paypal">Doar com o PayPal</string>
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
</resources>
<string name="donations_info" formatted="false">Acha que o EVMap é útil? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>@string/pref_provider_osm_mapbox</item>
<item>@string/pref_provider_osm</item>
</string-array>
<string-array name="pref_map_provider_values" translatable="false">
<item>mapbox</item>

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři.
\n
\nGoogle si z každého daru strhne 15 %.</string>
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři. \n \nGoogle si z každého daru strhne 15 %.</string>
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap.</string>
</resources>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
</resources>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="data_sources_hint">Seadistustes saad valida kahe kaardiandmete allika vahel: Google Maps ja OpenStreetMap.</string>
<string name="donations_info" formatted="false">EVMap on sinu jaoks kasulik? Toeta edasist arendust oma rahalise panusega.\n\nGoogle võtab igast toestussummast teenustasuna 15%.</string>
</resources>

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.
\n
\nGoogle prend 15% sur chaque don.</string>
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile ? Soutenez son développement en envoyant un don au développeur. \n \nGoogle prend 15% sur chaque don.</string>
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap pour les données cartographiques.</string>
</resources>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Trovi utile EVMap? Sostieni il suo sviluppo inviando una donazione allo sviluppatore.\n\nGoogle si prende il 15% su ogni donazione.</string>
<string name="data_sources_hint">Nelle impostazioni si può anche scegliere tra Google Maps e OpenStreetMap per i dati cartografici.</string>
</resources>

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende penger til utvikleren.
\n
\nGoogle tar 15% av alle donasjoner.</string>
<string name="donations_info" formatted="false">Synes du EVMap er nyttig? Støtt utviklingen ved å sende penger til utvikleren. \n \nGoogle tar 15% av alle donasjoner.</string>
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap for kartdata.</string>
</resources>
</resources>

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
\n
\nGoogle houdt 15% in van elke donatie.</string>
<string name="donations_info" formatted="false">Vind je EVMap nuttig? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar. \n \nGoogle houdt 15% in van elke donatie.</string>
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap voor de kaartgegevens.</string>
</resources>
</resources>

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.
\n
\nA Google cobra 15% de cada doação.</string>
<string name="donations_info" formatted="false">Acha que o EVMap é útil? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app. \n \nA Google cobra 15% de cada doação.</string>
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap nas definições da app.</string>
</resources>
</resources>

View File

@@ -1,2 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
</resources>

View File

@@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
@@ -24,6 +25,10 @@
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<package android:name="com.google.android.projection.gearhead" />
<package android:name="com.google.android.apps.automotive.templates.host" />

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering;
import com.car2go.maps.model.LatLng;
import java.util.Collection;
/**
* A collection of ClusterItems that are nearby each other.
*/
public interface Cluster<T extends ClusterItem> {
LatLng getPosition();
Collection<T> getItems();
int getSize();
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.car2go.maps.model.LatLng;
/**
* ClusterItem represents a marker on the map.
*/
public interface ClusterItem {
/**
* The position of this marker. This must always return the same value.
*/
@NonNull
LatLng getPosition();
/**
* The title of this marker.
*/
@Nullable
String getTitle();
/**
* The description of this marker.
*/
@Nullable
String getSnippet();
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.google.maps.android.clustering.ClusterItem;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Base Algorithm class that implements lock/unlock functionality.
*/
public abstract class AbstractAlgorithm<T extends ClusterItem> implements Algorithm<T> {
private final ReadWriteLock mLock = new ReentrantReadWriteLock();
@Override
public void lock() {
mLock.writeLock().lock();
}
@Override
public void unlock() {
mLock.writeLock().unlock();
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import java.util.Collection;
import java.util.Set;
/**
* Logic for computing clusters
*/
public interface Algorithm<T extends ClusterItem> {
/**
* Adds an item to the algorithm
*
* @param item the item to be added
* @return true if the algorithm contents changed as a result of the call
*/
boolean addItem(T item);
/**
* Adds a collection of items to the algorithm
*
* @param items the items to be added
* @return true if the algorithm contents changed as a result of the call
*/
boolean addItems(Collection<T> items);
void clearItems();
/**
* Removes an item from the algorithm
*
* @param item the item to be removed
* @return true if this algorithm contained the specified element (or equivalently, if this
* algorithm changed as a result of the call).
*/
boolean removeItem(T item);
/**
* Updates the provided item in the algorithm
*
* @param item the item to be updated
* @return true if the item existed in the algorithm and was updated, or false if the item did
* not exist in the algorithm and the algorithm contents remain unchanged.
*/
boolean updateItem(T item);
/**
* Removes a collection of items from the algorithm
*
* @param items the items to be removed
* @return true if this algorithm contents changed as a result of the call
*/
boolean removeItems(Collection<T> items);
Set<? extends Cluster<T>> getClusters(float zoom);
Collection<T> getItems();
void setMaxDistanceBetweenClusteredItems(int maxDistance);
int getMaxDistanceBetweenClusteredItems();
void lock();
void unlock();
}

View File

@@ -1,314 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.car2go.maps.model.LatLng;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import com.google.maps.android.geometry.Bounds;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import com.google.maps.android.quadtree.PointQuadTree;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not
* hierarchical.
* <p/>
* High level algorithm:<br>
* 1. Iterate over items in the order they were added (candidate clusters).<br>
* 2. Create a cluster with the center of the item. <br>
* 3. Add all items that are within a certain distance to the cluster. <br>
* 4. Move any items out of an existing cluster if they are closer to another cluster. <br>
* 5. Remove those items from the list of candidate clusters.
* <p/>
* Clusters have the center of the first element (not the centroid of the items within it).
*/
public class NonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> extends AbstractAlgorithm<T> {
private static final int DEFAULT_MAX_DISTANCE_AT_ZOOM = 100; // essentially 100 dp.
private int mMaxDistance = DEFAULT_MAX_DISTANCE_AT_ZOOM;
/**
* Any modifications should be synchronized on mQuadTree.
*/
private final Collection<QuadItem<T>> mItems = new LinkedHashSet<>();
/**
* Any modifications should be synchronized on mQuadTree.
*/
private final PointQuadTree<QuadItem<T>> mQuadTree = new PointQuadTree<>(0, 1, 0, 1);
private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1);
/**
* Adds an item to the algorithm
*
* @param item the item to be added
* @return true if the algorithm contents changed as a result of the call
*/
@Override
public boolean addItem(T item) {
boolean result;
final QuadItem<T> quadItem = new QuadItem<>(item);
synchronized (mQuadTree) {
result = mItems.add(quadItem);
if (result) {
mQuadTree.add(quadItem);
}
}
return result;
}
/**
* Adds a collection of items to the algorithm
*
* @param items the items to be added
* @return true if the algorithm contents changed as a result of the call
*/
@Override
public boolean addItems(Collection<T> items) {
boolean result = false;
for (T item : items) {
boolean individualResult = addItem(item);
if (individualResult) {
result = true;
}
}
return result;
}
@Override
public void clearItems() {
synchronized (mQuadTree) {
mItems.clear();
mQuadTree.clear();
}
}
/**
* Removes an item from the algorithm
*
* @param item the item to be removed
* @return true if this algorithm contained the specified element (or equivalently, if this
* algorithm changed as a result of the call).
*/
@Override
public boolean removeItem(T item) {
boolean result;
// QuadItem delegates hashcode() and equals() to its item so,
// removing any QuadItem to that item will remove the item
final QuadItem<T> quadItem = new QuadItem<>(item);
synchronized (mQuadTree) {
result = mItems.remove(quadItem);
if (result) {
mQuadTree.remove(quadItem);
}
}
return result;
}
/**
* Removes a collection of items from the algorithm
*
* @param items the items to be removed
* @return true if this algorithm contents changed as a result of the call
*/
@Override
public boolean removeItems(Collection<T> items) {
boolean result = false;
synchronized (mQuadTree) {
for (T item : items) {
// QuadItem delegates hashcode() and equals() to its item so,
// removing any QuadItem to that item will remove the item
final QuadItem<T> quadItem = new QuadItem<>(item);
boolean individualResult = mItems.remove(quadItem);
if (individualResult) {
mQuadTree.remove(quadItem);
result = true;
}
}
}
return result;
}
/**
* Updates the provided item in the algorithm
*
* @param item the item to be updated
* @return true if the item existed in the algorithm and was updated, or false if the item did
* not exist in the algorithm and the algorithm contents remain unchanged.
*/
@Override
public boolean updateItem(T item) {
// TODO - Can this be optimized to update the item in-place if the location hasn't changed?
boolean result;
synchronized (mQuadTree) {
result = removeItem(item);
if (result) {
// Only add the item if it was removed (to help prevent accidental duplicates on map)
result = addItem(item);
}
}
return result;
}
@Override
public Set<? extends Cluster<T>> getClusters(float zoom) {
final int discreteZoom = (int) zoom;
final double zoomSpecificSpan = mMaxDistance / Math.pow(2, discreteZoom) / 256;
final Set<QuadItem<T>> visitedCandidates = new HashSet<>();
final Set<Cluster<T>> results = new HashSet<>();
final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<>();
final Map<QuadItem<T>, StaticCluster<T>> itemToCluster = new HashMap<>();
synchronized (mQuadTree) {
for (QuadItem<T> candidate : getClusteringItems(mQuadTree, zoom)) {
if (visitedCandidates.contains(candidate)) {
// Candidate is already part of another cluster.
continue;
}
Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
Collection<QuadItem<T>> clusterItems;
clusterItems = mQuadTree.search(searchBounds);
if (clusterItems.size() == 1) {
// Only the current marker is in range. Just add the single item to the results.
results.add(candidate);
visitedCandidates.add(candidate);
distanceToCluster.put(candidate, 0d);
continue;
}
StaticCluster<T> cluster = new StaticCluster<>(candidate.mClusterItem.getPosition());
results.add(cluster);
for (QuadItem<T> clusterItem : clusterItems) {
Double existingDistance = distanceToCluster.get(clusterItem);
double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
if (existingDistance != null) {
// Item already belongs to another cluster. Check if it's closer to this cluster.
if (existingDistance < distance) {
continue;
}
// Move item to the closer cluster.
itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
}
distanceToCluster.put(clusterItem, distance);
cluster.add(clusterItem.mClusterItem);
itemToCluster.put(clusterItem, cluster);
}
visitedCandidates.addAll(clusterItems);
}
}
return results;
}
protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>> quadTree, float zoom) {
return mItems;
}
@Override
public Collection<T> getItems() {
final Set<T> items = new LinkedHashSet<>();
synchronized (mQuadTree) {
for (QuadItem<T> quadItem : mItems) {
items.add(quadItem.mClusterItem);
}
}
return items;
}
@Override
public void setMaxDistanceBetweenClusteredItems(int maxDistance) {
mMaxDistance = maxDistance;
}
@Override
public int getMaxDistanceBetweenClusteredItems() {
return mMaxDistance;
}
private double distanceSquared(Point a, Point b) {
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}
private Bounds createBoundsFromSpan(Point p, double span) {
// TODO: Use a span that takes into account the visual size of the marker, not just its
// LatLng.
double halfSpan = span / 2;
return new Bounds(
p.x - halfSpan, p.x + halfSpan,
p.y - halfSpan, p.y + halfSpan);
}
protected static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> {
private final T mClusterItem;
private final Point mPoint;
private final LatLng mPosition;
private Set<T> singletonSet;
private QuadItem(T item) {
mClusterItem = item;
mPosition = item.getPosition();
mPoint = PROJECTION.toPoint(mPosition);
singletonSet = Collections.singleton(mClusterItem);
}
@Override
public Point getPoint() {
return mPoint;
}
@Override
public LatLng getPosition() {
return mPosition;
}
@Override
public Set<T> getItems() {
return singletonSet;
}
@Override
public int getSize() {
return 1;
}
@Override
public int hashCode() {
return mClusterItem.hashCode();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof QuadItem<?>)) {
return false;
}
return ((QuadItem<?>) other).mClusterItem.equals(mClusterItem);
}
}
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.car2go.maps.model.LatLng;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* A cluster whose center is determined upon creation.
*/
public class StaticCluster<T extends ClusterItem> implements Cluster<T> {
private final LatLng mCenter;
private final List<T> mItems = new ArrayList<T>();
public StaticCluster(LatLng center) {
mCenter = center;
}
public boolean add(T t) {
return mItems.add(t);
}
@Override
public LatLng getPosition() {
return mCenter;
}
public boolean remove(T t) {
return mItems.remove(t);
}
@Override
public Collection<T> getItems() {
return mItems;
}
@Override
public int getSize() {
return mItems.size();
}
@Override
public String toString() {
return "StaticCluster{" +
"mCenter=" + mCenter +
", mItems.size=" + mItems.size() +
'}';
}
@Override
public int hashCode() {
return mCenter.hashCode() + mItems.hashCode();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof StaticCluster<?>)) {
return false;
}
return ((StaticCluster<?>) other).mCenter.equals(mCenter)
&& ((StaticCluster<?>) other).mItems.equals(mItems);
}
}

View File

@@ -1,61 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.geometry;
/**
* Represents an area in the cartesian plane.
*/
public class Bounds {
public final double minX;
public final double minY;
public final double maxX;
public final double maxY;
public final double midX;
public final double midY;
public Bounds(double minX, double maxX, double minY, double maxY) {
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
midX = (minX + maxX) / 2;
midY = (minY + maxY) / 2;
}
public boolean contains(double x, double y) {
return minX <= x && x <= maxX && minY <= y && y <= maxY;
}
public boolean contains(Point point) {
return contains(point.x, point.y);
}
public boolean intersects(double minX, double maxX, double minY, double maxY) {
return minX < this.maxX && this.minX < maxX && minY < this.maxY && this.minY < maxY;
}
public boolean intersects(Bounds bounds) {
return intersects(bounds.minX, bounds.maxX, bounds.minY, bounds.maxY);
}
public boolean contains(Bounds bounds) {
return bounds.minX >= minX && bounds.maxX <= maxX && bounds.minY >= minY && bounds.maxY <= maxY;
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.geometry;
public class Point {
public final double x;
public final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.projection;
/**
* @deprecated since 0.2. Use {@link com.google.maps.android.geometry.Point} instead.
*/
@Deprecated
public class Point extends com.google.maps.android.geometry.Point {
public Point(double x, double y) {
super(x, y);
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.projection;
import com.car2go.maps.model.LatLng;
public class SphericalMercatorProjection {
final double mWorldWidth;
public SphericalMercatorProjection(final double worldWidth) {
mWorldWidth = worldWidth;
}
@SuppressWarnings("deprecation")
public Point toPoint(final LatLng latLng) {
final double x = latLng.longitude / 360 + .5;
final double siny = Math.sin(Math.toRadians(latLng.latitude));
final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5;
return new Point(x * mWorldWidth, y * mWorldWidth);
}
public LatLng toLatLng(com.google.maps.android.geometry.Point point) {
final double x = point.x / mWorldWidth - 0.5;
final double lng = x * 360;
double y = .5 - (point.y / mWorldWidth);
final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2);
return new LatLng(lat, lng);
}
}

View File

@@ -1,226 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.quadtree;
import com.google.maps.android.geometry.Bounds;
import com.google.maps.android.geometry.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* A quad tree which tracks items with a Point geometry.
* See http://en.wikipedia.org/wiki/Quadtree for details on the data structure.
* This class is not thread safe.
*/
public class PointQuadTree<T extends PointQuadTree.Item> {
public interface Item {
Point getPoint();
}
/**
* The bounds of this quad.
*/
private final Bounds mBounds;
/**
* The depth of this quad in the tree.
*/
private final int mDepth;
/**
* Maximum number of elements to store in a quad before splitting.
*/
private final static int MAX_ELEMENTS = 50;
/**
* The elements inside this quad, if any.
*/
private Set<T> mItems;
/**
* Maximum depth.
*/
private final static int MAX_DEPTH = 40;
/**
* Child quads.
*/
private List<PointQuadTree<T>> mChildren = null;
/**
* Creates a new quad tree with specified bounds.
*
* @param minX
* @param maxX
* @param minY
* @param maxY
*/
public PointQuadTree(double minX, double maxX, double minY, double maxY) {
this(new Bounds(minX, maxX, minY, maxY));
}
public PointQuadTree(Bounds bounds) {
this(bounds, 0);
}
private PointQuadTree(double minX, double maxX, double minY, double maxY, int depth) {
this(new Bounds(minX, maxX, minY, maxY), depth);
}
private PointQuadTree(Bounds bounds, int depth) {
mBounds = bounds;
mDepth = depth;
}
/**
* Insert an item.
*/
public void add(T item) {
Point point = item.getPoint();
if (this.mBounds.contains(point.x, point.y)) {
insert(point.x, point.y, item);
}
}
private void insert(double x, double y, T item) {
if (this.mChildren != null) {
if (y < mBounds.midY) {
if (x < mBounds.midX) { // top left
mChildren.get(0).insert(x, y, item);
} else { // top right
mChildren.get(1).insert(x, y, item);
}
} else {
if (x < mBounds.midX) { // bottom left
mChildren.get(2).insert(x, y, item);
} else {
mChildren.get(3).insert(x, y, item);
}
}
return;
}
if (mItems == null) {
mItems = new LinkedHashSet<>();
}
mItems.add(item);
if (mItems.size() > MAX_ELEMENTS && mDepth < MAX_DEPTH) {
split();
}
}
/**
* Split this quad.
*/
private void split() {
mChildren = new ArrayList<PointQuadTree<T>>(4);
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.minY, mBounds.midY, mDepth + 1));
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.minY, mBounds.midY, mDepth + 1));
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.midY, mBounds.maxY, mDepth + 1));
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.midY, mBounds.maxY, mDepth + 1));
Set<T> items = mItems;
mItems = null;
for (T item : items) {
// re-insert items into child quads.
insert(item.getPoint().x, item.getPoint().y, item);
}
}
/**
* Remove the given item from the set.
*
* @return whether the item was removed.
*/
public boolean remove(T item) {
Point point = item.getPoint();
if (this.mBounds.contains(point.x, point.y)) {
return remove(point.x, point.y, item);
} else {
return false;
}
}
private boolean remove(double x, double y, T item) {
if (this.mChildren != null) {
if (y < mBounds.midY) {
if (x < mBounds.midX) { // top left
return mChildren.get(0).remove(x, y, item);
} else { // top right
return mChildren.get(1).remove(x, y, item);
}
} else {
if (x < mBounds.midX) { // bottom left
return mChildren.get(2).remove(x, y, item);
} else {
return mChildren.get(3).remove(x, y, item);
}
}
} else {
if (mItems == null) {
return false;
} else {
return mItems.remove(item);
}
}
}
/**
* Removes all points from the quadTree
*/
public void clear() {
mChildren = null;
if (mItems != null) {
mItems.clear();
}
}
/**
* Search for all items within a given bounds.
*/
public Collection<T> search(Bounds searchBounds) {
final List<T> results = new ArrayList<T>();
search(searchBounds, results);
return results;
}
private void search(Bounds searchBounds, Collection<T> results) {
if (!mBounds.intersects(searchBounds)) {
return;
}
if (this.mChildren != null) {
for (PointQuadTree<T> quad : mChildren) {
quad.search(searchBounds, results);
}
} else if (mItems != null) {
if (searchBounds.contains(mBounds)) {
results.addAll(mItems);
} else {
for (T item : mItems) {
if (searchBounds.contains(item.getPoint())) {
results.add(item);
}
}
}
}
}
}

View File

@@ -3,9 +3,15 @@ package net.vonforst.evmap
import android.app.Activity
import android.app.Application
import android.os.Build
import androidx.work.*
import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import net.vonforst.evmap.storage.CleanupCacheWorker
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.storage.UpdateFullDownloadWorker
import net.vonforst.evmap.ui.updateAppLocale
import net.vonforst.evmap.ui.updateNightMode
import org.acra.config.dialog
@@ -24,7 +30,7 @@ class EvMapApplication : Application(), Configuration.Provider {
// Convert to new AppCompat storage for app language
val lang = prefs.language
if (lang != null && lang !in listOf("", "default")) {
if (lang != null) {
updateAppLocale(lang)
prefs.language = null
}
@@ -64,6 +70,7 @@ class EvMapApplication : Application(), Configuration.Provider {
}
}
val workManager = WorkManager.getInstance(this)
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
.setConstraints(Constraints.Builder().apply {
setRequiresBatteryNotLow(true)
@@ -71,9 +78,24 @@ class EvMapApplication : Application(), Configuration.Provider {
setRequiresDeviceIdle(true)
}
}.build()).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
workManager.enqueueUniquePeriodicWork(
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.UPDATE, cleanupCacheRequest
)
val updateFullDownloadRequest =
PeriodicWorkRequestBuilder<UpdateFullDownloadWorker>(Duration.ofDays(7))
.setConstraints(Constraints.Builder().apply {
setRequiresBatteryNotLow(true)
setRequiredNetworkType(NetworkType.UNMETERED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setRequiresDeviceIdle(true)
}
}.build()).build()
workManager.enqueueUniquePeriodicWork(
"UpdateOsmWorker",
ExistingPeriodicWorkPolicy.UPDATE,
updateFullDownloadRequest
)
}
override val workManagerConfiguration = Configuration.Builder().build()

View File

@@ -3,6 +3,8 @@ package net.vonforst.evmap
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -11,12 +13,12 @@ import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
@@ -44,22 +46,21 @@ const val EXTRA_DONATE = "donate"
class MapsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
interface FragmentCallback {
fun getRootView(): View
}
private var reenterState: Bundle? = null
private lateinit var navController: NavController
private lateinit var navHostFragment: NavHostFragment
lateinit var appBarConfiguration: AppBarConfiguration
var fragmentCallback: FragmentCallback? = null
private lateinit var prefs: PreferenceDataSource
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
WindowCompat.enableEdgeToEdge(window)
setContentView(R.layout.activity_maps)
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.map,
@@ -67,9 +68,9 @@ class MapsActivity : AppCompatActivity(),
R.id.about,
R.id.settings
),
findViewById<DrawerLayout>(R.id.drawer_layout)
drawerLayout
)
val navHostFragment =
navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
@@ -87,6 +88,17 @@ class MapsActivity : AppCompatActivity(),
checkPlayServices(this)
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
var deepLink: PendingIntent? = null
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.onboarding) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
} else {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
}
}
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// wait for splash screen animation to finish on first start
@@ -104,137 +116,128 @@ class MapsActivity : AppCompatActivity(),
}
})
}
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else if (!prefs.privacyAccepted) {
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else {
navGraph.setStartDestination(R.id.map)
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
var deepLink: PendingIntent? = null
} else if (intent?.scheme == "geo") {
val query = intent.data?.query?.split("=")?.get(1)
val coords = getLocationFromIntent(intent)
if (intent?.scheme == "geo") {
val query = intent.data?.query?.split("=")?.get(1)
val coords = getLocationFromIntent(intent)
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
} else if (!query.isNullOrEmpty()) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "goingelectric") {
prefs.dataSource = "goingelectric"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_goingelectric)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host in listOf("openchargemap.org", "map.openchargemap.io")) {
val id = when (intent.data?.host) {
"openchargemap.org" -> intent.data?.pathSegments?.last()?.toLongOrNull()
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
else -> null
}
if (id != null) {
if (prefs.dataSource != "openchargemap") {
prefs.dataSource = "openchargemap"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_openchargemap)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent.scheme == "net.vonforst.evmap") {
intent.data?.let {
if (it.host == "find_charger") {
val lat = it.getQueryParameter("latitude")?.toDouble()
val lon = it.getQueryParameter("longitude")?.toDouble()
val name = it.getQueryParameter("name")
if (lat != null && lon != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
latLng = LatLng(lat, lon),
locationName = name
).toBundle()
)
.createPendingIntent()
} else if (name != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = name).toBundle())
.createPendingIntent()
}
}
}
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
} else if (!query.isNullOrEmpty()) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.favs)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
}
deepLink?.send()
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "goingelectric") {
prefs.dataSource = "goingelectric"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_goingelectric)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host in listOf(
"openchargemap.org",
"map.openchargemap.io"
)
) {
val id = when (intent.data?.host) {
"openchargemap.org" -> intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
else -> null
}
if (id != null) {
if (prefs.dataSource != "openchargemap") {
prefs.dataSource = "openchargemap"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_openchargemap)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent.scheme == "net.vonforst.evmap") {
intent.data?.let {
if (it.host == "find_charger") {
val lat = it.getQueryParameter("latitude")?.toDouble()
val lon = it.getQueryParameter("longitude")?.toDouble()
val name = it.getQueryParameter("name")
if (lat != null && lon != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
latLng = LatLng(lat, lon),
locationName = name
).toBundle()
)
.createPendingIntent()
} else if (name != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = name).toBundle())
.createPendingIntent()
}
}
}
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
deepLink = navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.favs)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.createPendingIntent()
}
deepLink?.send()
}
fun navigateTo(charger: ChargeLocation) {
fun navigateTo(charger: ChargeLocation, rootView: View) {
// google maps navigation
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
@@ -244,11 +247,11 @@ class MapsActivity : AppCompatActivity(),
startActivity(intent)
} else {
// fallback: generic geo intent
showLocation(charger)
showLocation(charger, rootView)
}
}
fun showLocation(charger: ChargeLocation) {
fun showLocation(charger: ChargeLocation, rootView: View) {
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(
@@ -256,20 +259,33 @@ class MapsActivity : AppCompatActivity(),
Uri.encode(charger.name)
})"
)
if (intent.resolveActivity(packageManager) != null) {
val resolveInfo =
packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg =
resolveInfo?.activityInfo?.packageName.takeIf { it != "android" && it != packageName }
if (pkg == null) {
// There is no default maps app or EVMap itself is the current default, fall back to app chooser
val chooserIntent = Intent.createChooser(intent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
startActivity(chooserIntent)
return
}
intent.setPackage(pkg)
try {
startActivity(intent)
} else {
val cb = fragmentCallback ?: return
} catch (e: ActivityNotFoundException) {
Snackbar.make(
cb.getRootView(),
rootView,
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
).show()
}
}
fun openUrl(url: String, preferBrowser: Boolean = true) {
val pkg = CustomTabsClient.getPackageName(this, null)
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = false) {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
@@ -277,17 +293,49 @@ class MapsActivity : AppCompatActivity(),
.build()
)
.build()
pkg?.let {
// prefer to open URL in custom tab, even if native app
// available (such as EVMap itself)
if (preferBrowser) intent.intent.setPackage(pkg)
val uri = Uri.parse(url)
val viewIntent = Intent(Intent.ACTION_VIEW, uri)
if (preferBrowser) {
// EVMap may be set as default app for this link, but we want to open it in a browser
// try to find default web browser
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
val resolveInfo =
packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg = resolveInfo?.activityInfo?.packageName.takeIf { it != "android" }
if (pkg == null) {
// There is no default browser, fall back to app chooser
val chooserIntent = Intent.createChooser(viewIntent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
val targets: List<ResolveInfo> = packageManager.queryIntentActivities(
viewIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
// add missing browsers (if EVMap is already set as default, Android might not find other browsers with the specific intent)
val browsers = packageManager.queryIntentActivities(
browserIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
val extraIntents = browsers.filter { browser ->
targets.find { it.activityInfo.packageName == browser.activityInfo.packageName } == null
}.map { browser ->
Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(browser.activityInfo.packageName)
}
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toTypedArray())
startActivity(chooserIntent)
return
}
intent.intent.setPackage(pkg)
}
try {
intent.launchUrl(this, Uri.parse(url))
intent.launchUrl(this, uri)
} catch (e: ActivityNotFoundException) {
val cb = fragmentCallback ?: return
Snackbar.make(
cb.getRootView(),
rootView,
R.string.no_browser_app_found,
Snackbar.LENGTH_SHORT
).show()

View File

@@ -16,7 +16,10 @@ import android.text.SpannableStringBuilder
import android.text.SpannedString
import android.text.TextUtils
import android.text.style.StyleSpan
import android.view.View
import android.view.ViewTreeObserver
import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.Currency
import java.util.Locale
fun Bundle.optDouble(name: String): Double? {
@@ -139,4 +142,14 @@ fun PackageManager.isAppInstalled(packageName: String): Boolean {
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
}
fun currencyDisplayName(code: String) = "${Currency.getInstance(code).displayName} ($code)"
inline fun View.waitForLayout(crossinline f: () -> Unit) =
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
})

View File

@@ -9,12 +9,12 @@ import androidx.core.text.buildSpannedString
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.availability.tesla.Rates
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.bold
import net.vonforst.evmap.joinToSpannedString
import net.vonforst.evmap.model.ChargeCard
import net.vonforst.evmap.model.ChargeCardId
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Coordinate
import net.vonforst.evmap.model.OpeningHoursDays
import net.vonforst.evmap.plus
import net.vonforst.evmap.ui.currency

View File

@@ -5,7 +5,6 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@@ -14,6 +13,7 @@ import coil.load
import coil.memory.MemoryCache
import net.vonforst.evmap.R
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.waitForLayout
class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) :
@@ -39,12 +39,9 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
val item = getItem(position)
if (holder.view.height == 0) {
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this)
loadImage(item, holder)
}
})
holder.view.waitForLayout {
loadImage(item, holder)
}
} else {
loadImage(item, holder)
}

View File

@@ -6,6 +6,7 @@ import com.car2go.maps.model.LatLngBounds
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.Resource
import java.time.Duration
@@ -58,6 +59,27 @@ interface ChargepointApi<out T : ReferenceData> {
* Duration we are limited to if there is a required API local cache time limit.
*/
val cacheLimit: Duration
/**
* Whether this API supports querying for chargers at the backend
*
* This determines whether the getChargepoints, getChargepointsRadius and getChargepointDetail functions are supported.
*/
val supportsOnlineQueries: Boolean
/**
* Whether this API supports downloading the whole dataset into local storage
*
* This determines whether the getAllChargepoints function is supported.
*/
val supportsFullDownload: Boolean
/**
* Fetches all available chargers from this API.
*
* This may take a long time and should only be used when the user explicitly wants to download all chargers.
*/
suspend fun fullDownload(): FullDownloadResult<T>
}
interface StringProvider {
@@ -79,6 +101,7 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
)
)
}
"goingelectric" -> {
GoingElectricApiWrapper(
ctx.getString(
@@ -86,6 +109,11 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
)
)
}
"openstreetmap" -> {
OpenStreetMapApiWrapper()
}
else -> throw IllegalArgumentException()
}
}
@@ -100,4 +128,20 @@ data class ChargepointList(val items: List<ChargepointListItem>, val isComplete:
companion object {
fun empty() = ChargepointList(emptyList(), true)
}
}
/**
* Result returned from fullDownload() function.
*
* Note that [chargers] is implemented as a [Sequence] so that downloaded chargers can be saved
* while they are being parsed instead of having to keep all of them in RAM at once.
*
* [progress] is updated regularly to indicate the current download progress.
* [referenceData] will typically only be available once the download is completed, i.e. you have
* iterated over the whole sequence of [chargers].
*/
interface FullDownloadResult<out T : ReferenceData> {
val chargers: Sequence<ChargeLocation>
val progress: Float
val referenceData: T
}

View File

@@ -1,18 +1,20 @@
package net.vonforst.evmap.api
import com.google.common.util.concurrent.RateLimiter
import okhttp3.Interceptor
import okhttp3.Response
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource
class RateLimitInterceptor : Interceptor {
private val rateLimiter = RateLimiter.create(3.0)
private val rateLimiter = SimpleRateLimiter(3.0)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host == "ui-map.shellrecharge.com") {
// limit requests sent to NewMotion to 3 per second
rateLimiter.acquire(1)
rateLimiter.acquire()
var response: Response = chain.proceed(request)
// 403 is how the NewMotion API indicates a rate limit error
@@ -30,4 +32,27 @@ class RateLimitInterceptor : Interceptor {
return chain.proceed(request)
}
}
}
internal class SimpleRateLimiter(private val permitsPerSecond: Double) {
private val interval: Duration = (1.0 / permitsPerSecond).seconds
private var nextAvailable = TimeSource.Monotonic.markNow()
@Synchronized
fun acquire() {
val now = TimeSource.Monotonic.markNow()
if (now < nextAvailable) {
val waitTime = nextAvailable - now
waitTime.sleep()
nextAvailable += interval
} else {
nextAvailable = now + interval
}
}
}
fun Duration.sleep() {
if (this.isPositive()) {
Thread.sleep(this.inWholeMilliseconds, (this.inWholeNanoseconds % 1_000_000).toInt())
}
}

View File

@@ -1,54 +1,17 @@
package net.vonforst.evmap.api
import androidx.annotation.DrawableRes
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import net.vonforst.evmap.R
import net.vonforst.evmap.model.Chargepoint
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import org.json.JSONArray
import java.io.IOException
import kotlin.coroutines.resumeWithException
import kotlin.math.abs
operator fun <T> JSONArray.iterator(): Iterator<T> =
(0 until length()).asSequence().map {
@Suppress("UNCHECKED_CAST")
get(it) as T
}.iterator()
@ExperimentalCoroutinesApi
suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response) {}
}
override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
})
continuation.invokeOnCancellation {
try {
cancel()
} catch (ex: Throwable) {
//Ignore cancel exception
}
}
}
}
private val plugNames = mapOf(
Chargepoint.TYPE_1 to R.string.plug_type_1,
Chargepoint.TYPE_2_UNKNOWN to R.string.plug_type_2,
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2,
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
Chargepoint.TYPE_3 to R.string.plug_type_3,
Chargepoint.TYPE_3A to R.string.plug_type_3a,
Chargepoint.TYPE_3C to R.string.plug_type_3c,
Chargepoint.CCS_UNKNOWN to R.string.plug_ccs,
Chargepoint.CCS_TYPE_1 to R.string.plug_ccs,
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
@@ -101,7 +64,7 @@ fun iconForPlugType(type: String): Int =
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
// TODO: add other connectors
else -> 0
else -> R.drawable.ic_connector_unknown
}
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)

View File

@@ -7,8 +7,6 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.cartesianProduct
import net.vonforst.evmap.model.ChargeLocation
@@ -18,7 +16,6 @@ import net.vonforst.evmap.viewmodel.Resource
import okhttp3.Cache
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.HttpException
import java.io.IOException
import java.net.CookieManager
@@ -41,16 +38,6 @@ interface AvailabilityDetector {
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
protected val radius = 150 // max radius in meters
protected suspend fun httpGet(url: String): String {
val request = Request.Builder().url(url).build()
val response = client.newCall(request).await()
if (!response.isSuccessful) throw IOException(response.message)
val str = response.body!!.string()
return str
}
protected fun getCorrespondingChargepoint(
cps: Iterable<Chargepoint>, type: String, power: Double
): Chargepoint? {

View File

@@ -4,7 +4,6 @@ import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import net.vonforst.evmap.api.availability.tesla.LocalTimeAdapter
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.utils.distanceBetween
@@ -15,7 +14,6 @@ import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import java.time.Instant
import java.time.LocalTime
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
private const val maxDistance = 60 // max distance between reported positions in meters
@@ -202,7 +200,8 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
val id = index.toLong()
val power = connector.maxPowerInKw ?: 0.0
val type = when (connector.plugTypeName) {
"Typ 3A" -> Chargepoint.TYPE_3
"Typ 3A" -> Chargepoint.TYPE_3A
"Typ 3C \"Scame\"" -> Chargepoint.TYPE_3C
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
"Typ 1" -> Chargepoint.TYPE_1
"Steckdose(D)" -> Chargepoint.SCHUKO
@@ -243,8 +242,8 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
}
override fun isChargerSupported(charger: ChargeLocation): Boolean {
val country = charger.chargepriceData?.country
?: charger.address?.country ?: return false
val country = charger.chargepriceData?.country ?: charger.address?.country
return when (charger.dataSource) {
// list of countries as of 2023/04/14, according to
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
@@ -286,6 +285,12 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
"ES",
"CZ"
) && charger.chargepriceData?.network !in listOf("23", "3534")
/* TODO: OSM usually does not have the country tagged. Therefore we currently just use
a bounding box to determine whether the charger is roughly in Europe */
"openstreetmap" -> charger.coordinates.lat in 35.0..72.0
&& charger.coordinates.lng in 25.0..65.0
&& charger.operator !in listOf("Tesla, Inc.", "Tesla")
else -> false
}
}

View File

@@ -1,6 +1,5 @@
package net.vonforst.evmap.api.availability
import androidx.car.app.model.DateTimeWithZone
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
@@ -13,12 +12,8 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeParseException
import java.util.*
import java.util.Locale
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
private const val maxDistance = 60 // max distance between reported positions in meters
@@ -180,7 +175,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
"type3" -> Chargepoint.TYPE_3
"type3" -> Chargepoint.TYPE_3C
"type2" -> Chargepoint.TYPE_2_UNKNOWN
"type1" -> Chargepoint.TYPE_1
"domestic" -> Chargepoint.SCHUKO
@@ -226,6 +221,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
return when (charger.dataSource) {
"goingelectric" -> charger.network != "Tesla Supercharger"
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
"openstreetmap" -> charger.operator !in listOf("Tesla, Inc.", "Tesla")
else -> false
}
}

View File

@@ -23,6 +23,10 @@ class TeslaGuestAvailabilityDetector(
private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl)
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) {
throw AvailabilityDetectorException("no candidates found.")
}
val results = cuaApi.getTeslaLocations()
val result =
@@ -52,32 +56,31 @@ class TeslaGuestAvailabilityDetector(
val (detailsA, guestPricing) = coroutineScope {
val details = async {
api.getChargingSiteDetails(
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
api.getSiteDetails(
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
TeslaChargingGuestGraphQlApi.Identifier(
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
trtId
trtId, TeslaChargingGuestGraphQlApi.Experience.ADHOC
)
),
TeslaChargingGuestGraphQlApi.Experience.ADHOC
)
)
).data.site ?: throw AvailabilityDetectorException("no candidates found.")
).data.chargingNetwork?.site
?: throw AvailabilityDetectorException("no candidates found.")
}
val guestPricing = async {
api.getChargingSiteDetails(
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
api.getSiteDetails(
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
TeslaChargingGuestGraphQlApi.Identifier(
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
trtId
trtId, TeslaChargingGuestGraphQlApi.Experience.GUEST
)
),
TeslaChargingGuestGraphQlApi.Experience.GUEST
)
)
).data.site?.pricing
).data.chargingNetwork?.site?.pricing
}
details to guestPricing
}
@@ -103,12 +106,9 @@ class TeslaGuestAvailabilityDetector(
"charger has unknown connectors"
)
val chargerDetails = details.chargersAvailable.chargerDetails
val chargers = details.chargers.associateBy { it.id }
var detailsSorted = chargerDetails
.sortedBy { chargers[it.id]?.labelLetter }
.sortedBy { chargers[it.id]?.labelNumber }
var detailsSorted = details.chargerList
.sortedBy { c -> c.labelLetter }
.sortedBy { c -> c.labelNumber }
if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
// apparently some connectors are missing in Tesla data
@@ -120,7 +120,7 @@ class TeslaGuestAvailabilityDetector(
detailsSorted + List(numMissing) {
TeslaChargingGuestGraphQlApi.ChargerDetail(
ChargerAvailability.UNKNOWN,
""
"", ""
)
}
} else {
@@ -151,9 +151,9 @@ class TeslaGuestAvailabilityDetector(
}
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
val labelsMap = detailsMap.mapValues { it.value.map { chargers[it.id]?.label } }
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
val pricing = details.pricing.copy(memberRates = guestPricing.await()?.userRates)
val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)
return ChargeLocationStatus(
statusMap,
@@ -167,6 +167,7 @@ class TeslaGuestAvailabilityDetector(
return when (charger.dataSource) {
"goingelectric" -> charger.network == "Tesla Supercharger"
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
else -> false
}
}

View File

@@ -2,7 +2,6 @@ package net.vonforst.evmap.api.availability
import net.vonforst.evmap.api.availability.tesla.ChargerAvailability
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingGuestGraphQlApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.api.availability.tesla.asTeslaCoord
import net.vonforst.evmap.model.ChargeLocation
@@ -30,6 +29,10 @@ class TeslaOwnerAvailabilityDetector(
}
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) {
throw AvailabilityDetectorException("no candidates found.")
}
val api = initApi()
val req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest(
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables(
@@ -42,8 +45,7 @@ class TeslaOwnerAvailabilityDetector(
TeslaChargingOwnershipGraphQlApi.Coordinate(
location.coordinates.lat - coordRange,
location.coordinates.lng + coordRange
),
TeslaChargingOwnershipGraphQlApi.OpenToNonTeslasFilterValue(false)
)
)
)
)
@@ -59,7 +61,7 @@ class TeslaOwnerAvailabilityDetector(
val details = api.getChargingSiteInformation(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables(
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.id.text),
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.locationGUID),
TeslaChargingOwnershipGraphQlApi.VehicleMakeType.NON_TESLA
)
)
@@ -164,6 +166,7 @@ class TeslaOwnerAvailabilityDetector(
return when (charger.dataSource) {
"goingelectric" -> charger.network == "Tesla Supercharger"
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
else -> false
}
}

View File

@@ -58,7 +58,7 @@ data class Rates(
@JsonClass(generateAdapter = true)
data class Pricebook(
val charging: PricebookDetails,
val parking: PricebookDetails,
val parking: PricebookDetails?,
val priceBookID: Long?
)

View File

@@ -1,12 +1,8 @@
package net.vonforst.evmap.api.availability.tesla
import com.squareup.moshi.Json
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import retrofit2.Retrofit
@@ -15,7 +11,6 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import java.lang.reflect.Type
import java.util.concurrent.TimeUnit
interface TeslaCuaApi {
@@ -71,24 +66,22 @@ interface TeslaCuaApi {
interface TeslaChargingGuestGraphQlApi {
@POST("graphql")
suspend fun getChargingSiteDetails(
@Body request: GetChargingSiteDetailsRequest,
@Query("operationName") operationName: String = "getGuestChargingSiteDetails"
suspend fun getSiteDetails(
@Body request: GetSiteDetailsRequest,
@Query("operationName") operationName: String = "GetSiteDetails"
): GetChargingSiteDetailsResponse
@JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsRequest(
override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getGuestChargingSiteDetails",
data class GetSiteDetailsRequest(
override val variables: GetSiteDetailsVariables,
override val operationName: String = "GetSiteDetails",
override val query: String =
"\n query getGuestChargingSiteDetails(\$identifier: ChargingSiteIdentifierInput!, \$deviceLocale: String!, \$experience: ChargingExperienceEnum!) {\n site(\n identifier: \$identifier\n deviceLocale: \$deviceLocale\n experience: \$experience\n ) {\n activeOutages\n address {\n countryCode\n }\n chargers {\n id\n label\n }\n chargersAvailable {\n chargerDetails {\n id\n availability\n }\n }\n holdAmount {\n holdAmount\n currencyCode\n }\n maxPowerKw\n name\n programType\n publicStallCount\n id\n pricing(experience: \$experience) {\n userRates {\n activePricebook {\n charging {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n parking {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n congestion {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n }\n }\n }\n }\n}\n "
"\n query GetSiteDetails(\$siteId: SiteIdInput!) {\n chargingNetwork {\n site(siteId: \$siteId) {\n address {\n countryCode\n }\n chargerList {\n id\n label\n availability\n }\n holdAmount {\n amount\n currencyCode\n }\n maxPowerKw\n name\n programType\n publicStallCount\n trtId\n pricing {\n userRates {\n activePricebook {\n charging {\n ...ChargingRate\n }\n parking {\n ...ChargingRate\n }\n congestion {\n ...ChargingRate\n }\n }\n }\n }\n }\n }\n}\n \n fragment ChargingRate on ChargingUserRate {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n}\n "
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
data class GetChargingSiteInformationVariables(
val identifier: Identifier,
val experience: Experience,
val deviceLocale: String = "de-DE",
data class GetSiteDetailsVariables(
val siteId: Identifier,
)
enum class Experience {
@@ -97,22 +90,22 @@ interface TeslaChargingGuestGraphQlApi {
@JsonClass(generateAdapter = true)
data class Identifier(
val siteId: ChargingSiteIdentifier
val byTrtId: ChargingSiteIdentifier
)
@JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier(
val id: Long,
val siteType: SiteType = SiteType.SUPERCHARGER
val trtId: Long,
val chargingExperience: Experience,
val programType: String = "PTSCH",
val locale: String = "de-DE",
)
enum class SiteType {
@Json(name = "SITE_TYPE_SUPERCHARGER")
SUPERCHARGER
}
@JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseDataNetwork)
@JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseData)
data class GetChargingSiteDetailsResponseDataNetwork(val chargingNetwork: GetChargingSiteDetailsResponseData?)
@JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponseData(val site: ChargingSiteInformation?)
@@ -120,19 +113,19 @@ interface TeslaChargingGuestGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSiteInformation(
val activeOutages: List<Outage>?,
val chargers: List<ChargerId>,
val chargersAvailable: ChargersAvailable,
val id: Long,
val chargerList: List<ChargerDetail>,
val trtId: Long,
val maxPowerKw: Int,
val name: String,
val pricing: Pricing,
val pricing: Pricing?,
val publicStallCount: Int
)
@JsonClass(generateAdapter = true)
data class ChargerId(
val id: String,
data class ChargerDetail(
val availability: ChargerAvailability,
val label: String?,
val id: String
) {
val labelNumber
get() = label?.replace(Regex("""\D"""), "")?.toInt()
@@ -140,15 +133,6 @@ interface TeslaChargingGuestGraphQlApi {
get() = label?.replace(Regex("""\d"""), "")
}
@JsonClass(generateAdapter = true)
data class ChargersAvailable(val chargerDetails: List<ChargerDetail>)
@JsonClass(generateAdapter = true)
data class ChargerDetail(
val availability: ChargerAvailability,
val id: String
)
companion object {
fun create(
client: OkHttpClient,

View File

@@ -16,7 +16,6 @@ import retrofit2.http.POST
import retrofit2.http.Query
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.LocalTime
interface TeslaAuthenticationApi {
@POST("oauth2/v3/token")
@@ -101,7 +100,8 @@ interface TeslaAuthenticationApi {
.appendQueryParameter("code_challenge_method", "S256")
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("scope", "openid email offline_access")
.appendQueryParameter("scope", "openid email offline_access phone")
.appendQueryParameter("is_in_app", "true")
.appendQueryParameter("state", "123").build()
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
@@ -131,8 +131,8 @@ interface TeslaOwnerApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)
@@ -173,7 +173,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetNearbyChargingSitesVariables,
override val operationName: String = "GetNearbyChargingSites",
override val query: String =
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n "
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n locationGUID\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n teslaExclusive\n amenities\n chargingAccessibility\n ownerType\n isThirdPartySite\n usabilityArchetype\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isMagicDockSupportedSite\n hasParkingBenefit\n hasTou\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n"
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -184,7 +184,7 @@ interface TeslaChargingOwnershipGraphQlApi {
val userLocation: Coordinate,
val northwestCorner: Coordinate,
val southeastCorner: Coordinate,
val openToNonTeslasFilter: OpenToNonTeslasFilterValue,
val filters: List<String> = emptyList(),
val languageCode: String = "en",
val countryCode: String = "US",
//val vin: String = "",
@@ -202,7 +202,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getChargingSiteInformation",
override val query: String =
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isMagicDockSupportedSite\n trtId {\n value\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n }\n waitEstimateBucket\n currentCongestion\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n upsellingBanner(vehicleMakeType: \$vehicleMakeType) {\n header\n caption\n backgroundImageUrl\n routeName\n }\n nacsOnlyAssets {\n banner {\n header\n caption\n link\n }\n disclaimer {\n text\n sheetTitle\n sheetContent\n }\n }\n enableChargingSiteReportIssue\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n locationGUID\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isThirdPartySite\n isMagicDockSupportedSite\n trtId {\n value\n }\n siteDisclaimer\n chargingAccessibility\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isCanvasSite\n ownerDisclaimer\n chargingFeesDisclaimer {\n title\n description\n }\n idleFeesDisclaimer {\n title\n description\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n stateOfCharge\n chargerDisabled\n }\n waitEstimateBucket\n currentCongestion\n usabilityArchetype\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n dynamicRates {\n enabled\n }\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n congestion {\n ...ChargingUserRateFragment\n }\n service {\n ...ChargingUserRateFragment\n }\n electricity {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n stateOfCharge\n congestionGracePeriodSecs\n congestionPercent\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -217,11 +217,11 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier(
val id: String,
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.LOCATION_GUID
)
enum class ChargingSiteIdentifierType {
SITE_ID
SITE_ID, LOCATION_GUID
}
enum class VehicleMakeType {
@@ -242,7 +242,6 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSite(
val activeOutages: List<Outage>,
val availableStalls: Value<Int>?,
val centroid: Coordinate,
val drivingDistanceMiles: Value<Double>?,
@@ -251,7 +250,8 @@ interface TeslaChargingOwnershipGraphQlApi {
val id: Text,
val localizedSiteName: Value<String>,
val maxPowerKw: Value<Int>,
val totalStalls: Value<Int>
val totalStalls: Value<Int>,
val locationGUID: String
// TODO: siteType, accessType
)
@@ -274,7 +274,6 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class SiteDynamic(
val activeOutages: List<Outage>,
val chargerDetails: List<ChargerDetail>,
val chargersAvailable: Value<Int>?,
val currentCongestion: Double,
@@ -373,8 +372,8 @@ interface TeslaChargingOwnershipGraphQlApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $t")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)

View File

@@ -1,10 +1,7 @@
package net.vonforst.evmap.api.fronyx
import android.content.Context
import com.squareup.moshi.JsonDataException
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType
@@ -13,8 +10,6 @@ import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.Resource
import retrofit2.HttpException
import java.io.IOException
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
@@ -55,9 +50,9 @@ class PredictionRepository(private val context: Context) {
evseIds: Map<Chargepoint, List<String>>,
filteredConnectors: Set<String>?
): Resource<List<FronyxEvseIdResponse>> {
if (!prefs.predictionEnabled) return Resource.success(null)
return Resource.success(null)
val allEvseIds =
/*val allEvseIds =
evseIds.filterKeys {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors?.let { filtered ->
@@ -89,7 +84,7 @@ class PredictionRepository(private val context: Context) {
// malformed JSON response from fronyx API
e.printStackTrace()
return Resource.error(e.message, null)
}
}*/
}
private fun buildPredictionGraph(

View File

@@ -15,6 +15,7 @@ import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.FiltersSQLQuery
import net.vonforst.evmap.api.FullDownloadResult
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.mapPower
import net.vonforst.evmap.api.mapPowerInverse
@@ -159,6 +160,12 @@ class GoingElectricApiWrapper(
override val name = "GoingElectric.de"
override val id = "goingelectric"
override val cacheLimit = Duration.ofDays(1)
override val supportsOnlineQueries = true
override val supportsFullDownload = false
override suspend fun fullDownload(): FullDownloadResult<GEReferenceData> {
throw NotImplementedError()
}
override suspend fun getChargepoints(
referenceData: ReferenceData,
@@ -452,7 +459,10 @@ class GoingElectricApiWrapper(
if (responses.map { it.isSuccessful }.all { it }
&& plugsResponse.body()!!.status == STATUS_OK
&& chargeCardsResponse.body()!!.status == STATUS_OK
&& networksResponse.body()!!.status == STATUS_OK) {
&& networksResponse.body()!!.status == STATUS_OK
&& plugsResponse.body()!!.result != null
&& chargeCardsResponse.body()!!.result != null
&& networksResponse.body()!!.result != null) {
Resource.success(
GEReferenceData(
plugsResponse.body()!!.result!!,

View File

@@ -208,7 +208,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
return when (type) {
Chargepoint.TYPE_1 -> "Typ1"
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
Chargepoint.TYPE_3 -> "Typ3"
Chargepoint.TYPE_3C -> "Typ3"
Chargepoint.CCS_UNKNOWN -> "CCS"
Chargepoint.CCS_TYPE_2 -> "Typ2"
Chargepoint.SCHUKO -> "Schuko"
@@ -225,7 +225,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
return when (type) {
"Typ1" -> Chargepoint.TYPE_1
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
"Typ3" -> Chargepoint.TYPE_3
"Typ3" -> Chargepoint.TYPE_3C
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
"CCS" -> Chargepoint.CCS_UNKNOWN
"Schuko" -> Chargepoint.SCHUKO

View File

@@ -11,6 +11,7 @@ import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.FiltersSQLQuery
import net.vonforst.evmap.api.FullDownloadResult
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.mapPower
import net.vonforst.evmap.api.mapPowerInverse
@@ -130,6 +131,12 @@ class OpenChargeMapApiWrapper(
override val name = "OpenChargeMap.org"
override val id = "openchargemap"
override val supportsOnlineQueries = true
override val supportsFullDownload = false
override suspend fun fullDownload(): FullDownloadResult<OCMReferenceData> {
throw NotImplementedError()
}
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
if (value == null || value.all) null else value.values.joinToString(",")

View File

@@ -76,7 +76,7 @@ data class OCMChargepoint(
mediaItems?.mapNotNull { it.convert() },
null,
null,
cost?.let { Cost(descriptionShort = it) },
cost?.takeIf { it.isNotBlank() }.let { Cost(descriptionShort = it) },
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
ChargepriceData(
addressInfo.countryISOCode(refData),
@@ -180,8 +180,8 @@ data class OCMConnection(
25L -> Chargepoint.TYPE_2_SOCKET
1036L -> Chargepoint.TYPE_2_PLUG
1L -> Chargepoint.TYPE_1
36L -> Chargepoint.TYPE_3
26L -> Chargepoint.TYPE_3
36L -> Chargepoint.TYPE_3A
26L -> Chargepoint.TYPE_3C
else -> title ?: ""
}
}
@@ -254,7 +254,7 @@ data class OCMUserComment(
@Json(name = "ID") val id: Long,
@Json(name = "CommentTypeID") val commentTypeId: Long,
@Json(name = "Comment") val comment: String?,
@Json(name = "UserName") val userName: String,
@Json(name = "UserName") val userName: String?,
@Json(name = "DateCreated") val dateCreated: ZonedDateTime
)

View File

@@ -0,0 +1,67 @@
package net.vonforst.evmap.api.openstreetmap
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.rawType
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
import java.time.Instant
import kotlin.math.floor
internal class InstantAdapter {
@FromJson
fun fromJson(value: Double?): Instant? = value?.let {
val seconds = floor(it).toLong()
val nanos = ((value - seconds) * 1e9).toLong()
Instant.ofEpochSecond(seconds, nanos)
}
@ToJson
fun toJson(value: Instant?): Double? = value?.let {
it.epochSecond.toDouble() + it.nano / 1e9
}
}
internal class OSMConverterFactory(val moshi: Moshi) : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
if (type.rawType != OSMDocument::class.java) return null
val instantAdapter = moshi.adapter(Instant::class.java)
val osmChargingStationAdapter = moshi.adapter(OSMChargingStation::class.java)
val longAdapter = moshi.adapter(Long::class.java)
return Converter<ResponseBody, OSMDocument> { body ->
val reader = JsonReader.of(body.source())
reader.beginObject()
var timestamp: Instant? = null
var doc: Sequence<OSMChargingStation>? = null
var count: Long? = null
while (reader.hasNext()) {
when (reader.nextName()) {
"timestamp" -> timestamp = instantAdapter.fromJson(reader)!!
"count" -> count = longAdapter.fromJson(reader)!!
"elements" -> {
doc = sequence {
reader.beginArray()
while (reader.hasNext()) {
yield(osmChargingStationAdapter.fromJson(reader)!!)
}
reader.endArray()
reader.close()
}
break
}
}
}
OSMDocument(timestamp!!, count!!, doc!!)
}
}
}

View File

@@ -0,0 +1,269 @@
package net.vonforst.evmap.api.openstreetmap
import android.database.DatabaseUtils
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.squareup.moshi.Moshi
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.FiltersSQLQuery
import net.vonforst.evmap.api.FullDownloadResult
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.mapPower
import net.vonforst.evmap.api.mapPowerInverse
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter
import net.vonforst.evmap.api.powerSteps
import net.vonforst.evmap.model.BooleanFilter
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.Filter
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.MultipleChoiceFilter
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.model.SliderFilter
import net.vonforst.evmap.model.getBooleanValue
import net.vonforst.evmap.model.getMultipleChoiceValue
import net.vonforst.evmap.model.getSliderValue
import net.vonforst.evmap.viewmodel.Resource
import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.http.GET
import java.io.IOException
import java.time.Duration
interface OpenStreetMapApi {
@GET("charging-stations-osm.json")
suspend fun getAllChargingStations(): Response<OSMDocument>
companion object {
private val moshi = Moshi.Builder()
.add(ZonedDateTimeAdapter())
.add(InstantAdapter())
.build()
fun create(
baseurl: String = "https://osm.ev-map.app/"
): OpenStreetMapApi {
val client = OkHttpClient.Builder().apply {
if (BuildConfig.DEBUG) addDebugInterceptors()
}.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseurl)
.addConverterFactory(OSMConverterFactory(moshi))
.client(client)
.build()
return retrofit.create(OpenStreetMapApi::class.java)
}
}
}
class OpenStreetMapApiWrapper(baseurl: String = "https://osm.ev-map.app/") :
ChargepointApi<OSMReferenceData> {
override val name = "OpenStreetMap"
override val id = "openstreetmap"
override val cacheLimit = Duration.ofDays(300L)
override val supportsOnlineQueries = false
override val supportsFullDownload = true
val api = OpenStreetMapApi.create(baseurl)
override suspend fun getChargepoints(
referenceData: ReferenceData,
bounds: LatLngBounds,
zoom: Float,
useClustering: Boolean,
filters: FilterValues?
): Resource<ChargepointList> {
throw NotImplementedError()
}
override suspend fun getChargepointsRadius(
referenceData: ReferenceData,
location: LatLng,
radius: Int,
zoom: Float,
useClustering: Boolean,
filters: FilterValues?
): Resource<ChargepointList> {
throw NotImplementedError()
}
override suspend fun getChargepointDetail(
referenceData: ReferenceData,
id: Long
): Resource<ChargeLocation> {
throw NotImplementedError()
}
override suspend fun getReferenceData(): Resource<OSMReferenceData> {
throw NotImplementedError()
}
override fun getFilters(
referenceData: ReferenceData,
sp: StringProvider
): List<Filter<FilterValue>> {
val plugs = listOf(
Chargepoint.TYPE_1,
Chargepoint.CCS_TYPE_1,
Chargepoint.TYPE_2_SOCKET,
Chargepoint.TYPE_2_PLUG,
Chargepoint.CCS_TYPE_2,
Chargepoint.CHADEMO,
Chargepoint.SUPERCHARGER,
Chargepoint.CEE_BLAU,
Chargepoint.CEE_ROT,
Chargepoint.SCHUKO
)
val plugMap = plugs.associateWith { plug ->
nameForPlugType(sp, plug)
}
val refData = referenceData as OSMReferenceData
val networkMap = refData.networks.associateWith { it }
return listOf(
BooleanFilter(sp.getString(R.string.filter_free), "freecharging"),
BooleanFilter(sp.getString(R.string.filter_free_parking), "freeparking"),
BooleanFilter(sp.getString(R.string.filter_open_247), "open_247"),
SliderFilter(
sp.getString(R.string.filter_min_power), "min_power",
powerSteps.size - 1,
mapping = ::mapPower,
inverseMapping = ::mapPowerInverse,
unit = "kW"
),
MultipleChoiceFilter(
sp.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(
Chargepoint.TYPE_1,
Chargepoint.TYPE_2_SOCKET,
Chargepoint.TYPE_2_PLUG,
Chargepoint.CCS_TYPE_1,
Chargepoint.CCS_TYPE_2,
Chargepoint.CHADEMO
),
manyChoices = true
),
MultipleChoiceFilter(
sp.getString(R.string.filter_networks), "networks",
networkMap, manyChoices = true
),
SliderFilter(
sp.getString(R.string.filter_min_connectors),
"min_connectors",
10,
min = 1
)
)
}
override fun convertFiltersToSQL(
filters: FilterValues,
referenceData: ReferenceData
): FiltersSQLQuery {
if (filters.isEmpty()) return FiltersSQLQuery("", false, false)
var requiresChargepointQuery = false
val result = StringBuilder()
if (filters.getBooleanValue("freecharging") == true) {
result.append(" AND freecharging IS 1")
}
if (filters.getBooleanValue("freeparking") == true) {
result.append(" AND freeparking IS 1")
}
if (filters.getBooleanValue("open_247") == true) {
result.append(" AND twentyfourSeven IS 1")
}
val minPower = filters.getSliderValue("min_power")
if (minPower != null && minPower > 0) {
result.append(" AND json_extract(cp.value, '$.power') >= ${minPower}")
requiresChargepointQuery = true
}
val connectors = filters.getMultipleChoiceValue("connectors")
if (connectors != null && !connectors.all) {
val connectorsList = if (connectors.values.size == 0) {
""
} else {
connectors.values.joinToString(",") {
DatabaseUtils.sqlEscapeString(it)
}
}
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
requiresChargepointQuery = true
}
val minConnectors = filters.getSliderValue("min_connectors")
if (minConnectors != null && minConnectors > 1) {
result.append(" GROUP BY ChargeLocation.id HAVING SUM(json_extract(cp.value, '$.count')) >= $minConnectors")
requiresChargepointQuery = true
}
val networks = filters.getMultipleChoiceValue("networks")
if (networks != null && !networks.all) {
val networksList = if (networks.values.size == 0) {
""
} else {
networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) }
}
result.append(" AND network IN (${networksList})")
}
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false)
}
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
return true
}
override suspend fun fullDownload(): FullDownloadResult<OSMReferenceData> {
val response = api.getAllChargingStations()
if (!response.isSuccessful) {
throw IOException(response.message())
} else {
val body = response.body()!!
return OSMFullDownloadResult(body)
}
}
}
data class OSMReferenceData(val networks: List<String>) : ReferenceData()
class OSMFullDownloadResult(private val body: OSMDocument) : FullDownloadResult<OSMReferenceData> {
private var downloadProgress = 0f
private var refData: OSMReferenceData? = null
override val chargers: Sequence<ChargeLocation>
get() {
val time = body.timestamp
val networks = mutableListOf<String>()
return sequence {
body.elements.forEachIndexed { i, it ->
val charger = it.convert(time)
yield(charger)
downloadProgress = i.toFloat() / body.count
charger.network?.let { networks.add(it) }
}
refData = OSMReferenceData(networks)
}
}
override val progress: Float
get() = downloadProgress
override val referenceData: OSMReferenceData
get() = refData
?: throw UnsupportedOperationException("referenceData is only available once download is complete")
}

View File

@@ -2,6 +2,7 @@ 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 okhttp3.internal.immutableListOf
import java.time.Instant
@@ -40,6 +41,7 @@ private val SOCKET_TYPES = immutableListOf(
// Tesla
OsmSocket("tesla_standard", null),
OsmSocket("tesla_supercharger", Chargepoint.SUPERCHARGER),
OsmSocket("tesla_supercharger_ccs", Chargepoint.CCS_UNKNOWN),
// CEE
OsmSocket("cee_blue", Chargepoint.CEE_BLAU), // Also known as "caravan socket"
@@ -58,6 +60,12 @@ private val SOCKET_TYPES = immutableListOf(
OsmSocket("sev1011_t25", null),
)
data class OSMDocument(
val timestamp: Instant,
val count: Long,
val elements: Sequence<OSMChargingStation>
)
@JsonClass(generateAdapter = true)
data class OSMChargingStation(
// Unique numeric ID
@@ -87,7 +95,7 @@ data class OSMChargingStation(
"openstreetmap",
getName(),
Coordinate(lat, lon),
null, // TODO: Can we determine this with overpass?
getAddress(),
getChargepoints(),
tags["network"],
"https://www.openstreetmap.org/node/$id",
@@ -99,18 +107,31 @@ data class OSMChargingStation(
tags["description"],
null,
null,
null,
getPhotos(),
null,
getOpeningHours(),
getCost(),
"© OpenStreetMap contributors",
null,
null,
null,
tags["website"],
dataFetchTimestamp,
true,
)
private fun getAddress(): Address? {
val city = tags["addr:city"]
val country = tags["addr:country"]
val postcode = tags["addr:postcode"]
val street = tags["addr:street"]
val housenumber = tags["addr:housenumber"] ?: tags["addr:housename"]
return if (listOf(city, country, postcode, street, housenumber).any { it != null }) {
Address(city, country, postcode, "$street $housenumber")
} else {
null
}
}
/**
* Return the name for this charging station.
*/
@@ -165,7 +186,7 @@ data class OSMChargingStation(
return null
}
private fun getCost(): Cost? {
private fun getCost(): Cost {
val freecharging = when (tags["fee"]?.lowercase()) {
"yes", "y" -> false
"no", "n" -> true
@@ -176,7 +197,28 @@ data class OSMChargingStation(
"yes", "y", "interval" -> false
else -> null
}
return Cost(freecharging, freeparking)
val description = listOfNotNull(tags["charge"], tags["charge:conditional"]).ifEmpty { null }
?.joinToString("\n")
return Cost(freecharging, freeparking, null, description)
}
private fun getPhotos(): List<ChargerPhoto> {
val photos = mutableListOf<ChargerPhoto>()
for (i in -1..9) {
val url = tags["image" + if (i >= 0) ":$i" else ""]
if (url != null) {
if (url.startsWith("https://i.imgur.com")) {
ImgurChargerPhoto.create(url)?.let { photos.add(it) }
}
/*
TODO: Imgur seems to be by far the most common image hoster (650 images),
followed by Mapillary (450, requires an API key to retrieve images)
Other than that, we have Google Photos, Wikimedia Commons (100-150 images each).
And there are some other links to various sites, but not all are valid links pointing directly to a JPEG file...
*/
}
}
return photos
}
companion object {
@@ -201,4 +243,26 @@ data class OSMChargingStation(
return numberString.toDoubleOrNull()
}
}
}
}
@Parcelize
@JsonClass(generateAdapter = true)
class ImgurChargerPhoto(override val id: String) : ChargerPhoto(id) {
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
return if (allowOriginal) {
"https://i.imgur.com/$id.jpg"
} else {
val value = width ?: size ?: height
"https://i.imgur.com/${id}_d.jpg?maxwidth=$value"
}
}
companion object {
private val regex = Regex("https?://i.imgur.com/([\\w\\d]+)(?:_d)?.(?:webp|jpg)")
fun create(url: String): ImgurChargerPhoto? {
val id = regex.find(url)?.groups?.get(1)?.value
return id?.let { ImgurChargerPhoto(it) }
}
}
}

View File

@@ -29,6 +29,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.car2go.maps.model.LatLng
import net.vonforst.evmap.R
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.location.FusionEngine
import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.location.Priority
@@ -45,14 +46,20 @@ interface LocationAwareScreen {
class CarAppService : androidx.car.app.CarAppService() {
private val CHANNEL_ID = "car_location"
private val NOTIFICATION_ID = 1000
private val TAG = "CarAppService"
private var foregroundStarted = false
fun ensureForegroundService() {
// we want to run as a foreground service to make sure we can use location
if (!foregroundStarted) {
createNotificationChannel()
startForeground(NOTIFICATION_ID, getNotification())
foregroundStarted = true
try {
if (!foregroundStarted) {
createNotificationChannel()
startForeground(NOTIFICATION_ID, getNotification())
foregroundStarted = true
Log.i(TAG, "Started foreground service")
}
} catch (e: SecurityException) {
Log.w(TAG, "Failed to start foreground service: ", e)
}
}
@@ -125,8 +132,11 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
}
override fun onCreateScreen(intent: Intent): Screen {
val mapScreen = MapScreen(carContext, this)
val mapScreen = if (supportsNewMapScreen(carContext)) {
MapScreen(carContext, this)
} else {
LegacyMapScreen(carContext, this)
}
val screens = mutableListOf<Screen>(mapScreen)
handleActionsIntent(intent)?.let {
@@ -156,7 +166,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
}
if (!prefs.privacyAccepted) {
screens.add(
AcceptPrivacyScreen(carContext)
AcceptPrivacyScreen(carContext, this)
)
}
handleACRAIntent(intent)?.let {
@@ -186,7 +196,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
val lon = it.getQueryParameter("longitude")?.toDouble()
val name = it.getQueryParameter("name")
if (lat != null && lon != null) {
prefs.placeSearchResultAndroidAuto = LatLng(lat, lon)
prefs.placeSearchResultAndroidAuto = PlaceWithBounds(LatLng(lat, lon), null)
prefs.placeSearchResultAndroidAutoName = name ?: "%.4f,%.4f".format(lat, lon)
return null
} else if (name != null) {

View File

@@ -3,13 +3,22 @@ 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.*
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
@@ -18,7 +27,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
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
@@ -32,7 +50,9 @@ import retrofit2.HttpException
import java.io.IOException
import kotlin.math.roundToInt
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
@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 {
@@ -70,7 +90,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
carContext.stringProvider(),
chargepoint.type
)
} ${chargepoint.formatPower()} ${
} ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
carContext.getString(
R.string.chargeprice_stats,
meta.energy,
@@ -130,7 +150,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
)
).build()
).setOnClickListener {
openUrl(carContext, ChargepriceApi.getPoiUrl(charger))
openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
}.build()
).build()
)

View File

@@ -3,7 +3,6 @@ package net.vonforst.evmap.auto
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
@@ -14,6 +13,7 @@ import android.text.Spanned
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.model.Action
import androidx.car.app.model.ActionStrip
@@ -32,6 +32,7 @@ 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
@@ -49,12 +50,8 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.fronyx.FronyxApi
import net.vonforst.evmap.api.fronyx.PredictionData
import net.vonforst.evmap.api.fronyx.PredictionRepository
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
@@ -64,8 +61,8 @@ import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.formatDMS
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.awaitFinished
import java.time.ZoneId
@@ -74,8 +71,12 @@ import java.time.format.FormatStyle
import kotlin.math.floor
import kotlin.math.roundToInt
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
@ExperimentalCarApi
class ChargerDetailScreen(
ctx: CarContext,
val chargerSparse: ChargeLocation,
val session: EVMapSession
) : Screen(ctx) {
var charger: ChargeLocation? = null
var photo: Bitmap? = null
private var availability: ChargeLocationStatus? = null
@@ -88,6 +89,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val availabilityRepo = AvailabilityRepository(ctx)
private val predictionRepo = PredictionRepository(ctx)
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
@@ -134,7 +136,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
.setFlags(Action.FLAG_PRIMARY)
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener {
navigateToCharger(charger)
navigateToCharger(carContext, session.cas, charger)
}
.build())
if (ChargepriceApi.isChargerSupported(charger)) {
@@ -151,14 +153,20 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
.setTitle(carContext.getString(R.string.auto_prices))
.setOnClickListener {
if (prefs.chargepriceNativeIntegration) {
screenManager.push(ChargepriceScreen(carContext, charger))
screenManager.push(
ChargepriceScreen(
carContext,
session,
charger
)
)
} else {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(ChargepriceApi.getPoiUrl(charger))
)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
carContext.startActivity(intent)
session.cas.startActivity(intent)
}
}
.build())
@@ -177,12 +185,12 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
Action.Builder()
.setTitle(carContext.getString(R.string.open_in_app))
.setOnClickListener(ParkedOnlyOnClickListener.create {
val intent = Intent(carContext, MapsActivity::class.java)
val intent = Intent(session.cas, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_CHARGER_ID, chargerSparse.id)
.putExtra(EXTRA_LAT, chargerSparse.coordinates.lat)
.putExtra(EXTRA_LON, chargerSparse.coordinates.lng)
carContext.startActivity(intent)
session.cas.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
@@ -251,7 +259,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
// Row 1: address + chargepoints
rows.add(Row.Builder().apply {
setTitle(charger.address.toString())
setTitle(charger.address?.toString() ?: charger.coordinates.formatDMS())
if (photo == null) {
// show just the icon
@@ -271,7 +279,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
Row.IMAGE_TYPE_LARGE
)
}
addText(generateChargepointsText(charger))
addText(generateChargepointsText(charger, availability, carContext))
}.build())
if (maxRows <= 3) {
// row 2: operator + cost + fault report
@@ -364,7 +372,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
var text = formatTeslaPricing(teslaPricing, carContext) as CharSequence
formatTeslaParkingFee(teslaPricing, carContext)?.let { text += "\n\n" + it }
addText(text)
} ?: {
} ?: run {
addText(carContext.getString(if (prediction != null) R.string.auto_no_data else R.string.loading))
}
}.build())
@@ -484,47 +492,6 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
return string
}
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
chargepointsText.apply {
if (i > 0) append(" · ")
append("${cp.count}× ")
val plugIcon = iconForPlugType(cp.type)
if (plugIcon != 0) {
append(
nameForPlugType(carContext.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
plugIcon
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
} else {
append(nameForPlugType(carContext.stringProvider(), cp.type))
}
cp.formatPower()?.let {
append(" ")
append(it)
}
}
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
return chargepointsText
}
private fun generateOperatorText(charger: ChargeLocation) =
if (charger.operator != null && charger.network != null) {
if (charger.operator.contains(charger.network)) {
@@ -542,20 +509,6 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
carContext.getString(R.string.unknown_operator)
}
private fun navigateToCharger(charger: ChargeLocation) {
val coord = charger.coordinates
val intent =
Intent(
CarContext.ACTION_NAVIGATE,
Uri.parse("geo:${coord.lat},${coord.lng}")
)
try {
carContext.startCarApp(intent)
} catch (e: UnsupportedOperationException) {
CarToast.makeText(carContext, R.string.no_maps_app_found, CarToast.LENGTH_SHORT).show()
}
}
private fun loadCharger() {
lifecycleScope.launch {
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
@@ -607,12 +560,12 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
)
this@ChargerDetailScreen.photo = outImg
}
fronyxSupported = charger.chargepoints.any {
fronyxSupported = false /*charger.chargepoints.any {
FronyxApi.isChargepointSupported(
charger,
it
)
} && !availabilityRepo.isSupercharger(charger)
} && !availabilityRepo.isSupercharger(charger)*/
teslaSupported = availabilityRepo.isTeslaSupported(charger)
invalidate()

View File

@@ -0,0 +1,249 @@
package net.vonforst.evmap.auto
import android.location.Location
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarIconSpan
import androidx.car.app.model.CarLocation
import androidx.car.app.model.CarText
import androidx.car.app.model.DistanceSpan
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.Metadata
import androidx.car.app.model.Pane
import androidx.car.app.model.Place
import androidx.car.app.model.PlaceMarker
import androidx.car.app.model.Row
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.utils.formatDecimal
import java.time.ZonedDateTime
import kotlin.math.roundToInt
interface ChargerListDelegate : ItemList.OnItemVisibilityChangedListener {
val locationError: Boolean
val loadingError: Boolean
val maxRows: Int
val filterStatus: Long
val location: Location?
val energyLevel: EnergyLevel?
fun onChargerClick(charger: ChargeLocation)
}
@ExperimentalCarApi
class ChargerListFormatter(
val carContext: CarContext,
val screen: ChargerListDelegate,
val cas: CarAppService
) {
private val iconGen = ChargerIconGenerator(carContext, null, height = 96)
var favorites: Set<Long> = emptySet()
fun buildChargerList(
chargers: List<ChargeLocation>?,
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>
): ItemList? {
return if (chargers != null) {
val chargerList = chargers.take(screen.maxRows)
val builder = ItemList.Builder()
// only show the city if not all chargers are in the same city
val showCity = chargerList.map { it.address?.city }.distinct().size > 1
chargerList.forEach { charger ->
builder.addItem(
formatCharger(
charger,
availabilities,
showCity,
charger.id in favorites
)
)
}
builder.setNoItemsMessage(
carContext.getString(
if (screen.filterStatus == FILTERS_FAVORITES) {
R.string.auto_no_favorites_found
} else {
R.string.auto_no_chargers_found
}
)
)
builder.setOnItemsVisibilityChangedListener(screen)
builder.build()
} else {
if (screen.loadingError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.connection_error)
)
builder.build()
} else if (screen.locationError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.location_error)
)
builder.build()
} else {
null
}
}
}
private fun formatCharger(
charger: ChargeLocation,
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>,
showCity: Boolean,
isFavorite: Boolean
): Row {
val markerTint = getMarkerTint(charger)
val backgroundTint = if ((charger.maxPower ?: 0.0) > 100) {
R.color.charger_100kw_dark // slightly darker color for better contrast
} else {
markerTint
}
val color = ContextCompat.getColor(carContext, backgroundTint)
val place =
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
.setMarker(
PlaceMarker.Builder()
.setColor(CarColor.createCustom(color, color))
.build()
)
.build()
val icon = iconGen.getBitmap(
markerTint,
fault = charger.faultReport != null,
multi = charger.isMulti(),
fav = isFavorite
)
val iconSpan =
CarIconSpan.create(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
return Row.Builder().apply {
// only show the city if not all chargers are in the same city (-> showCity == true)
// and the city is not already contained in the charger name
val title = SpannableStringBuilder().apply {
append(" ", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
append(" ")
append(charger.name)
}
if (showCity && charger.address?.city != null && charger.address.city !in charger.name) {
val titleWithCity = SpannableStringBuilder().apply {
append("", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
append(" ")
append("${charger.name} · ${charger.address.city}")
}
setTitle(CarText.Builder(titleWithCity).addVariant(title).build())
} else {
setTitle(title)
}
val text = SpannableStringBuilder()
// distance
screen.location?.let {
val distanceMeters = distanceBetween(
it.latitude, it.longitude,
charger.coordinates.lat, charger.coordinates.lng
)
text.append(
"distance",
DistanceSpan.create(
roundValueToDistance(
distanceMeters,
screen.energyLevel?.distanceDisplayUnit?.value,
carContext
)
),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// power
val power = charger.maxPower
if (power != null) {
if (text.isNotEmpty()) text.append(" · ")
text.append("${power.roundToInt()} kW")
}
// availability
availabilities[charger.id]?.second?.let { av ->
val status = av.status.values.flatten()
val available = availabilityText(status)
val total = charger.chargepoints.sumOf { it.count }
if (text.isNotEmpty()) text.append(" · ")
text.append(
"$available/$total",
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
addText(text)
setMetadata(
Metadata.Builder()
.setPlace(place)
.build()
)
setOnClickListener {
screen.onChargerClick(charger)
}
}.build()
}
fun buildSingleCharger(
charger: ChargeLocation,
availability: ChargeLocationStatus?,
onClick: () -> Unit
) = Pane.Builder().apply {
val icon = iconGen.getBitmap(
getMarkerTint(charger),
fault = charger.faultReport != null,
multi = charger.isMulti(),
fav = charger.id in favorites
)
addRow(Row.Builder().apply {
setImage(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
setTitle(charger.address?.toString() ?: charger.coordinates.formatDecimal())
addText(generateChargepointsText(charger, availability, carContext))
}.build())
addAction(Action.Builder().apply {
setTitle(carContext.getString(R.string.show_more))
setOnClickListener(onClick)
}.build())
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_navigation
)
).build()
)
setTitle(carContext.getString(R.string.navigate))
setBackgroundColor(CarColor.PRIMARY)
setOnClickListener {
navigateToCharger(carContext, cas, charger)
}
}.build())
}.build()
}

View File

@@ -9,14 +9,36 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Pane
import androidx.car.app.model.PaneTemplate
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.model.*
import net.vonforst.evmap.model.BooleanFilter
import net.vonforst.evmap.model.BooleanFilterValue
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.MultipleChoiceFilter
import net.vonforst.evmap.model.MultipleChoiceFilterValue
import net.vonforst.evmap.model.SliderFilter
import net.vonforst.evmap.model.SliderFilterValue
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -232,7 +254,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
),
CarToast.LENGTH_SHORT
).show()
invalidate()
}
}
}.build())
@@ -349,7 +370,6 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
),
CarToast.LENGTH_SHORT
).show()
invalidate()
screenManager.pop()
}
}
@@ -381,7 +401,6 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
}
if (!saveSuccess) return@pushForResult
}
invalidate()
}
.build()
)

View File

@@ -0,0 +1,532 @@
package net.vonforst.evmap.auto
import android.content.pm.PackageManager
import android.location.Location
import android.os.Handler
import android.os.Looper
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarLocation
import androidx.car.app.model.OnContentRefreshListener
import androidx.car.app.model.Place
import androidx.car.app.model.PlaceListMapTemplate
import androidx.car.app.model.PlaceMarker
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterWithValue
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.bearingBetween
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.utils.headingDiff
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.await
import net.vonforst.evmap.viewmodel.awaitFinished
import net.vonforst.evmap.viewmodel.filtersWithValue
import retrofit2.HttpException
import java.io.IOException
import java.time.Duration
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.min
/**
* Main map screen showing either nearby chargers or favorites
*
* Legacy implementation for Car App API level < 7
*/
@androidx.car.app.annotations.ExperimentalCarApi
class LegacyMapScreen(ctx: CarContext, val session: EVMapSession) :
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
ChargerListDelegate, DefaultLifecycleObserver {
private val db = AppDatabase.getInstance(carContext)
private var prefs = PreferenceDataSource(ctx)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val availabilityRepo = AvailabilityRepository(ctx)
private var updateCoroutine: Job? = null
private var availabilityUpdateCoroutine: Job? = null
private var visibleStart: Int? = null
private var visibleEnd: Int? = null
override var location: Location? = null
private var lastDistanceUpdateTime: Instant? = null
private var lastChargersUpdateTime: Instant? = null
private var chargers: List<ChargeLocation>? = null
private val favorites = db.favoritesDao().getAllFavorites()
override var loadingError = false
override var locationError = false
private val searchRadius = 5 // kilometers
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private val chargersUpdateThresholdDistance = 500 // meters
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
HashMap()
override val maxRows =
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
override var filterStatus = prefs.filterStatus
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
private val carInfo: CarInfo by lazy {
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
}
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
override var energyLevel: EnergyLevel? = null
private var heading: Compass? = null
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
listOf(
"android.car.permission.CAR_ENERGY",
"android.car.permission.CAR_ENERGY_PORTS",
"android.car.permission.READ_CAR_DISPLAY_UNITS",
)
} else {
listOf(
"com.google.android.gms.permission.CAR_FUEL"
)
}
private var searchLocation: LatLng? = null
private val formatter = ChargerListFormatter(ctx, this, session.cas)
init {
lifecycle.addObserver(this)
marker = MapScreen.MARKER
}
override fun onGetTemplate(): Template {
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
setTitle(
prefs.placeSearchResultAndroidAutoName?.let {
carContext.getString(R.string.auto_chargers_near_location, it)
} ?: carContext.getString(
if (filterStatus == FILTERS_FAVORITES) {
R.string.auto_favorites
} else {
R.string.auto_chargers_closeby
}
)
)
if (prefs.placeSearchResultAndroidAutoName != null) {
searchLocation?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
if (prefs.placeSearchResultAndroidAutoName != null) {
setMarker(
PlaceMarker.Builder()
.setColor(CarColor.PRIMARY)
.build()
)
}
}.build())
}
} else {
location?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
}
}
formatter.buildChargerList(chargers, availabilities)?.let {
setItemList(it)
} ?: setLoading(true)
setCurrentLocationEnabled(true)
setHeaderAction(Action.APP_ICON)
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
filtersWithValue?.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
}
}
setActionStrip(
ActionStrip.Builder()
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_settings
)
).setTint(CarColor.DEFAULT).build()
)
.setOnClickListener {
screenManager.push(SettingsScreen(carContext, session))
session.mapScreen = null
}
.build())
.addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (prefs.placeSearchResultAndroidAuto != null) {
R.drawable.ic_search_off
} else {
R.drawable.ic_search
}
)
).build()
)
setOnClickListener {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
if (!supportsRefresh) {
screenManager.pushForResult(DummyReturnScreen(carContext)) {
chargers = null
loadChargers()
}
} else {
chargers = null
loadChargers()
}
} else {
screenManager.pushForResult(
PlaceSearchScreen(
carContext,
session
)
) {
chargers = null
loadChargers()
}
session.mapScreen = null
}
}
}.build())
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_filter
)
)
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
.build()
)
.setOnClickListener {
screenManager.push(FilterScreen(carContext, session))
session.mapScreen = null
}
.build())
.build())
if (carContext.carAppApiLevel >= 5 ||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
) {
setOnContentRefreshListener(this@LegacyMapScreen)
}
}.build()
}
override fun onChargerClick(charger: ChargeLocation) {
screenManager.push(ChargerDetailScreen(carContext, charger, session))
session.mapScreen = null
}
override fun updateLocation(location: Location) {
if (location.latitude == this.location?.latitude
&& location.longitude == this.location?.longitude
) {
return
}
val previousLocation = this.location
this.location = location
if (previousLocation == null) {
loadChargers()
return
}
val now = Instant.now()
if (lastDistanceUpdateTime == null ||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
) {
lastDistanceUpdateTime = now
// update displayed distances
invalidate()
}
// if chargers are searched around current location, consider app-driven refresh
val searchLocation =
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
val distance = searchLocation?.let {
distanceBetween(
it.latitude, it.longitude, location.latitude, location.longitude
)
} ?: 0.0
if (supportsRefresh && (lastChargersUpdateTime == null ||
Duration.between(
lastChargersUpdateTime,
now
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
) {
onContentRefreshRequested()
}
}
private fun loadChargers() {
val location = location ?: return
val searchLocation =
prefs.placeSearchResultAndroidAuto?.latLng ?: LatLng.fromLocation(location)
this.searchLocation = searchLocation
updateCoroutine = lifecycleScope.launch {
loadingError = false
try {
filterStatus = prefs.filterStatus
val filterValues =
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
val filters = repo.getFiltersAsync(carContext.stringProvider())
filtersWithValue = filtersWithValue(filters, filterValues)
val apiId = repo.api.value!!.id
// load chargers
if (filterStatus == FILTERS_FAVORITES) {
val chargers = favorites.await().map { it.charger }.sortedBy {
distanceBetween(
location.latitude, location.longitude,
it.coordinates.lat, it.coordinates.lng
)
}
this@LegacyMapScreen.chargers = chargers
} else {
// try multiple search radii until we have enough chargers
var chargers: List<ChargeLocation>? = null
val radiusValues = listOf(searchRadius, searchRadius * 10, searchRadius * 50)
for (radius in radiusValues) {
val response = repo.getChargepointsRadius(
searchLocation,
radius,
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data.isNullOrEmpty() else response.data == null) {
loadingError = true
this@LegacyMapScreen.chargers = null
invalidate()
return@launch
}
chargers = response.data
if (prefs.placeSearchResultAndroidAutoName == null) {
chargers = headingFilter(
chargers,
searchLocation
)
}
if (chargers == null || chargers.size >= maxRows) {
break
}
}
this@LegacyMapScreen.chargers = chargers
}
updateCoroutine = null
lastChargersUpdateTime = Instant.now()
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
loadingError = true
invalidate()
} catch (e: HttpException) {
loadingError = true
invalidate()
}
}
}
/**
* Filters by heading if heading available and enabled
*/
private fun headingFilter(
chargers: List<ChargeLocation>?,
searchLocation: LatLng
): List<ChargeLocation>? {
// use compass heading if available, otherwise fall back to GPS
val location = location
val heading = heading?.orientations?.value?.get(0)
?: if (location?.hasBearing() == true) location.bearing else null
return heading?.let {
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
chargers?.filter {
val bearing = bearingBetween(
searchLocation.latitude,
searchLocation.longitude,
it.coordinates.lat,
it.coordinates.lng
)
val diff = headingDiff(bearing, heading.toDouble())
abs(diff) < 30
}
} ?: chargers
}
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
val isUpdate = this.energyLevel == null
this.energyLevel = energyLevel
if (isUpdate) invalidate()
}
private fun onCompassUpdated(compass: Compass) {
this.heading = compass
}
override fun onStart(owner: LifecycleOwner) {
setupListeners()
session.requestLocationUpdates()
locationError = false
Handler(Looper.getMainLooper()).postDelayed({
if (location == null) {
locationError = true
invalidate()
}
}, 5000)
// Reloading chargers in onStart does not seem to count towards content limit.
// So let's do this so the user gets fresh chargers when re-entering the app.
if (prefs.dataSource != repo.api.value?.id) {
repo.api.value = createApi(prefs.dataSource, carContext)
}
invalidate()
loadChargers()
}
private fun setupListeners() {
val exec = ContextCompat.getMainExecutor(carContext)
if (supportsCarApiLevel3(carContext)) {
carSensors.addCompassListener(
CarSensors.UPDATE_RATE_NORMAL,
exec,
::onCompassUpdated
)
}
if (!permissions.all {
ContextCompat.checkSelfPermission(
carContext,
it
) == PackageManager.PERMISSION_GRANTED
})
return
if (supportsCarApiLevel3(carContext)) {
println("Setting up energy level listener")
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
}
}
override fun onStop(owner: LifecycleOwner) {
// Reloading chargers in onStart does not seem to count towards content limit.
// So let's do this so the user gets fresh chargers when re-entering the app.
// Deleting the data already in onStop makes sure that we show a loading screen directly
// (i.e. onGetTemplate is not called while the old data is still there)
chargers = null
availabilities.clear()
location = null
removeListeners()
}
private fun removeListeners() {
if (supportsCarApiLevel3(carContext)) {
println("Removing energy level listener")
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
carSensors.removeCompassListener(::onCompassUpdated)
}
}
override fun onContentRefreshRequested() {
loadChargers()
availabilities.clear()
val start = visibleStart
val end = visibleEnd
if (start != null && end != null) {
onItemVisibilityChanged(start, end)
}
}
override fun onItemVisibilityChanged(startIndex: Int, endIndex: Int) {
// when the list is scrolled, load corresponding availabilities
if (startIndex == visibleStart && endIndex == visibleEnd && availabilities.isNotEmpty()) return
if (startIndex == -1 || endIndex == -1) return
if (availabilityUpdateCoroutine != null) return
visibleEnd = endIndex
visibleStart = startIndex
// remove outdated availabilities
availabilities = availabilities.filter {
Duration.between(
it.value.first,
ZonedDateTime.now()
) <= availabilityUpdateThreshold
}.toMutableMap()
// update availabilities
availabilityUpdateCoroutine = lifecycleScope.launch {
delay(300L)
val chargers = chargers ?: return@launch
if (chargers.isEmpty()) return@launch
val tasks = chargers.subList(
min(startIndex, chargers.size - 1),
min(endIndex, chargers.size - 1)
).mapNotNull {
// update only if not yet stored
if (!availabilities.containsKey(it.id)) {
lifecycleScope.async {
val availability = availabilityRepo.getAvailability(it).data
val date = ZonedDateTime.now()
availabilities[it.id] = date to availability
}
} else null
}
if (tasks.isNotEmpty()) {
tasks.awaitAll()
invalidate()
}
availabilityUpdateCoroutine = null
}
}
}

View File

@@ -0,0 +1,46 @@
package net.vonforst.evmap.auto
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import com.car2go.maps.AttributionClickListener
import net.vonforst.evmap.R
@ExperimentalCarApi
class MapAttributionScreen(
ctx: CarContext,
val session: EVMapSession,
val attributions: List<AttributionClickListener.Attribution>
) : Screen(ctx) {
override fun onGetTemplate(): Template {
return ListTemplate.Builder()
.setHeader(
Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.maplibre_attributionsDialogTitle))
.build()
)
.setSingleList(ItemList.Builder().apply {
attributions.forEach { attr ->
addItem(
Row.Builder()
.setTitle(attr.title)
.setBrowsable(true)
.setOnClickListener(
ParkedOnlyOnClickListener.create {
openUrl(carContext, session.cas, attr.url)
}).build()
)
}
}.build())
.build()
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,284 @@
package net.vonforst.evmap.auto
import android.animation.ValueAnimator
import android.app.Presentation
import android.content.Context
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.os.Build
import android.os.SystemClock
import android.util.Log
import android.view.MotionEvent
import androidx.car.app.CarContext
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.car.app.annotations.RequiresCarApi
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.lifecycle.LifecycleCoroutineScope
import com.car2go.maps.AnyMap
import com.car2go.maps.AnyMap.CancelableCallback
import com.car2go.maps.CameraUpdate
import com.car2go.maps.MapContainerView
import com.car2go.maps.MapFactory
import com.car2go.maps.OnMapReadyCallback
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
import kotlin.math.hypot
import kotlin.math.roundToInt
import kotlin.math.roundToLong
class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCoroutineScope) :
SurfaceCallback, OnMapReadyCallback {
private val VIRTUAL_DISPLAY_NAME = "evmap_map"
private val VELOCITY_THRESHOLD_IGNORE_FLING = 1000
private val STATUSBAR_OFFSET_SYSTEMS = listOf(
"VolvoCars/ihu_emulator_volvo_car/ihu_emulator:11",
"Google/sdk_gcar_x86_64/generic_64bitonly_x86_64:11"
)
private val prefs = PreferenceDataSource(ctx)
private lateinit var virtualDisplay: VirtualDisplay
lateinit var presentation: Presentation
private lateinit var mapView: MapContainerView
private var width: Int = 0
private var height: Int = 0
private var visibleArea: Rect? = null
private var map: AnyMap? = null
private val mapCallbacks = mutableListOf<OnMapReadyCallback>()
private var flingAnimator: ValueAnimator? = null
private var idle = true
private var idleDelay: Job? = null
var cameraMoveStartedListener: (() -> Unit)? = null
var cameraIdleListener: (() -> Unit)? = null
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
if (surfaceContainer.surface == null || surfaceContainer.dpi == 0 || surfaceContainer.height == 0 || surfaceContainer.width == 0) {
return
}
if (Build.FINGERPRINT.contains("emulator") || Build.FINGERPRINT.contains("sdk_gcar")) {
// fix for MapLibre in Android Automotive Emulators
System.setProperty("ro.kernel.qemu", "1")
}
width = surfaceContainer.width
height = surfaceContainer.height
virtualDisplay = ContextCompat
.getSystemService(ctx, DisplayManager::class.java)!!
.createVirtualDisplay(
VIRTUAL_DISPLAY_NAME,
width,
height,
(surfaceContainer.dpi * when (getMapProvider()) {
"mapbox" -> 1.6
"google" -> 1.0
else -> 1.0
}).roundToInt(),
surfaceContainer.surface,
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
)
presentation = Presentation(ctx, virtualDisplay.display, R.style.AppTheme)
mapView = createMap(presentation.context)
mapView.onCreate(null)
mapView.onResume()
presentation.setContentView(mapView)
presentation.show()
mapView.getMapAsync(this)
}
private fun getMapProvider(): String = if (BuildConfig.FLAVOR_automotive == "automotive") {
// Google Maps SDK is not available on AAOS (not even AAOS with GAS, so far)
"mapbox"
} else prefs.mapProvider
override fun onVisibleAreaChanged(visibleArea: Rect) {
Log.d("MapSurfaceCallback", "visible area: $visibleArea")
this.visibleArea = visibleArea
updateVisibleArea()
}
override fun onStableAreaChanged(stableArea: Rect) {
Log.d("MapSurfaceCallback", "stable area: $stableArea")
}
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
mapView.onPause()
mapView.onStop()
mapView.onDestroy()
map = null
presentation.dismiss()
virtualDisplay.release()
}
@RequiresCarApi(2)
override fun onScroll(distanceX: Float, distanceY: Float) {
flingAnimator?.cancel()
val map = map ?: return
map.moveCamera(map.cameraUpdateFactory.scrollBy(distanceX, distanceY))
dispatchCameraMoveStarted()
}
@RequiresCarApi(2)
override fun onFling(velocityX: Float, velocityY: Float) {
val map = map ?: return
val screenDensity: Float = presentation.resources.displayMetrics.density
// calculate velocity vector for xy dimensions, independent from screen size
val velocityXY =
hypot((velocityX / screenDensity).toDouble(), (velocityY / screenDensity).toDouble())
if (velocityXY < VELOCITY_THRESHOLD_IGNORE_FLING) {
// ignore short flings, these can occur when other gestures just have finished executing
return
}
idleDelay?.cancel()
val offsetX = velocityX / 10
val offsetY = velocityY / 10
val animationTime = (velocityXY / 10).roundToLong()
flingAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = animationTime
interpolator = LinearOutSlowInInterpolator()
var last = 0f
addUpdateListener {
val current = it.animatedFraction
val diff = last - current
map.moveCamera(map.cameraUpdateFactory.scrollBy(diff * offsetX, diff * offsetY))
last = current
}
start()
doOnEnd { dispatchCameraIdle() }
}
}
@RequiresCarApi(2)
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
flingAnimator?.cancel()
val map = map ?: return
if (scaleFactor == 2f) return
val offsetX = (focusX - mapView.width / 2) * (scaleFactor - 1f)
val offsetY = (offsetY(focusY) - mapView.height / 2) * (scaleFactor - 1f)
Log.i("MapSurfaceCallback", "focus: $focusX, $focusY, scaleFactor: $scaleFactor")
map.moveCamera(map.cameraUpdateFactory.zoomBy(scaleFactor - 1))
map.moveCamera(map.cameraUpdateFactory.scrollBy(offsetX, offsetY))
dispatchCameraMoveStarted()
}
fun animateCamera(update: CameraUpdate) {
val map = map ?: return
map.animateCamera(update, object : CancelableCallback {
override fun onFinish() {
dispatchCameraIdle()
}
override fun onCancel() {
}
})
}
private fun dispatchCameraMoveStarted() {
if (idle) {
idle = false
cameraMoveStartedListener?.invoke()
}
idleDelay?.cancel()
idleDelay = lifecycleScope.launch {
delay(500)
dispatchCameraIdle()
}
}
private fun dispatchCameraIdle() {
idle = true
cameraIdleListener?.invoke()
}
@RequiresCarApi(5)
override fun onClick(x: Float, y: Float) {
flingAnimator?.cancel()
val downTime: Long = SystemClock.uptimeMillis()
val eventTime: Long = downTime + 100
val yOffset = offsetY(y)
val downEvent = MotionEvent.obtain(
downTime,
downTime,
MotionEvent.ACTION_DOWN,
x,
yOffset,
0
)
mapView.dispatchTouchEvent(downEvent)
downEvent.recycle()
val upEvent = MotionEvent.obtain(
downTime,
eventTime,
MotionEvent.ACTION_UP,
x,
yOffset,
0
)
mapView.dispatchTouchEvent(upEvent)
upEvent.recycle()
}
private fun offsetY(y: Float): Float {
if (!STATUSBAR_OFFSET_SYSTEMS.any { Build.FINGERPRINT.startsWith(it) }) return y
// In some emulators, touch locations are offset by the status bar height
// 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
}
private fun createMap(ctx: Context): MapContainerView {
val priority = arrayOf(
when (getMapProvider()) {
"mapbox" -> MapFactory.MAPLIBRE
"google" -> MapFactory.GOOGLE
else -> null
},
MapFactory.GOOGLE,
MapFactory.MAPLIBRE
)
return MapFactory.createMap(ctx, priority).view
}
override fun onMapReady(anyMap: AnyMap) {
this.map = anyMap
updateVisibleArea()
mapCallbacks.forEach { it.onMapReady(anyMap) }
mapCallbacks.clear()
}
private fun updateVisibleArea() {
visibleArea?.let {
map?.setPadding(it.left, it.top, width - it.right, height - it.bottom)
}
}
fun getMapAsync(callback: OnMapReadyCallback) {
mapCallbacks.add(callback)
}
}

View File

@@ -1,18 +1,25 @@
package net.vonforst.evmap.auto
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.add
import androidx.fragment.app.commit
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import net.vonforst.evmap.R
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
companion object {
private val resultRegistry: MutableMap<String, MutableSharedFlow<String>> = mutableMapOf()
fun registerForResult(url: String): Flow<String> {
val flow = MutableSharedFlow<String>(replay = 1)
resultRegistry[url] = flow
return flow
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
@@ -22,10 +29,14 @@ class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
}
}
LocalBroadcastManager.getInstance(this).registerReceiver(object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
finish()
val url = intent.getStringExtra(OAuthLoginFragment.EXTRA_URL)!!
supportFragmentManager.setFragmentResultListener(url, this) { _, result ->
val resultUrl = result.getString(OAuthLoginFragment.EXTRA_URL) ?: return@setFragmentResultListener
resultRegistry[url]?.let { flow ->
flow.tryEmit(resultUrl)
resultRegistry.remove(url)
}
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
finish()
}
}
}

View File

@@ -11,19 +11,34 @@ import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.*
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.DistanceSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.Row
import androidx.car.app.model.SearchTemplate
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.iconForPlaceType
import net.vonforst.evmap.adapter.isSpecialPlace
import net.vonforst.evmap.autocomplete.*
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.AutocompletePlace
import net.vonforst.evmap.autocomplete.AutocompleteProvider
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.autocomplete.getAutocompleteProviders
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.storage.RecentAutocompletePlace
@@ -117,7 +132,7 @@ class PlaceSearchScreen(
setOnClickListener {
lifecycleScope.launch {
val placeDetails = getDetails(place.id) ?: return@launch
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
prefs.placeSearchResultAndroidAuto = placeDetails
prefs.placeSearchResultAndroidAutoName =
place.primaryText.toString()
screenManager.popTo(MapScreen.MARKER)

View File

@@ -4,7 +4,14 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.ItemList
import androidx.car.app.model.Row
import androidx.car.app.model.SearchTemplate
import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
@@ -45,7 +52,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
} ?: run {
setLoading(true)
}
if (isMultiSelect) {
if (isMultiSelect && shouldShowSelectAll) {
setActionStrip(ActionStrip.Builder().apply {
addAction(
Action.Builder().setIcon(

View File

@@ -29,9 +29,13 @@ 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 androidx.localbroadcastmanager.content.LocalBroadcastManager
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
@@ -43,6 +47,7 @@ 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
@@ -78,7 +83,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
)
setBrowsable(true)
setOnClickListener {
screenManager.push(DataSettingsScreen(carContext))
screenManager.push(DataSettingsScreen(carContext, session))
}
}.build())
addItem(Row.Builder().apply {
@@ -113,22 +118,25 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
}
.build()
)
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
.setToggle(Toggle.Builder {
prefs.showChargersAheadAndroidAuto = it
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_navigation
)
).setTint(CarColor.DEFAULT).build()
)
.build()
)
if (carContext.carAppApiLevel < 7 || !carContext.isAppDrivenRefreshSupported) {
// this option is only supported in LegacyMapScreen
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
.setToggle(Toggle.Builder {
prefs.showChargersAheadAndroidAuto = it
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_navigation
)
).setTint(CarColor.DEFAULT).build()
)
.build()
)
}
}
addItem(
Row.Builder()
@@ -143,7 +151,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
)
.setBrowsable(true)
.setOnClickListener {
screenManager.push(AboutScreen(carContext))
screenManager.push(AboutScreen(carContext, session))
}
.build()
)
@@ -152,7 +160,8 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
}
}
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
@ExperimentalCarApi
class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), DefaultLifecycleObserver {
val prefs = PreferenceDataSource(ctx)
val encryptedPrefs = EncryptedPreferenceDataStore(ctx)
val db = AppDatabase.getInstance(ctx)
@@ -163,14 +172,22 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
carContext.resources.getStringArray(R.array.pref_search_provider_names)
val searchProviderValues =
carContext.resources.getStringArray(R.array.pref_search_provider_values)
val mapProviderNames =
carContext.resources.getStringArray(R.array.pref_map_provider_names)
val mapProviderValues =
carContext.resources.getStringArray(R.array.pref_map_provider_values)
var teslaLoggingIn = false
init {
lifecycle.addObserver(this)
}
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.settings_data_sources))
setHeaderAction(Action.BACK)
setSingleList(ItemList.Builder().apply {
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_data_source))
setBrowsable(true)
@@ -186,36 +203,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
)
}
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_search_provider))
setBrowsable(true)
val searchProviderId = prefs.searchProvider
val searchProviderDesc =
searchProviderNames[searchProviderValues.indexOf(searchProviderId)]
addText(searchProviderDesc)
setOnClickListener {
screenManager.push(
ChooseDataSourceScreen(
carContext,
ChooseDataSourceScreen.Type.SEARCH_PROVIDER
)
)
}
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_search_delete_recent))
setOnClickListener {
lifecycleScope.launch {
db.recentAutocompletePlaceDao().deleteAll()
CarToast.makeText(
carContext,
R.string.deleted_recent_search_results,
CarToast.LENGTH_SHORT
).show()
}
}
}.build())
addItem(
/*addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.pref_prediction_enabled))
.addText(carContext.getString(R.string.pref_prediction_enabled_summary))
@@ -223,7 +211,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
prefs.predictionEnabled = it
}.setChecked(prefs.predictionEnabled).build())
.build()
)
)*/
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_tesla_account))
addText(
@@ -248,10 +236,104 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
})
}
}.build())
}.build())
}.build(), carContext.getString(R.string.settings_charger_data)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_search_provider))
setBrowsable(true)
val searchProviderId = prefs.searchProvider
val searchProviderDesc =
searchProviderNames[searchProviderValues.indexOf(searchProviderId)]
addText(searchProviderDesc)
setOnClickListener {
screenManager.push(
ChooseDataSourceScreen(
carContext,
ChooseDataSourceScreen.Type.SEARCH_PROVIDER
)
)
}
}.build())
if (supportsNewMapScreen(carContext) && BuildConfig.FLAVOR_automotive != "automotive") {
// Google Maps SDK is not available on AAOS (not even AAOS with GAS, so far)
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_map_provider))
setBrowsable(true)
val mapProviderId = prefs.mapProvider
val mapProviderDesc =
mapProviderNames[mapProviderValues.indexOf(mapProviderId)]
addText(mapProviderDesc)
setOnClickListener {
screenManager.push(
ChooseDataSourceScreen(
carContext,
ChooseDataSourceScreen.Type.MAP_PROVIDER
)
)
}
}.build())
}
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_search_delete_recent))
setOnClickListener {
lifecycleScope.launch {
db.recentAutocompletePlaceDao().deleteAll()
CarToast.makeText(
carContext,
R.string.deleted_recent_search_results,
CarToast.LENGTH_SHORT
).show()
}
}
}.build())
}.build(), carContext.getString(R.string.settings_map)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.settings_cache_count))
cacheCount?.let { count ->
cacheSize?.let { size ->
val sizeMb = size.toFloat() / 1024 / 1024
addText(
carContext.getString(
R.string.settings_cache_count_summary,
count,
sizeMb
)
)
}
}
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.settings_cache_clear))
addText(carContext.getString(R.string.settings_cache_clear_summary))
setOnClickListener {
lifecycleScope.launch {
db.savedRegionDao().deleteAll()
db.chargeLocationsDao().deleteAllIfNotFavorite()
loadCacheSize()
}
}
}.build())
}.build(), carContext.getString(R.string.settings_caching)))
}.build()
}
var cacheCount: Long? = null
var cacheSize: Long? = null
private suspend fun loadCacheSize() {
cacheCount = db.chargeLocationsDao().getCountAsync()
cacheSize = db.chargeLocationsDao().getSize()
invalidate()
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
lifecycleScope.launch {
loadCacheSize()
}
}
private fun teslaLogin() {
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
@@ -260,25 +342,20 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
val args = OAuthLoginFragmentArgs(
uri.toString(),
TeslaAuthenticationApi.resultUrlPrefix,
"#000000"
"#FFFFFF"
).toBundle()
val intent = Intent(carContext, OAuthLoginActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtras(args)
LocalBroadcastManager.getInstance(carContext)
.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
val url = IntentCompat.getParcelableExtra(
intent,
OAuthLoginFragment.EXTRA_URL,
Uri::class.java
)
teslaGetAccessToken(url!!, codeVerifier)
}
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
val resultFlow = OAuthLoginActivity.registerForResult(uri.toString())
lifecycleScope.launch {
resultFlow.collect { resultUrl ->
teslaGetAccessToken(resultUrl.toUri(), codeVerifier)
}
}
carContext.startActivity(intent)
session.cas.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
CarToast.makeText(
@@ -340,32 +417,42 @@ class ChooseDataSourceScreen(
@StringRes val extraDesc: Int? = null
) : Screen(ctx) {
enum class Type {
CHARGER_DATA_SOURCE, SEARCH_PROVIDER
CHARGER_DATA_SOURCE, SEARCH_PROVIDER, MAP_PROVIDER
}
val prefs = PreferenceDataSource(carContext)
val title = when (type) {
Type.CHARGER_DATA_SOURCE -> R.string.pref_data_source
Type.SEARCH_PROVIDER -> R.string.pref_search_provider
Type.MAP_PROVIDER -> R.string.pref_map_provider
}
val names = when (type) {
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_names)
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_names)
}
val values = when (type) {
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_values)
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_values)
}
val names = carContext.resources.getStringArray(
when (type) {
Type.CHARGER_DATA_SOURCE -> R.array.pref_data_source_names
Type.SEARCH_PROVIDER -> R.array.pref_search_provider_names
Type.MAP_PROVIDER -> R.array.pref_map_provider_names
}
)
val values = carContext.resources.getStringArray(
when (type) {
Type.CHARGER_DATA_SOURCE -> R.array.pref_data_source_values
Type.SEARCH_PROVIDER -> R.array.pref_search_provider_values
Type.MAP_PROVIDER -> R.array.pref_map_provider_values
}
)
val currentValue: String = when (type) {
Type.CHARGER_DATA_SOURCE -> prefs.dataSource
Type.SEARCH_PROVIDER -> prefs.searchProvider
Type.MAP_PROVIDER -> prefs.mapProvider
}
val descriptions = when (type) {
Type.CHARGER_DATA_SOURCE -> listOf(
carContext.getString(R.string.data_source_goingelectric_desc),
carContext.getString(R.string.data_source_openchargemap_desc)
carContext.getString(R.string.data_source_openchargemap_desc),
carContext.getString(R.string.data_source_openstreetmap_desc)
)
Type.SEARCH_PROVIDER -> null
Type.MAP_PROVIDER -> null
}
val callback: (String) -> Unit = when (type) {
Type.CHARGER_DATA_SOURCE -> { it ->
@@ -375,6 +462,9 @@ class ChooseDataSourceScreen(
Type.SEARCH_PROVIDER -> { it ->
prefs.searchProvider = it
}
Type.MAP_PROVIDER -> { it ->
prefs.mapProvider = it
}
}
override fun onGetTemplate(): Template {
@@ -486,10 +576,9 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_currency))
val names =
carContext.resources.getStringArray(R.array.pref_chargeprice_currency_names)
val values =
carContext.resources.getStringArray(R.array.pref_chargeprice_currency_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 "")
@@ -629,8 +718,8 @@ class SelectCurrencyScreen(ctx: CarContext) : MultiSelectSearchScreen<Pair<Strin
override fun getLabel(it: Pair<String, String>): String = it.first
override suspend fun loadData(): List<Pair<String, String>> {
val names = carContext.resources.getStringArray(R.array.pref_chargeprice_currency_names)
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currency_values)
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
val names = values.map(::currencyDisplayName)
return names.zip(values)
}
}
@@ -752,7 +841,8 @@ class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
}
}
class AboutScreen(ctx: CarContext) : Screen(ctx) {
@ExperimentalCarApi
class AboutScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
var developerOptionsCounter = 0
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
@@ -797,7 +887,11 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setTitle(carContext.getString(R.string.faq))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.faq_link))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.faq_link)
)
}).build()
)
addItem(
@@ -808,12 +902,16 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setOnClickListener(ParkedOnlyOnClickListener.create {
if (BuildConfig.FLAVOR_automotive == "automotive") {
// we can't open the donation page on the phone in this case
openUrl(carContext, carContext.getString(R.string.donate_link))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.donate_link)
)
} else {
val intent = Intent(carContext, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_DONATE, true)
carContext.startActivity(intent)
session.cas.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
@@ -825,39 +923,75 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
}.build(), carContext.getString(R.string.about)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.twitter))
.addText(carContext.getString(R.string.twitter_handle))
.setTitle(carContext.getString(R.string.mastodon))
.addText(carContext.getString(R.string.mastodon_handle))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.twitter_url))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.mastodon_url)
)
}).build()
)
if (maxRows > 8) {
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.twitter))
.addText(carContext.getString(R.string.twitter_handle))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(
carContext,
session.cas,
carContext.getString(R.string.twitter_url)
)
}).build()
)
}
if (maxRows > 6) {
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.goingelectric_forum))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(
carContext,
carContext, session.cas,
carContext.getString(R.string.goingelectric_forum_url)
)
}).build()
)
}
if (maxRows > 7) {
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.tff_forum))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(
carContext, session.cas,
carContext.getString(R.string.tff_forum_url)
)
}).build()
)
}
}.build(), carContext.getString(R.string.contact)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.github_link_title))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.github_link))
openUrl(carContext, session.cas, carContext.getString(R.string.github_link))
}).build()
)
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.privacy))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.privacy_link)
)
}).build()
)
}.build(), carContext.getString(R.string.other)))
@@ -865,7 +999,8 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
}
}
class AcceptPrivacyScreen(ctx: CarContext) : Screen(ctx) {
@ExperimentalCarApi
class AcceptPrivacyScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template {
val textWithoutLink = HtmlCompat.fromHtml(
@@ -886,7 +1021,7 @@ class AcceptPrivacyScreen(ctx: CarContext) : Screen(ctx) {
addAction(Action.Builder()
.setTitle(carContext.getString(R.string.privacy))
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link))
openUrl(carContext, session.cas, carContext.getString(R.string.privacy_link))
}).build()
)
}.build()

View File

@@ -4,31 +4,46 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextPaint
import android.util.Log
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.HostException
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarIconSpan
import androidx.car.app.model.Distance
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.ftPerMile
import net.vonforst.evmap.getPackageInfoCompat
import net.vonforst.evmap.kmPerMile
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.shouldUseImperialUnits
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ydPerMile
import java.util.Locale
import kotlin.math.roundToInt
@@ -201,7 +216,7 @@ fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): Li
fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0)
return info.versionName.split(".")
return info.versionName!!.split(".")
}
fun supportsCarApiLevel3(ctx: CarContext): Boolean {
@@ -221,13 +236,17 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
return true
}
fun openUrl(carContext: CarContext, url: String) {
fun supportsNewMapScreen(ctx: CarContext) =
ctx.carAppApiLevel >= 7 && ctx.isAppDrivenRefreshSupported
@ExperimentalCarApi
fun openUrl(carContext: CarContext, cas: CarAppService, url: String) {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(
ContextCompat.getColor(
carContext,
cas,
R.color.colorPrimary
)
)
@@ -237,7 +256,7 @@ fun openUrl(carContext: CarContext, url: String) {
intent.data = Uri.parse(url)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
carContext.startActivity(intent)
cas.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
// only show the toast "opened on phone" if we're running on a phone
CarToast.makeText(
@@ -255,6 +274,64 @@ fun openUrl(carContext: CarContext, url: String) {
}
}
@ExperimentalCarApi
fun navigateToCharger(ctx: CarContext, cas: CarAppService, charger: ChargeLocation) {
var success = navigateCarApp(ctx, charger)
if (!success && BuildConfig.FLAVOR_automotive == "automotive") {
// on AAOS, some OEMs' navigation apps might not support
success = navigateRegularApp(ctx, cas, charger)
}
if (!success) {
CarToast.makeText(ctx, R.string.no_maps_app_found, CarToast.LENGTH_SHORT).show()
}
}
private fun navigateCarApp(ctx: CarContext, charger: ChargeLocation): Boolean {
val coord = charger.coordinates
val intent =
Intent(
CarContext.ACTION_NAVIGATE,
Uri.parse("geo:${coord.lat},${coord.lng}")
)
try {
ctx.startCarApp(intent)
return true
} catch (e: HostException) {
Log.w("navigateToCharger", "Could not start navigation using car app intent")
Log.w("navigateToCharger", intent.toString())
e.printStackTrace()
} catch (e: SecurityException) {
Log.w("navigateToCharger", "Could not start navigation using car app intent")
Log.w("navigateToCharger", intent.toString())
e.printStackTrace()
}
return false
}
@ExperimentalCarApi
private fun navigateRegularApp(
ctx: CarContext,
cas: CarAppService,
charger: ChargeLocation
): Boolean {
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(
"geo:${coord.lat},${coord.lng}?q=${coord.lat},${coord.lng}(${
Uri.encode(charger.name)
})"
)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (intent.resolveActivity(ctx.packageManager) != null) {
cas.startActivity(intent)
return true
} else {
Log.w("navigateToCharger", "Could not start navigation using regular intent")
Log.w("navigateToCharger", intent.toString())
}
return false
}
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
/*
Dummy screen to get around template refresh limitations.
@@ -279,4 +356,49 @@ class TextMeasurer(ctx: CarContext) {
fun measureText(text: CharSequence): Float {
return textPaint.measureText(text, 0, text.length)
}
}
fun generateChargepointsText(
charger: ChargeLocation,
availability: ChargeLocationStatus?,
ctx: Context
): SpannableStringBuilder {
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
chargepointsText.apply {
if (i > 0) append(" · ")
append("${cp.count}× ")
val plugIcon = iconForPlugType(cp.type)
if (plugIcon != 0) {
append(
nameForPlugType(ctx.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
ctx,
plugIcon
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
} else {
append(nameForPlugType(ctx.stringProvider(), cp.type))
}
cp.formatPower(ctx.currentOrDefaultLocale)?.let {
append(" ")
append(it)
}
}
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
return chargepointsText
}

View File

@@ -16,6 +16,7 @@ import com.mapbox.api.geocoding.v5.models.CarmenFeature
import com.mapbox.geojson.BoundingBox
import com.mapbox.geojson.Point
import net.vonforst.evmap.R
import retrofit2.HttpException
import java.io.IOException
class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
@@ -25,7 +26,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
override val id = "mapbox"
override fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace> {
val result = MapboxGeocoding.builder().apply {
val request = MapboxGeocoding.builder().apply {
location?.let {
proximity(Point.fromLngLat(location.longitude, location.latitude))
}
@@ -33,7 +34,12 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
accessToken(context.getString(R.string.mapbox_key))
autocomplete(true)
this.query(query)
}.build().executeCall()
}
val result = try {
request.build().executeCall()
} catch (e: HttpException) {
throw IOException(e)
}
if (!result.isSuccessful) {
throw IOException(result.message())
}

View File

@@ -6,6 +6,9 @@ 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
@@ -100,14 +103,19 @@ class ChargepriceFragment : Fragment() {
inflater,
R.layout.fragment_chargeprice_header, container, false
)
binding.lifecycleOwner = this
binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm
headerBinding.lifecycleOwner = this
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
}
@@ -141,7 +149,7 @@ class ChargepriceFragment : Fragment() {
val chargepriceAdapter = ChargepriceAdapter().apply {
onClickListener = {
(requireActivity() as MapsActivity).openUrl(it.url)
(requireActivity() as MapsActivity).openUrl(it.url, binding.root)
}
}
val joinedAdapter = ConcatAdapter(
@@ -194,7 +202,10 @@ class ChargepriceFragment : Fragment() {
}
binding.imgChargepriceLogo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(ChargepriceApi.getPoiUrl(charger))
(requireActivity() as MapsActivity).openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
}
binding.btnSettings.setOnClickListener {
@@ -213,11 +224,19 @@ class ChargepriceFragment : Fragment() {
}
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))
(activity as? MapsActivity)?.openUrl(
getString(R.string.chargeprice_faq_link),
binding.root
)
true
}
else -> false

View File

@@ -14,14 +14,14 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.databinding.DialogConnectorDetailsBinding
import net.vonforst.evmap.databinding.DialogConnectorDetailsHeaderBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
class ConnectorDetailsDialog(
val binding: DialogConnectorDetailsBinding,
binding: DialogConnectorDetailsBinding,
context: Context,
onClose: () -> Unit
) {
private val headerBinding: DialogConnectorDetailsHeaderBinding
private var headerBinding_: DialogConnectorDetailsHeaderBinding? = null
private val headerBinding get() = headerBinding_!!
private val detailsAdapter = ConnectorDetailsAdapter()
init {
@@ -30,7 +30,7 @@ class ConnectorDetailsDialog(
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
headerBinding = DataBindingUtil.inflate(
headerBinding_ = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.dialog_connector_details_header, binding.list, false
)
@@ -60,4 +60,8 @@ class ConnectorDetailsDialog(
headerBinding.divider.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
headerBinding.item = ConnectorAdapter.ChargepointWithAvailability(cp, cpStatus)
}
fun onDestroy() {
headerBinding_ = null
}
}

View File

@@ -54,6 +54,7 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
when (prefs.dataSource) {
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
"openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true
}
}
@@ -65,6 +66,8 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
"goingelectric"
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
"openchargemap"
} else if (binding.rgDataSource.rbOpenStreetMap.isChecked) {
"openstreetmap"
} else {
return@setOnClickListener
}

View File

@@ -1,29 +1,34 @@
package net.vonforst.evmap.fragment
import android.content.Intent
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
abstract class DonateFragmentBase : Fragment() {
fun setupReferrals(referrals: FragmentDonateReferralBinding) {
referrals.referralTesla.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.tesla_referral_link))
referrals.referralWebView.loadUrl(getString(R.string.referral_link))
referrals.referralWebView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
Intent(Intent.ACTION_VIEW, request.url).apply {
startActivity(this)
}
return true
}
}
referrals.referralJuicify.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.juicify_referral_link))
}
referrals.referralGeldfuereauto.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.geldfuereauto_referral_link))
}
referrals.referralMaingau.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.maingau_referral_link))
}
referrals.referralEwieeinfach.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.ewieeinfach_referral_link))
}
referrals.referralEprimo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.eprimo_referral_link))
ViewCompat.setOnApplyWindowInsetsListener(referrals.root) { v, insets ->
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
WindowInsetsCompat.CONSUMED
}
}
}

View File

@@ -7,6 +7,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
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
@@ -65,9 +68,16 @@ class FavoritesFragment : Fragment() {
inflater,
R.layout.fragment_favorites, container, false
)
binding.lifecycleOwner = this
binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm
ViewCompat.setOnApplyWindowInsetsListener(
binding.favsList
) { v, insets ->
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
WindowInsetsCompat.CONSUMED
}
return binding.root
}

View File

@@ -9,6 +9,9 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
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
@@ -45,10 +48,17 @@ class FilterFragment : Fragment(), MenuProvider {
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false)
binding.lifecycleOwner = this
binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm
vm.filterProfile.observe(viewLifecycleOwner) {}
ViewCompat.setOnApplyWindowInsetsListener(
binding.filtersList
) { v, insets ->
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
WindowInsetsCompat.CONSUMED
}
return binding.root
}

Some files were not shown because too many files have changed in this diff Show More