Compare commits

...

124 Commits

Author SHA1 Message Date
johan12345
b99e2ea2c8 WIP: replace CustomBottomSheetBehavior 2025-07-12 18:06:51 +02:00
johan12345
d2ae3733d1 Big toolchain update
- Gradle + AGP
- Java 17
- compile/targetSdk 35
- Room & Moshi use KSP, not KAPT
2025-07-12 18:06:27 +02:00
johan12345
72845da4b5 Release 1.9.18 2025-06-14 17:40:09 +02:00
johan12345
51b57433a8 TeslaAvailabilityDetector: Fix nullability bug 2025-06-14 17:36:47 +02:00
johan12345
3202f821d1 always show current location on start, even if we were not following the location before 2025-06-14 17:32:24 +02:00
johan12345
b7e1ff09db FilterScreen: remove unnecessary invalidate() calls
we are already observing filterProfiles
2025-06-13 22:13:05 +02:00
Licaon_Kter
feabf49b8d Remove some non-determinism 2025-05-30 12:03:49 +02:00
johan12345
dcbe4c6325 Release 1.9.17 2025-05-29 00:41:28 +02:00
johan12345
dcff74c125 update TeslaOwnerApi 2025-05-28 23:58:01 +02:00
johan12345
d8f7d77a36 Release 1.9.16 2025-05-17 19:41:36 +02:00
johan12345
d03cf70499 capture (but ignore) clicks on searchResultMarker 2025-05-17 19:36:22 +02:00
johan12345
7a6bebd143 RTL and Arabic locale fixes 2025-05-17 19:24:39 +02:00
johan12345
66d68ca68e disable extendBounds if map is zoomed out far 2025-05-17 18:04:50 +02:00
johan12345
772885a8eb add "zoom in to see all charging stations" snackbar if response is not complete 2025-05-17 17:53:15 +02:00
johan12345
6b07ce012a Rework showLocation function
avoid opening within EVMap itself
2025-05-15 23:15:51 +02:00
johan12345
29dbc202d8 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

Cherry-picked from 17efe71
2025-05-15 22:53:19 +02:00
johan12345
cf8371d095 allow Unicode license 2025-05-07 23:38:47 +02:00
johan12345
01cb551cbc 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:28:54 +02:00
johan12345
45fe297616 Update car app library
fixes #375 for permission request

https://developer.android.com/training/cars/apps#permissions

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 22:35:02 +02:00
johan12345
32cabefe7d 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:35:38 +02:00
johan12345
9ff8329171 Release 1.9.15 2025-03-29 12:15:44 +01:00
johan12345
e9b70a2f00 fix bug in extendBounds
introduced in 890af2ddef
fixes #373
2025-03-29 12:15:33 +01:00
johan12345
c4c3aba7c7 Release 1.9.14 2025-03-15 15:48:53 +01:00
johan12345
890af2ddef try to better handle situations where map bounds cross the Antimeridian 2025-03-12 22:04:12 +01:00
johan12345
ba0b36b3ec update AnyMaps 2025-03-12 21:11:32 +01:00
johan12345
161b48789f Chargeprice: reset to default charging range when tapping title 2025-03-09 23:07:37 +01:00
johan12345
042b983aa3 CI: run checksec on release APKs 2025-03-04 22:11:32 +01:00
johan12345
1c21da7be0 CI: move apikeys-ci.xml to _ci folder 2025-03-04 21:24:19 +01:00
johan12345
405baed0f7 upgrade android-spatialite 2025-03-04 21:24:05 +01:00
johan12345
19c0d57f2b upgrade maplibre 2025-03-04 20:21:19 +01:00
johan12345
42c2a2f72a improve setLinkify BindingAdapter
fixes #371
2025-02-26 21:21:12 +01:00
johan12345
36ee3ff231 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:23 +01:00
johan12345
883735ef05 FusionEngine: change log level 2025-02-26 20:00:22 +01:00
johan12345
4c68356ae9 Release 1.9.13 2025-02-18 21:56:07 +01:00
johan12345
7fde5b50aa fix disappearing markers
fixes #368
2025-02-18 21:49:55 +01:00
johan12345
7c4136c66d Release 1.9.12 2025-02-07 22:08:41 +01:00
johan12345
6e56f5c3ff update social links 2025-02-07 22:05:23 +01:00
johan12345
017be6f31a increase heap space 2025-02-07 22:02:11 +01:00
johan12345
b398a5dc81 embed referral links as webpage instead of native Android buttons 2025-02-07 19:13:58 +01:00
johan12345
3fb0dec868 Release 1.9.11 (only for Harman Ignite) 2025-01-28 20:58:23 +01:00
johan12345
8c4de115ec remove setting for fronyx predictions in AAOS app
see also e7c9432191
2025-01-28 20:51:05 +01:00
johan12345
334b68cf5e SearchSelectScreen: fix displaying selectAll buttons 2025-01-27 23:40:37 +01:00
johan12345
788c68c9dd Onboarding Android Auto page: fix icon disappearing 2025-01-18 23:43:14 +01:00
johan12345
7842a15529 fix more possible memory leak issues 2025-01-07 20:42:22 +01:00
johan12345
e7c9432191 Disable fronyx predictions
API has been broken for >4 months now
2024-12-03 22:18:47 +01:00
johan12345
76b6abd3ca Release 1.9.10 (only for Faurecia Aptoide) 2024-11-24 18:32:51 +01:00
johan12345
752c184146 fix crash on first start 2024-11-24 18:13:39 +01:00
johan12345
5471ac5073 Release 1.9.9 (only for Faurecia Aptoide) 2024-11-13 20:31:33 +01:00
johan12345
69ae13a199 fix crash on first start 2024-11-13 20:30:58 +01:00
johan12345
8a2e2d9a25 release 1.9.8 (only for Faurecia Aptoide) 2024-11-09 13:56:06 +01:00
johan12345
fe69a78b94 fix possible memory leak issues 2024-11-08 20:50:06 +01:00
johan12345
2663bd7964 update MapLibre & AnyMaps 2024-10-26 22:30:25 +02:00
johan12345
3b54b2799f Databinding: use viewLifecycleOwner instead of Fragment as lifecycle owner 2024-10-26 22:25:08 +02:00
johan12345
3a24711626 translations: move nb-rNO to nb 2024-10-25 22:03:52 +02:00
johan12345
c158744bc2 fix unnecessary recreation of MapFragment 2024-10-23 22:51:15 +02:00
johan12345
c01033a036 upgrade AnyMaps 2024-10-23 22:07:33 +02:00
Hosted Weblate
16474c3864 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:55:26 +02:00
Hosted Weblate
7ce2f8d452 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:55:26 +02:00
Hosted Weblate
28df158d94 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:43:40 +02:00
johan12345
90b3645a0b fix crash when trying to rename a filter profile to a name that already exists 2024-10-15 20:20:27 +02:00
johan12345
de901aa825 Release 1.9.7 (only for Faurecia Aptoide) 2024-08-16 19:13:30 +02:00
johan12345
2ce61f2f6b AAOS: more navigateToCharger fixes 2024-08-16 19:12:34 +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
johan12345
1d63e37467 Release 1.9.2 2024-05-29 21:25:04 +02:00
johan12345
b5b0254bdd Coil: disable hardware acceleration for gallery images
https://github.com/coil-kt/coil/issues/159
2024-05-29 21:19:19 +02:00
johan12345
6514197920 upgrade dependencies 2024-05-29 21:06:30 +02:00
johan12345
4c5388350f upgrade car app library to stable 1.4.0 2024-05-29 20:33:32 +02:00
johan12345
20e9e43f0d upgrade coil library 2024-05-29 20:20:55 +02:00
johan12345
541646dda9 fix missing catch HttpException 2024-05-29 20:16:53 +02:00
johan12345
b9354e77a9 AAOS: fix crash when no navigation app is installed 2024-05-29 20:09:31 +02:00
johan12345
65fa54ef36 CarInfoWrapper: fix NPE 2024-05-29 19:57:29 +02:00
johan12345
6e419849b1 update sponsors 2024-05-25 16:38:33 +02:00
johan12345
c4c6f09a05 update sponsors 2024-05-25 16:23:24 +02:00
johan12345
3b602c03c4 update sponsors 2024-05-25 16:21:39 +02:00
Hosted Weblate
012e5d7362 Translated using Weblate (Portuguese)
Currently translated at 100.0% (367 of 367 strings)

Co-authored-by: Asmodeus <colligare1Asmodeum@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2024-05-25 13:17:28 +02:00
johan12345
4f5007ca0d fix czech string CDATA 2024-05-25 13:17:24 +02:00
Hosted Weblate
b40a74aa37 Translated using Weblate (Czech)
Currently translated at 100.0% (367 of 367 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (366 of 366 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-05-25 13:16:40 +02:00
johan12345
ce172bd4b0 Release 1.9.1 2024-05-25 13:07:22 +02:00
johan12345
b3bbca576f revert to MapLibre 10.x
fixes #345
2024-05-25 12:59:34 +02:00
johan12345
72ccd99c1f re-release 1.9.0 for Play Store 2024-05-24 23:09:37 +02:00
johan12345
dd5c8659df fix declaration of car app category in manifest 2024-05-24 23:08:13 +02:00
johan12345
8f7f9e5a09 re-release 1.9.0 for Play Store
Car App Library alpha version is not accepted
2024-05-21 23:38:33 +02:00
johan12345
8256981b8d workaround for car app library crash with invalid speed units 2024-05-21 23:38:33 +02:00
johan12345
5649ef202f fix Android Auto version check 2024-05-21 23:38:33 +02:00
johan12345
ed9c729684 Revert "update car app library to 1.7.0-alpha02"
This reverts commit d540faa179.
2024-05-21 22:14:55 +02:00
johan12345
add66811ae Export licenses to CSV
needed for Faurecia Aptoide store
2024-05-20 23:10:26 +02:00
johan12345
2df5710910 Release 1.9.0 2024-05-20 22:04:09 +02:00
johan12345
a513a8d6d4 fix source sets for releaseAutomotivePackageName build type 2024-05-20 22:04:09 +02:00
johan12345
a55e4df62d hide Tesla referral link
referral program has ended
2024-05-20 21:08:18 +02:00
johan12345
d540faa179 update car app library to 1.7.0-alpha02 2024-05-20 20:39:29 +02:00
johan12345
3d69d3e50c update AGP 2024-05-20 00:18:39 +02:00
johan12345
fce27f0c19 add SVG for Play Store icon 2024-05-20 00:18:07 +02:00
johan12345
da3b9643bc add build type with different package name for fossAutomotive
needed for Faurecia Aptoide
2024-05-20 00:16:21 +02:00
johan12345
b690d9744d update Play Store icon according to new specs
https://developer.android.com/distribute/google-play/resources/icon-design-specifications
2024-05-20 00:16:21 +02:00
johan12345
70387ec350 fix docs in README 2024-05-19 17:55:47 +02:00
johan12345
5a331df232 add Jawg Maps sponsor logo 2024-05-19 17:53:20 +02:00
johan12345
5dc5e1f43f Migrate Mapbox -> MapLibre
Use Jawg Maps for basemap, ArcGIS for satellite maps

fixes #141
refs #169, #197

hide traffic checkbox if traffic is not supported by map
2024-05-19 17:53:20 +02:00
156 changed files with 3233 additions and 1537 deletions

View File

@@ -30,6 +30,8 @@ jobs:
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }} OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }} CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }} MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
JAWG_API_KEY: ${{ secrets.JAWG_API_KEY }}
ARCGIS_API_KEY: ${{ secrets.ARCGIS_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }} FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }} ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}

View File

@@ -26,7 +26,7 @@ jobs:
cache: 'gradle' cache: 'gradle'
- name: Copy apikeys.xml - 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 - name: Build app
run: ./gradlew assemble${{ matrix.buildvariant }}Debug --no-daemon run: ./gradlew assemble${{ matrix.buildvariant }}Debug --no-daemon
@@ -34,3 +34,55 @@ jobs:
run: ./gradlew test${{ matrix.buildvariant }}DebugUnitTest --no-daemon run: ./gradlew test${{ matrix.buildvariant }}DebugUnitTest --no-daemon
- name: Run Android Lint - name: Run Android Lint
run: ./gradlew lint${{ matrix.buildvariant }}Debug --no-daemon 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

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ apikeys.xml
/app/**/*.apk /app/**/*.apk
/_img/connectors/*.ai /_img/connectors/*.ai
api-7125266970515251116-798419-8e2dda660c80.json api-7125266970515251116-798419-8e2dda660c80.json
output-metadata.json output-metadata.json
licenses_*.csv

View File

@@ -24,7 +24,8 @@ Features
- Android Auto & Android Automotive OS integration - Android Auto & Android Automotive OS integration
- No ads, fully open source - No ads, fully open source
- Compatible with Android 5.0 and above - Compatible with Android 5.0 and above
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox. - Can use Google Maps or OpenStreetMap as map backends - the version available on F-Droid only uses
OSM.
Screenshots Screenshots
----------- -----------
@@ -42,12 +43,13 @@ EVMap uses and put them into the app in the form of a resource file called `apik
`app/src/main/res/values`. You can find more information on which API keys are necessary for which `app/src/main/res/values`. You can find more information on which API keys are necessary for which
features and how they can be obtained in our [documentation page](doc/api_keys.md). features and how they can be obtained in our [documentation page](doc/api_keys.md).
There are three different build flavors, `googleNormal`, `fossNormal` and `googleAutomotive`. There are four different build flavors, `googleNormal`, `fossNormal`, `googleAutomotive`, and
- The `foss` variants only use Mapbox data and should run on most Android devices, even without `fossAutomotive`.
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 - `fossNormal` is intended to run on smartphones and tablets, and also includes the Android
Auto app for use on the car display (however for that to work, the Android Auto app is Auto app for use on the car display (however Android Auto may not work if the app is not
necessary, which in turn does require Google Play Services). installed from Google Play, see https://github.com/ev-map/EVMap/issues/319).
- `fossAutomotive` can be installed directly on - `fossAutomotive` can be installed directly on
[Android Automotive OS (AAOS)](https://source.android.com/docs/automotive/start/what_automotive) [Android Automotive OS (AAOS)](https://source.android.com/docs/automotive/start/what_automotive)
headunits without Google services. headunits without Google services.
@@ -75,5 +77,19 @@ You can use our [Weblate page](https://hosted.weblate.org/projects/evmap/) to he
into new languages. into new languages.
<a href="https://hosted.weblate.org/engage/evmap/"> <a href="https://hosted.weblate.org/engage/evmap/">
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="500" alt="Translation status" /> <img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="400" alt="Translation status" />
</a> </a>
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>
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>
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.

View File

@@ -1,6 +1,8 @@
<resources> <resources>
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">ci</string> <string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">ci</string>
<string name="mapbox_key" translatable="false">ci</string> <string name="mapbox_key" translatable="false">ci</string>
<string name="jawg_key" translatable="false">ci</string>
<string name="arcgis_key" translatable="false">ci</string>
<string name="goingelectric_key" translatable="false">ci</string> <string name="goingelectric_key" translatable="false">ci</string>
<string name="chargeprice_key" translatable="false">ci</string> <string name="chargeprice_key" translatable="false">ci</string>
<string name="openchargemap_key" translatable="false">ci</string> <string name="openchargemap_key" translatable="false">ci</string>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<style>
.cls-1 {
fill: #00e676;
}
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8 {
stroke-width: 0px;
}
.cls-2 {
fill: rgba(255, 255, 255, .2);
}
.cls-3 {
fill: #ffb300;
}
.cls-4 {
fill: #000;
isolation: isolate;
opacity: .45;
}
.cls-5 {
fill: #fff;
}
.cls-6 {
fill: #90a4ae;
}
.cls-7 {
fill: #546e7a;
}
.cls-8 {
fill: rgba(62, 39, 35, .2);
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<rect class="cls-5" width="512" height="512" />
<g>
<g>
<path class="cls-3"
d="M159.42,338.98l-6.43-56.15-9.81,1.01,6.43,56.15,9.81-1.01ZM194.26,334.92l-6.43-56.15-9.81,1.01,6.43,56.15,9.81-1.01Z" />
<path class="cls-6"
d="M212.53,411.37c-3.04,3.72-5.41,6.09-5.75,6.43-8.79,7.1-15.9,9.13-21.65,6.43-10.15-5.07-9.47-24.02-9.13-26.05l7.1.34c-.34,5.41.68,16.91,5.41,19.28,2.71,1.35,7.44-.34,13.53-5.41h0s19.62-19.62,15.56-35.18c-4.74-18.6,16.91-45.33,24.02-54.46l1.01-1.01,5.75,4.4-1.01,1.35c-21.99,27.06-24.35,40.93-22.66,48.03,3.38,13.53-5.75,28.08-12.18,35.85Z" />
<path class="cls-6"
d="M137.78,338.3l2.71,23,21.31,14.21,28.75-3.04,17.59-18.6-2.71-23-67.65,7.44Z" />
<path class="cls-7"
d="M190.21,372.47l-28.75,3.04,6.09,25.37,22.66-2.71v-25.71h0ZM210.84,311.58l2.37,20.97-82.53,9.47-2.37-20.97,82.53-9.47Z" />
</g>
<g>
<g>
<path class="cls-1"
d="M275.45,80.22c-59.19,0-107.23,48.03-107.23,107.23,0,80.84,90.31,123.12,101.14,238.47.34,3.38,3.04,5.75,6.43,5.75s6.09-2.37,6.43-5.75c10.82-115.34,101.14-157.63,101.14-238.47-.68-59.53-48.71-107.23-107.9-107.23Z" />
<path class="cls-2"
d="M275.45,82.58c58.86,0,106.55,47.36,107.23,105.87v-1.01c0-59.19-48.03-107.23-107.23-107.23s-107.23,47.69-107.23,107.23v1.01c.68-58.52,48.37-105.87,107.23-105.87h0Z" />
<path class="cls-8"
d="M281.87,423.21c-.34,3.38-3.04,5.75-6.43,5.75s-6.09-2.37-6.43-5.75c-10.49-115.01-100.12-157.29-100.8-237.12v1.69c0,80.84,90.31,123.12,101.14,238.47.34,3.38,3.04,5.75,6.43,5.75s6.09-2.37,6.43-5.75c10.82-115.34,101.14-157.63,101.14-238.47v-1.69c-1.35,79.83-90.99,122.11-101.48,237.12h0Z" />
</g>
<path class="cls-4"
d="M250.75,135.01v64.94h17.59v53.11l41.27-71.03h-23.68l23.68-47.36c.34.34-58.86.34-58.86.34Z" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,23 @@
import subprocess
import json
build_types = ["fossNormalRelease", "fossAutomotiveRelease"]
for build_type in build_types:
result = subprocess.run(["gradlew.bat", f"generateLibraryDefinitions{build_type.capitalize()}"],
capture_output=True)
data = json.load(
open(f"app/build/generated/aboutLibraries/{build_type}/res/raw/aboutlibraries.json"))
with open(f"licenses_{build_type}.csv", "w") as f:
f.write("component_name;license_title;license_url;public_repository;copyrights\n")
for lib in data["libraries"]:
license = data["licenses"][lib["licenses"][0]] if len(lib["licenses"]) > 0 else None
license_name = license["name"] if license is not None else " "
license_url = license["url"] if license is not None else " "
copyrights = ", ".join([dev["name"] for dev in lib["developers"] if "name" in dev])
if copyrights == "":
copyrights = " "
repo_url = lib['scm']['url'] if 'scm' in lib else ''
f.write(f"{lib['name']};{license_name};{license_url};\"{copyrights}\";{repo_url}\n")

View File

@@ -6,28 +6,37 @@ plugins {
id("kotlin-android") id("kotlin-android")
id("kotlin-parcelize") id("kotlin-parcelize")
id("kotlin-kapt") id("kotlin-kapt")
id("com.google.devtools.ksp").version("2.0.21-1.0.28")
id("androidx.navigation.safeargs.kotlin") id("androidx.navigation.safeargs.kotlin")
id("com.mikepenz.aboutlibraries.plugin") id("com.mikepenz.aboutlibraries.plugin")
} }
android { android {
useLibrary("android.car")
defaultConfig { defaultConfig {
applicationId = "net.vonforst.evmap" applicationId = "net.vonforst.evmap"
compileSdk = 34 compileSdk = 35
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 35
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1 // NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 212 versionCode = 256
versionName = "1.8.2" versionName = "1.9.18"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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 { signingConfigs {
create("release") { create("release") {
val isRunningOnCI = System.getenv("CI") == "true" if (isRunningOnCI && isCIKeystoreAvailable) {
if (isRunningOnCI) {
// configure keystore // configure keystore
storeFile = file("../_ci/keystore.jks") storeFile = file("../_ci/keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD") storePassword = System.getenv("KEYSTORE_PASSWORD")
@@ -44,7 +53,16 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "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
initWith(getByName("release"))
applicationIdSuffix = ".automotive"
} }
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
@@ -52,6 +70,10 @@ android {
} }
} }
sourceSets {
getByName("releaseAutomotivePackageName").setRoot("src/release")
}
flavorDimensions += listOf("dependencies", "automotive") flavorDimensions += listOf("dependencies", "automotive")
productFlavors { productFlavors {
create("foss") { create("foss") {
@@ -74,18 +96,12 @@ android {
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_17.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
} }
buildFeatures { buildFeatures {
@@ -146,6 +162,28 @@ android {
if (mapboxKey != null) { if (mapboxKey != null) {
resValue("string", "mapbox_key", mapboxKey) resValue("string", "mapbox_key", mapboxKey)
} }
var jawgKey =
System.getenv("JAWG_API_KEY") ?: project.findProperty("JAWG_API_KEY")?.toString()
if (jawgKey == null && project.hasProperty("JAWG_API_KEY_ENCRYPTED")) {
jawgKey = decode(
project.findProperty("JAWG_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (jawgKey != null) {
resValue("string", "jawg_key", jawgKey)
}
var arcgisKey =
System.getenv("ARCGIS_API_KEY") ?: project.findProperty("ARCGIS_API_KEY")?.toString()
if (arcgisKey == null && project.hasProperty("ARCGIS_API_KEY_ENCRYPTED")) {
arcgisKey = decode(
project.findProperty("ARCGIS_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (arcgisKey != null) {
resValue("string", "arcgis_key", jawgKey)
}
var chargepriceKey = var chargepriceKey =
System.getenv("CHARGEPRICE_API_KEY") ?: project.findProperty("CHARGEPRICE_API_KEY") System.getenv("CHARGEPRICE_API_KEY") ?: project.findProperty("CHARGEPRICE_API_KEY")
?.toString() ?.toString()
@@ -196,6 +234,22 @@ android {
} }
} }
androidComponents {
beforeVariants { variantBuilder ->
if (variantBuilder.buildType == "releaseAutomotivePackageName"
&& !variantBuilder.productFlavors.containsAll(
listOf(
"automotive" to "automotive",
"dependencies" to "foss"
)
)
) {
// releaseAutomotivePackageName type is only needed for fossAutomotive
variantBuilder.enable = false
}
}
}
configurations { configurations {
create("googleNormalImplementation") {} create("googleNormalImplementation") {}
create("googleAutomotiveImplementation") {} create("googleAutomotiveImplementation") {}
@@ -203,11 +257,16 @@ configurations {
aboutLibraries { aboutLibraries {
allowedLicenses = arrayOf( allowedLicenses = arrayOf(
"Apache-2.0", "mit", "BSD-2-Clause", "Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
"asdkl", // Android SDK "asdkl", // Android SDK
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL "Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
"Google Maps Platform Terms of Service" // Google Maps SDK "Google Maps Platform Terms of Service", // Google Maps SDK
"provided without support or warranty", // org.json
"Unicode/ICU License", "Unicode-3.0", // icu4j
"Bouncy Castle Licence", // bcprov
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
) )
excludeFields = arrayOf("generated")
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
} }
@@ -222,29 +281,29 @@ dependencies {
val testGoogleImplementation by configurations val testGoogleImplementation by configurations
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.activity:activity-ktx:1.8.2") implementation("androidx.activity:activity-ktx:1.9.0")
implementation("androidx.fragment:fragment-ktx:1.6.2") implementation("androidx.fragment:fragment-ktx:1.7.1")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.11.0") implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.browser:browser:1.7.0") implementation("androidx.browser:browser:1.8.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.viewpager2:viewpager2:1.1.0")
implementation("androidx.security:security-crypto:1.1.0-alpha06") implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0") implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.11.0") implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0") implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("com.squareup.moshi:moshi-adapters:1.15.0") implementation("com.squareup.moshi:moshi-adapters:1.15.2")
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0") implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
implementation("io.coil-kt:coil:2.4.0") implementation("io.coil-kt:coil:2.6.0")
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392") implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion") implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion") implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
@@ -253,39 +312,29 @@ dependencies {
implementation("com.google.guava:guava:29.0-android") implementation("com.google.guava:guava:29.0-android")
implementation("com.github.pengrad:mapscaleview:1.6.0") implementation("com.github.pengrad:mapscaleview:1.6.0")
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5") implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
implementation("com.github.erfansn:locale-config-x:1.0.1")
// Android Auto // Android Auto
val carAppVersion = "1.4.0-rc02" val carAppVersion = "1.7.0-rc01"
implementation("androidx.car.app:app:$carAppVersion") implementation("androidx.car.app:app:$carAppVersion")
normalImplementation("androidx.car.app:app-projected:$carAppVersion") normalImplementation("androidx.car.app:app-projected:$carAppVersion")
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion") automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
// AnyMaps // AnyMaps
val anyMapsVersion = "4854581f72" val anyMapsVersion = "a3290b148d"
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion") implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$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.0.0")
implementation("com.github.ev-map.AnyMaps:anymaps-mapbox:$anyMapsVersion") { implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-accounts") // duplicates classes from mapbox-sdk-services
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-telemetry") exclude("org.maplibre.gl", "android-sdk-geojson")
exclude(group = "com.google.android.gms", module = "play-services-location")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-core")
} }
// original version of mapbox-android-core implementation("org.maplibre.gl:android-sdk:10.3.4") {
googleImplementation("com.mapbox.mapboxsdk:mapbox-android-core:2.0.1") exclude("org.maplibre.gl", "android-sdk-geojson")
// patched version that removes build-time dependency on GMS (-> no Google location services)
fossImplementation("com.github.ev-map:mapbox-events-android:a21c324501")
implementation("com.mapbox.mapboxsdk:mapbox-android-sdk") {
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-accounts")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-telemetry")
version {
strictly("9.1.0-SNAPSHOT")
}
} }
// Google Places // Google Places
googleImplementation("com.google.android.libraries.places:places:3.3.0") 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.7.3")
// Mapbox Geocoding // Mapbox Geocoding
@@ -296,22 +345,24 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:$navVersion") implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
// viewmodel library // viewmodel library
val lifecycle_version = "2.6.2" val lifecycle_version = "2.8.1"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version") implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// room library // room library
val room_version = "2.6.1" val room_version = "2.7.1"
implementation("androidx.room:room-runtime:$room_version") implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version") ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version") implementation("androidx.room:room-ktx:$room_version")
implementation("com.github.anboralabs:spatia-room:0.2.9") { implementation("com.github.anboralabs:spatia-room:0.3.0") {
exclude(group = "com.github.dalgarins", module = "android-spatialite") 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
// https://github.com/dalgarins/android-spatialite/pull/10
implementation("com.github.ev-map:android-spatialite:31495dcd81")
// billing library // billing library
val billing_version = "6.1.0" val billing_version = "7.0.0"
googleImplementation("com.android.billingclient:billing:$billing_version") googleImplementation("com.android.billingclient:billing:$billing_version")
googleImplementation("com.android.billingclient:billing-ktx:$billing_version") googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
@@ -325,10 +376,12 @@ dependencies {
debugImplementation("com.facebook.flipper:flipper:0.238.0") debugImplementation("com.facebook.flipper:flipper:0.238.0")
debugImplementation("com.facebook.soloader:soloader:0.10.5") debugImplementation("com.facebook.soloader:soloader:0.10.5")
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0") 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 // testing
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
//noinspection GradleDependency //noinspection GradleDependency
testImplementation("org.json:json:20080701") testImplementation("org.json:json:20080701")
testImplementation("org.robolectric:robolectric:4.11.1") testImplementation("org.robolectric:robolectric:4.11.1")
@@ -341,7 +394,7 @@ dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.arch.core:core-testing:2.2.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.0.4")
} }

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,171 @@
import android.car.Car
import android.car.VehiclePropertyIds
import android.car.VehicleUnit
import android.car.hardware.CarPropertyValue
import android.car.hardware.property.CarPropertyManager
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback
import androidx.annotation.OptIn
import androidx.car.app.CarContext
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.hardware.info.EnergyProfile
import androidx.car.app.hardware.info.EvStatus
import androidx.car.app.hardware.info.Mileage
import androidx.car.app.hardware.info.Model
import androidx.car.app.hardware.info.Speed
import androidx.car.app.hardware.info.TollCard
import java.util.concurrent.Executor
val CarContext.patchedCarInfo: CarInfo
get() = CarInfoWrapper(this)
class CarInfoWrapper(ctx: CarContext) : CarInfo {
private val wrapped =
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
private val carPropertyManager = try {
val car = Car.createCar(ctx)
car.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
} catch (e: NoClassDefFoundError) {
null
}
private val callbacks = mutableMapOf<OnCarDataAvailableListener<*>, CarPropertyEventCallback>()
override fun fetchModel(executor: Executor, listener: OnCarDataAvailableListener<Model>) =
wrapped.fetchModel(executor, listener)
override fun fetchEnergyProfile(
executor: Executor,
listener: OnCarDataAvailableListener<EnergyProfile>
) = wrapped.fetchEnergyProfile(executor, listener)
override fun addTollListener(
executor: Executor,
listener: OnCarDataAvailableListener<TollCard>
) = wrapped.addTollListener(executor, listener)
override fun removeTollListener(listener: OnCarDataAvailableListener<TollCard>) =
wrapped.removeTollListener(listener)
override fun addEnergyLevelListener(
executor: Executor,
listener: OnCarDataAvailableListener<EnergyLevel>
) = wrapped.addEnergyLevelListener(executor, listener)
override fun removeEnergyLevelListener(listener: OnCarDataAvailableListener<EnergyLevel>) =
wrapped.removeEnergyLevelListener(listener)
override fun addSpeedListener(executor: Executor, listener: OnCarDataAvailableListener<Speed>) {
// TODO: This is a emporary workaround until Car App Library 1.7.0 is released - previous versions would crash if the car reported an invalid speed display unit
carPropertyManager ?: return
val callback = object : CarPropertyEventCallback {
private var speedRaw: CarPropertyValue<Float>? = null
private var speedDisplay: CarPropertyValue<Float>? = null
private var speedUnit: CarPropertyValue<Int>? = null
override fun onChangeEvent(value: CarPropertyValue<*>?) {
when (value?.propertyId) {
VehiclePropertyIds.PERF_VEHICLE_SPEED -> speedRaw =
value as CarPropertyValue<Float>?
VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY -> speedDisplay =
value as CarPropertyValue<Float>?
VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS -> speedUnit =
value as CarPropertyValue<Int>?
}
executor.execute {
listener.onCarDataAvailable(Speed.Builder().apply {
speedRaw?.let {
setRawSpeedMetersPerSecond(
CarValue(
it.value,
it.timestamp,
if (it.value != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
)
)
}
speedDisplay?.let {
setDisplaySpeedMetersPerSecond(
CarValue(
it.value,
it.timestamp,
if (it.value != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
)
)
}
speedUnit?.let {
val unit = when (it.value) {
VehicleUnit.METER_PER_SEC -> CarUnit.METERS_PER_SEC
VehicleUnit.MILES_PER_HOUR -> CarUnit.MILES_PER_HOUR
VehicleUnit.KILOMETERS_PER_HOUR -> CarUnit.KILOMETERS_PER_HOUR
else -> null
}
setSpeedDisplayUnit(
CarValue(
unit,
it.timestamp,
if (unit != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
)
)
}
}.build())
}
}
override fun onErrorEvent(propertyId: Int, areaId: Int) {
listener.onCarDataAvailable(
Speed.Builder()
.setRawSpeedMetersPerSecond(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
.setDisplaySpeedMetersPerSecond(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
.setSpeedDisplayUnit(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
.build()
)
}
}
carPropertyManager.registerCallback(
callback,
VehiclePropertyIds.PERF_VEHICLE_SPEED,
CarPropertyManager.SENSOR_RATE_NORMAL
)
carPropertyManager.registerCallback(
callback,
VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY,
CarPropertyManager.SENSOR_RATE_NORMAL
)
carPropertyManager.registerCallback(
callback,
VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS,
CarPropertyManager.SENSOR_RATE_NORMAL
)
}
override fun removeSpeedListener(listener: OnCarDataAvailableListener<Speed>) {
val callback = callbacks[listener] ?: return
carPropertyManager?.unregisterCallback(callback)
}
override fun addMileageListener(
executor: Executor,
listener: OnCarDataAvailableListener<Mileage>
) = wrapped.addMileageListener(executor, listener)
override fun removeMileageListener(listener: OnCarDataAvailableListener<Mileage>) =
wrapped.removeMileageListener(listener)
@OptIn(ExperimentalCarApi::class)
override fun addEvStatusListener(
executor: Executor,
listener: OnCarDataAvailableListener<EvStatus>
) = wrapped.addEvStatusListener(executor, listener)
@OptIn(ExperimentalCarApi::class)
override fun removeEvStatusListener(listener: OnCarDataAvailableListener<EvStatus>) =
wrapped.removeEvStatusListener(listener)
}

View File

@@ -11,6 +11,7 @@ import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import timber.log.Timber
private val networkFlipperPlugin = NetworkFlipperPlugin() private val networkFlipperPlugin = NetworkFlipperPlugin()
@@ -24,6 +25,8 @@ fun addDebugInterceptors(context: Context) {
client.addPlugin(DatabasesFlipperPlugin(context)) client.addPlugin(DatabasesFlipperPlugin(context))
client.addPlugin(SharedPreferencesFlipperPlugin(context)) client.addPlugin(SharedPreferencesFlipperPlugin(context))
client.start() client.start()
Timber.plant(Timber.DebugTree())
} }
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder { fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {

View File

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

View File

@@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
@@ -43,7 +42,7 @@ class DonateFragment : DonateFragmentBase() {
) )
binding.btnDonate.setOnClickListener { 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) setupReferrals(referrals)

View File

@@ -2,5 +2,5 @@
<resources> <resources>
<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="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="donate_paypal">Přispět pomocí PayPalu</string>
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap (Mapbox).</string> <string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
</resources> </resources>

View File

@@ -2,5 +2,5 @@
<resources> <resources>
<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="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="donate_paypal">Mit PayPal spenden</string>
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap (Mapbox).</string> <string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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 (Mapbox).</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> <string name="donate_paypal">Faire un don avec PayPal</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="donate_paypal">Doner med PayPal</string> <string name="donate_paypal">Doner med PayPal</string>
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap (Mapbox).</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> <string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
</resources> </resources>

View File

@@ -2,5 +2,5 @@
<resources> <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="donate_paypal">Doneer via PayPal</string>
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap (Mapbox).</string> <string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap (Mapbox).</string> <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="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> <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> </resources>

View File

@@ -2,5 +2,5 @@
<resources> <resources>
<string name="donations_info" formatted="false">Crezi ca EVMap este util? Sprijina dezvoltarea printr-o donatie pentru dezvoltator.</string> <string name="donations_info" formatted="false">Crezi ca EVMap este util? Sprijina dezvoltarea printr-o donatie pentru dezvoltator.</string>
<string name="donate_paypal">Doneaza cu PayPal</string> <string name="donate_paypal">Doneaza cu PayPal</string>
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap (Mapbox).</string> <string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap.</string>
</resources> </resources>

View File

@@ -2,5 +2,5 @@
<resources> <resources>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string> <string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
<string name="donate_paypal">Donate with PayPal</string> <string name="donate_paypal">Donate with PayPal</string>
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap (Mapbox).</string> <string name="data_sources_hint">Map data in the app is provided by OpenStreetMap.</string>
</resources> </resources>

View File

@@ -3,5 +3,5 @@
<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. <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 \n
\nGoogle si z každého daru strhne 15 %.</string> \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 (Mapbox).</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

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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="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 (Mapbox) für die Kartendaten wechseln.</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

@@ -3,5 +3,5 @@
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur. <string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.
\n \n
\nGoogle prend 15% sur chaque don.</string> \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 (Mapbox) pour les données cartographiques.</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

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

View File

@@ -3,5 +3,5 @@
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar. <string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
\n \n
\nGoogle houdt 15% in van elke donatie.</string> \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 (Mapbox) voor de kaartgegevens.</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

@@ -3,5 +3,5 @@
<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 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 \n
\nA Google cobra 15% de cada doação.</string> \nA Google cobra 15% de cada doação.</string>
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap (Mapbox) nas definições da app.</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

@@ -2,7 +2,7 @@
<resources> <resources>
<string-array name="pref_map_provider_names"> <string-array name="pref_map_provider_names">
<item>@string/pref_provider_google_maps</item> <item>@string/pref_provider_google_maps</item>
<item>@string/pref_provider_osm_mapbox</item> <item>@string/pref_provider_osm</item>
</string-array> </string-array>
<string-array name="pref_map_provider_values" translatable="false"> <string-array name="pref_map_provider_values" translatable="false">
<item>google</item> <item>google</item>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string> <string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string>
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap (Mapbox) for the map data.</string> <string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap for the map data.</string>
</resources> </resources>

View File

@@ -24,6 +24,10 @@
<intent> <intent>
<action android:name="android.support.customtabs.action.CustomTabsService" /> <action android:name="android.support.customtabs.action.CustomTabsService" />
</intent> </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.projection.gearhead" />
<package android:name="com.google.android.apps.automotive.templates.host" /> <package android:name="com.google.android.apps.automotive.templates.host" />
@@ -41,12 +45,21 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:enableOnBackInvokedCallback="true">
<meta-data <meta-data
android:name="com.mapbox.ACCESS_TOKEN" android:name="com.mapbox.ACCESS_TOKEN"
android:value="@string/mapbox_key" /> android:value="@string/mapbox_key" />
<meta-data
android:name="io.jawg.ACCESS_TOKEN"
android:value="@string/jawg_key" />
<meta-data
android:name="com.arcgis.ACCESS_TOKEN"
android:value="@string/arcgis_key" />
<activity <activity
android:name=".MapsActivity" android:name=".MapsActivity"
android:label="@string/app_name" android:label="@string/app_name"
@@ -339,9 +352,8 @@
android:exported="true" android:exported="true"
android:foregroundServiceType="location"> android:foregroundServiceType="location">
<intent-filter> <intent-filter>
<action <action android:name="androidx.car.app.CarAppService" />
android:name="androidx.car.app.CarAppService" <category android:name="androidx.car.app.category.POI" />
android:category="androidx.car.app.category.POI" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View File

@@ -3,7 +3,11 @@ package net.vonforst.evmap
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.os.Build import android.os.Build
import androidx.work.* import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import net.vonforst.evmap.storage.CleanupCacheWorker import net.vonforst.evmap.storage.CleanupCacheWorker
import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateAppLocale import net.vonforst.evmap.ui.updateAppLocale
@@ -24,7 +28,7 @@ class EvMapApplication : Application(), Configuration.Provider {
// Convert to new AppCompat storage for app language // Convert to new AppCompat storage for app language
val lang = prefs.language val lang = prefs.language
if (lang != null && lang !in listOf("", "default")) { if (lang != null) {
updateAppLocale(lang) updateAppLocale(lang)
prefs.language = null prefs.language = null
} }

View File

@@ -3,6 +3,8 @@ package net.vonforst.evmap
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -11,7 +13,6 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen import androidx.core.splashscreen.SplashScreen
@@ -44,14 +45,11 @@ const val EXTRA_DONATE = "donate"
class MapsActivity : AppCompatActivity(), class MapsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
interface FragmentCallback {
fun getRootView(): View
}
private var reenterState: Bundle? = null private var reenterState: Bundle? = null
private lateinit var navController: NavController private lateinit var navController: NavController
private lateinit var navHostFragment: NavHostFragment
lateinit var appBarConfiguration: AppBarConfiguration lateinit var appBarConfiguration: AppBarConfiguration
var fragmentCallback: FragmentCallback? = null
private lateinit var prefs: PreferenceDataSource private lateinit var prefs: PreferenceDataSource
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -60,6 +58,7 @@ class MapsActivity : AppCompatActivity(),
setContentView(R.layout.activity_maps) setContentView(R.layout.activity_maps)
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
appBarConfiguration = AppBarConfiguration( appBarConfiguration = AppBarConfiguration(
setOf( setOf(
R.id.map, R.id.map,
@@ -67,9 +66,9 @@ class MapsActivity : AppCompatActivity(),
R.id.about, R.id.about,
R.id.settings R.id.settings
), ),
findViewById<DrawerLayout>(R.id.drawer_layout) drawerLayout
) )
val navHostFragment = navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController navController = navHostFragment.navController
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph) val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
@@ -87,6 +86,17 @@ class MapsActivity : AppCompatActivity(),
checkPlayServices(this) 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 (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// wait for splash screen animation to finish on first start // wait for splash screen animation to finish on first start
@@ -104,137 +114,128 @@ class MapsActivity : AppCompatActivity(),
} }
}) })
} }
navGraph.setStartDestination(R.id.onboarding) } else if (intent?.scheme == "geo") {
navController.graph = navGraph val query = intent.data?.query?.split("=")?.get(1)
return val coords = getLocationFromIntent(intent)
} 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
if (intent?.scheme == "geo") { if (coords != null) {
val query = intent.data?.query?.split("=")?.get(1) val lat = coords[0]
val coords = getLocationFromIntent(intent) val lon = coords[1]
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)) {
deepLink = navController.createDeepLink() deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map) .setDestination(R.id.map)
.setArguments( .setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.createPendingIntent() .createPendingIntent()
} else if (intent.hasExtra(EXTRA_FAVORITES)) { } else if (!query.isNullOrEmpty()) {
deepLink = navController.createDeepLink() deepLink = navController.createDeepLink()
.setGraph(navGraph) .setGraph(R.navigation.nav_graph)
.setDestination(R.id.favs) .setDestination(R.id.map)
.createPendingIntent() .setArguments(MapFragmentArgs(locationName = query).toBundle())
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.createPendingIntent() .createPendingIntent()
} }
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
deepLink?.send() 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 // google maps navigation
val coord = charger.coordinates val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
@@ -244,11 +245,11 @@ class MapsActivity : AppCompatActivity(),
startActivity(intent) startActivity(intent)
} else { } else {
// fallback: generic geo intent // fallback: generic geo intent
showLocation(charger) showLocation(charger, rootView)
} }
} }
fun showLocation(charger: ChargeLocation) { fun showLocation(charger: ChargeLocation, rootView: View) {
val coord = charger.coordinates val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse( intent.data = Uri.parse(
@@ -256,20 +257,33 @@ class MapsActivity : AppCompatActivity(),
Uri.encode(charger.name) 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) startActivity(intent)
} else { } catch (e: ActivityNotFoundException) {
val cb = fragmentCallback ?: return
Snackbar.make( Snackbar.make(
cb.getRootView(), rootView,
R.string.no_maps_app_found, R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT Snackbar.LENGTH_SHORT
).show() ).show()
} }
} }
fun openUrl(url: String, preferBrowser: Boolean = true) { fun openUrl(url: String, rootView: View, preferBrowser: Boolean = true) {
val pkg = CustomTabsClient.getPackageName(this, null)
val intent = CustomTabsIntent.Builder() val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams( .setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder() CustomTabColorSchemeParams.Builder()
@@ -277,17 +291,49 @@ class MapsActivity : AppCompatActivity(),
.build() .build()
) )
.build() .build()
pkg?.let {
// prefer to open URL in custom tab, even if native app val uri = Uri.parse(url)
// available (such as EVMap itself) val viewIntent = Intent(Intent.ACTION_VIEW, uri)
if (preferBrowser) intent.intent.setPackage(pkg) 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 { try {
intent.launchUrl(this, Uri.parse(url)) intent.launchUrl(this, uri)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
val cb = fragmentCallback ?: return
Snackbar.make( Snackbar.make(
cb.getRootView(), rootView,
R.string.no_browser_app_found, R.string.no_browser_app_found,
Snackbar.LENGTH_SHORT Snackbar.LENGTH_SHORT
).show() ).show()

View File

@@ -16,7 +16,10 @@ import android.text.SpannableStringBuilder
import android.text.SpannedString import android.text.SpannedString
import android.text.TextUtils import android.text.TextUtils
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.View
import android.view.ViewTreeObserver
import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.Currency
import java.util.Locale import java.util.Locale
fun Bundle.optDouble(name: String): Double? { fun Bundle.optDouble(name: String): Double? {
@@ -139,4 +142,14 @@ fun PackageManager.isAppInstalled(packageName: String): Boolean {
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
false 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,7 +9,6 @@ import androidx.core.text.buildSpannedString
import net.vonforst.evmap.R import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.tesla.Pricing import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.availability.tesla.Rates 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.bold
import net.vonforst.evmap.joinToSpannedString import net.vonforst.evmap.joinToSpannedString
import net.vonforst.evmap.model.ChargeCard import net.vonforst.evmap.model.ChargeCard

View File

@@ -5,16 +5,15 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.memory.MemoryCache import coil.memory.MemoryCache
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R import net.vonforst.evmap.R
import net.vonforst.evmap.model.ChargerPhoto import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.waitForLayout
class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) : class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) :
@@ -40,12 +39,9 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
val item = getItem(position) val item = getItem(position)
if (holder.view.height == 0) { if (holder.view.height == 0) {
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { holder.view.waitForLayout {
override fun onGlobalLayout() { loadImage(item, holder)
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this) }
loadImage(item, holder)
}
})
} else { } else {
loadImage(item, holder) loadImage(item, holder)
} }
@@ -71,7 +67,7 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
memoryKeys[item.id] = metadata.memoryCacheKey memoryKeys[item.id] = metadata.memoryCacheKey
} }
) )
allowHardware(!BuildConfig.DEBUG) allowHardware(false)
} }
} }
} }

View File

@@ -7,8 +7,6 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.addDebugInterceptors import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.RateLimitInterceptor 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.api.equivalentPlugTypes
import net.vonforst.evmap.cartesianProduct import net.vonforst.evmap.cartesianProduct
import net.vonforst.evmap.model.ChargeLocation import net.vonforst.evmap.model.ChargeLocation
@@ -18,7 +16,6 @@ import net.vonforst.evmap.viewmodel.Resource
import okhttp3.Cache import okhttp3.Cache
import okhttp3.JavaNetCookieJar import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import java.net.CookieManager import java.net.CookieManager
@@ -41,16 +38,6 @@ interface AvailabilityDetector {
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector { abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
protected val radius = 150 // max radius in meters 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( protected fun getCorrespondingChargepoint(
cps: Iterable<Chargepoint>, type: String, power: Double cps: Iterable<Chargepoint>, type: String, power: Double
): Chargepoint? { ): Chargepoint? {

View File

@@ -52,32 +52,31 @@ class TeslaGuestAvailabilityDetector(
val (detailsA, guestPricing) = coroutineScope { val (detailsA, guestPricing) = coroutineScope {
val details = async { val details = async {
api.getChargingSiteDetails( api.getSiteDetails(
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest( TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables( TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
TeslaChargingGuestGraphQlApi.Identifier( TeslaChargingGuestGraphQlApi.Identifier(
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier( 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 { val guestPricing = async {
api.getChargingSiteDetails( api.getSiteDetails(
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest( TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables( TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
TeslaChargingGuestGraphQlApi.Identifier( TeslaChargingGuestGraphQlApi.Identifier(
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier( TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
trtId trtId, TeslaChargingGuestGraphQlApi.Experience.GUEST
) )
), ),
TeslaChargingGuestGraphQlApi.Experience.GUEST
) )
) )
).data.site?.pricing ).data.chargingNetwork?.site?.pricing
} }
details to guestPricing details to guestPricing
} }
@@ -103,12 +102,9 @@ class TeslaGuestAvailabilityDetector(
"charger has unknown connectors" "charger has unknown connectors"
) )
val chargerDetails = details.chargersAvailable.chargerDetails var detailsSorted = details.chargerList
val chargers = details.chargers.associateBy { it.id } .sortedBy { c -> c.labelLetter }
var detailsSorted = chargerDetails .sortedBy { c -> c.labelNumber }
.sortedBy { chargers[it.id]?.labelLetter }
.sortedBy { chargers[it.id]?.labelNumber }
if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) { if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
// apparently some connectors are missing in Tesla data // apparently some connectors are missing in Tesla data
@@ -120,7 +116,7 @@ class TeslaGuestAvailabilityDetector(
detailsSorted + List(numMissing) { detailsSorted + List(numMissing) {
TeslaChargingGuestGraphQlApi.ChargerDetail( TeslaChargingGuestGraphQlApi.ChargerDetail(
ChargerAvailability.UNKNOWN, ChargerAvailability.UNKNOWN,
"" "", ""
) )
} }
} else { } else {
@@ -151,7 +147,7 @@ class TeslaGuestAvailabilityDetector(
} }
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } } 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)

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.ChargerAvailability
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi 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.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.api.availability.tesla.asTeslaCoord import net.vonforst.evmap.api.availability.tesla.asTeslaCoord
import net.vonforst.evmap.model.ChargeLocation import net.vonforst.evmap.model.ChargeLocation
@@ -59,7 +58,7 @@ class TeslaOwnerAvailabilityDetector(
val details = api.getChargingSiteInformation( val details = api.getChargingSiteInformation(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest( TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables( TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables(
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.id.text), TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.locationGUID),
TeslaChargingOwnershipGraphQlApi.VehicleMakeType.NON_TESLA TeslaChargingOwnershipGraphQlApi.VehicleMakeType.NON_TESLA
) )
) )

View File

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

View File

@@ -1,12 +1,8 @@
package net.vonforst.evmap.api.availability.tesla package net.vonforst.evmap.api.availability.tesla
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
@@ -15,7 +11,6 @@ import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
import java.lang.reflect.Type
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
interface TeslaCuaApi { interface TeslaCuaApi {
@@ -71,24 +66,22 @@ interface TeslaCuaApi {
interface TeslaChargingGuestGraphQlApi { interface TeslaChargingGuestGraphQlApi {
@POST("graphql") @POST("graphql")
suspend fun getChargingSiteDetails( suspend fun getSiteDetails(
@Body request: GetChargingSiteDetailsRequest, @Body request: GetSiteDetailsRequest,
@Query("operationName") operationName: String = "getGuestChargingSiteDetails" @Query("operationName") operationName: String = "GetSiteDetails"
): GetChargingSiteDetailsResponse ): GetChargingSiteDetailsResponse
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsRequest( data class GetSiteDetailsRequest(
override val variables: GetChargingSiteInformationVariables, override val variables: GetSiteDetailsVariables,
override val operationName: String = "getGuestChargingSiteDetails", override val operationName: String = "GetSiteDetails",
override val query: String = 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() ) : GraphQlRequest()
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GetChargingSiteInformationVariables( data class GetSiteDetailsVariables(
val identifier: Identifier, val siteId: Identifier,
val experience: Experience,
val deviceLocale: String = "de-DE",
) )
enum class Experience { enum class Experience {
@@ -97,22 +90,22 @@ interface TeslaChargingGuestGraphQlApi {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Identifier( data class Identifier(
val siteId: ChargingSiteIdentifier val byTrtId: ChargingSiteIdentifier
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier( data class ChargingSiteIdentifier(
val id: Long, val trtId: Long,
val siteType: SiteType = SiteType.SUPERCHARGER val chargingExperience: Experience,
val programType: String = "PTSCH",
val locale: String = "de-DE",
) )
enum class SiteType { @JsonClass(generateAdapter = true)
@Json(name = "SITE_TYPE_SUPERCHARGER") data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseDataNetwork)
SUPERCHARGER
}
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseData) data class GetChargingSiteDetailsResponseDataNetwork(val chargingNetwork: GetChargingSiteDetailsResponseData?)
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponseData(val site: ChargingSiteInformation?) data class GetChargingSiteDetailsResponseData(val site: ChargingSiteInformation?)
@@ -120,9 +113,8 @@ interface TeslaChargingGuestGraphQlApi {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ChargingSiteInformation( data class ChargingSiteInformation(
val activeOutages: List<Outage>?, val activeOutages: List<Outage>?,
val chargers: List<ChargerId>, val chargerList: List<ChargerDetail>,
val chargersAvailable: ChargersAvailable, val trtId: Long,
val id: Long,
val maxPowerKw: Int, val maxPowerKw: Int,
val name: String, val name: String,
val pricing: Pricing, val pricing: Pricing,
@@ -130,9 +122,10 @@ interface TeslaChargingGuestGraphQlApi {
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ChargerId( data class ChargerDetail(
val id: String, val availability: ChargerAvailability,
val label: String?, val label: String?,
val id: String
) { ) {
val labelNumber val labelNumber
get() = label?.replace(Regex("""\D"""), "")?.toInt() get() = label?.replace(Regex("""\D"""), "")?.toInt()
@@ -140,15 +133,6 @@ interface TeslaChargingGuestGraphQlApi {
get() = label?.replace(Regex("""\d"""), "") 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 { companion object {
fun create( fun create(
client: OkHttpClient, client: OkHttpClient,

View File

@@ -16,7 +16,6 @@ import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.time.LocalTime
interface TeslaAuthenticationApi { interface TeslaAuthenticationApi {
@POST("oauth2/v3/token") @POST("oauth2/v3/token")
@@ -131,8 +130,8 @@ interface TeslaOwnerApi {
// add API key to every request // add API key to every request
val request = chain.request().newBuilder() val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token") .header("Authorization", "Bearer $token")
.header("User-Agent", "okhttp/4.9.2") .header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27") .header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("Accept", "*/*") .header("Accept", "*/*")
.build() .build()
chain.proceed(request) chain.proceed(request)
@@ -173,7 +172,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetNearbyChargingSitesVariables, override val variables: GetNearbyChargingSitesVariables,
override val operationName: String = "GetNearbyChargingSites", override val operationName: String = "GetNearbyChargingSites",
override val query: String = 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() ) : GraphQlRequest()
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -202,7 +201,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetChargingSiteInformationVariables, override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getChargingSiteInformation", override val operationName: String = "getChargingSiteInformation",
override val query: String = 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() ) : GraphQlRequest()
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -217,11 +216,11 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier( data class ChargingSiteIdentifier(
val id: String, val id: String,
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.LOCATION_GUID
) )
enum class ChargingSiteIdentifierType { enum class ChargingSiteIdentifierType {
SITE_ID SITE_ID, LOCATION_GUID
} }
enum class VehicleMakeType { enum class VehicleMakeType {
@@ -242,7 +241,6 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ChargingSite( data class ChargingSite(
val activeOutages: List<Outage>,
val availableStalls: Value<Int>?, val availableStalls: Value<Int>?,
val centroid: Coordinate, val centroid: Coordinate,
val drivingDistanceMiles: Value<Double>?, val drivingDistanceMiles: Value<Double>?,
@@ -251,7 +249,8 @@ interface TeslaChargingOwnershipGraphQlApi {
val id: Text, val id: Text,
val localizedSiteName: Value<String>, val localizedSiteName: Value<String>,
val maxPowerKw: Value<Int>, val maxPowerKw: Value<Int>,
val totalStalls: Value<Int> val totalStalls: Value<Int>,
val locationGUID: String
// TODO: siteType, accessType // TODO: siteType, accessType
) )
@@ -274,7 +273,6 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class SiteDynamic( data class SiteDynamic(
val activeOutages: List<Outage>,
val chargerDetails: List<ChargerDetail>, val chargerDetails: List<ChargerDetail>,
val chargersAvailable: Value<Int>?, val chargersAvailable: Value<Int>?,
val currentCongestion: Double, val currentCongestion: Double,
@@ -373,8 +371,8 @@ interface TeslaChargingOwnershipGraphQlApi {
// add API key to every request // add API key to every request
val request = chain.request().newBuilder() val request = chain.request().newBuilder()
.header("Authorization", "Bearer $t") .header("Authorization", "Bearer $t")
.header("User-Agent", "okhttp/4.9.2") .header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27") .header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("Accept", "*/*") .header("Accept", "*/*")
.build() .build()
chain.proceed(request) chain.proceed(request)

View File

@@ -452,7 +452,10 @@ class GoingElectricApiWrapper(
if (responses.map { it.isSuccessful }.all { it } if (responses.map { it.isSuccessful }.all { it }
&& plugsResponse.body()!!.status == STATUS_OK && plugsResponse.body()!!.status == STATUS_OK
&& chargeCardsResponse.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( Resource.success(
GEReferenceData( GEReferenceData(
plugsResponse.body()!!.result!!, plugsResponse.body()!!.result!!,

View File

@@ -244,6 +244,8 @@ class OpenChargeMapApiWrapper(
return Resource.success(ChargepointList(result, data.size < 499)) return Resource.success(ChargepointList(result, data.size < 499))
} catch (e: IOException) { } catch (e: IOException) {
return Resource.error(e.message, null) return Resource.error(e.message, null)
} catch (e: HttpException) {
return Resource.error(e.message, null)
} }
} }

View File

@@ -254,7 +254,7 @@ data class OCMUserComment(
@Json(name = "ID") val id: Long, @Json(name = "ID") val id: Long,
@Json(name = "CommentTypeID") val commentTypeId: Long, @Json(name = "CommentTypeID") val commentTypeId: Long,
@Json(name = "Comment") val comment: String?, @Json(name = "Comment") val comment: String?,
@Json(name = "UserName") val userName: String, @Json(name = "UserName") val userName: String?,
@Json(name = "DateCreated") val dateCreated: ZonedDateTime @Json(name = "DateCreated") val dateCreated: ZonedDateTime
) )

View File

@@ -45,14 +45,20 @@ interface LocationAwareScreen {
class CarAppService : androidx.car.app.CarAppService() { class CarAppService : androidx.car.app.CarAppService() {
private val CHANNEL_ID = "car_location" private val CHANNEL_ID = "car_location"
private val NOTIFICATION_ID = 1000 private val NOTIFICATION_ID = 1000
private val TAG = "CarAppService"
private var foregroundStarted = false private var foregroundStarted = false
fun ensureForegroundService() { fun ensureForegroundService() {
// we want to run as a foreground service to make sure we can use location // we want to run as a foreground service to make sure we can use location
if (!foregroundStarted) { try {
createNotificationChannel() if (!foregroundStarted) {
startForeground(NOTIFICATION_ID, getNotification()) createNotificationChannel()
foregroundStarted = true startForeground(NOTIFICATION_ID, getNotification())
foregroundStarted = true
Log.i(TAG, "Started foreground service")
}
} catch (e: SecurityException) {
Log.w(TAG, "Failed to start foreground service: ", e)
} }
} }
@@ -156,7 +162,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
} }
if (!prefs.privacyAccepted) { if (!prefs.privacyAccepted) {
screens.add( screens.add(
AcceptPrivacyScreen(carContext) AcceptPrivacyScreen(carContext, this)
) )
} }
handleACRAIntent(intent)?.let { handleACRAIntent(intent)?.let {

View File

@@ -3,13 +3,22 @@ package net.vonforst.evmap.auto
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.Model 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.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
import jsonapi.Meta import jsonapi.Meta
import jsonapi.Relationship import jsonapi.Relationship
import jsonapi.Relationships import jsonapi.Relationships
@@ -18,7 +27,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.vonforst.evmap.R 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.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider import net.vonforst.evmap.api.stringProvider
@@ -32,7 +50,9 @@ import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import kotlin.math.roundToInt 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 prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext) private val db = AppDatabase.getInstance(carContext)
private val api by lazy { private val api by lazy {
@@ -70,7 +90,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
carContext.stringProvider(), carContext.stringProvider(),
chargepoint.type chargepoint.type
) )
} ${chargepoint.formatPower()} ${ } ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
carContext.getString( carContext.getString(
R.string.chargeprice_stats, R.string.chargeprice_stats,
meta.energy, meta.energy,
@@ -130,7 +150,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
) )
).build() ).build()
).setOnClickListener { ).setOnClickListener {
openUrl(carContext, ChargepriceApi.getPoiUrl(charger)) openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
}.build() }.build()
).build() ).build()
) )

View File

@@ -11,9 +11,12 @@ import android.net.Uri
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.HostException
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
@@ -32,6 +35,7 @@ import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -49,9 +53,7 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.tesla.Pricing import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.chargeprice.ChargepriceApi import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi 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.PredictionData
import net.vonforst.evmap.api.fronyx.PredictionRepository
import net.vonforst.evmap.api.iconForPlugType import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider import net.vonforst.evmap.api.stringProvider
@@ -74,8 +76,14 @@ import java.time.format.FormatStyle
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val TAG = "ChargerDetailScreen"
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 charger: ChargeLocation? = null
var photo: Bitmap? = null var photo: Bitmap? = null
private var availability: ChargeLocationStatus? = null private var availability: ChargeLocationStatus? = null
@@ -88,7 +96,8 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private val repo = private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs) ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val availabilityRepo = AvailabilityRepository(ctx) private val availabilityRepo = AvailabilityRepository(ctx)
private val predictionRepo = PredictionRepository(ctx)
//private val predictionRepo = PredictionRepository(ctx)
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
private val imageSize = 128 // images should be 128dp according to docs private val imageSize = 128 // images should be 128dp according to docs
@@ -151,14 +160,20 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
.setTitle(carContext.getString(R.string.auto_prices)) .setTitle(carContext.getString(R.string.auto_prices))
.setOnClickListener { .setOnClickListener {
if (prefs.chargepriceNativeIntegration) { if (prefs.chargepriceNativeIntegration) {
screenManager.push(ChargepriceScreen(carContext, charger)) screenManager.push(
ChargepriceScreen(
carContext,
session,
charger
)
)
} else { } else {
val intent = Intent( val intent = Intent(
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
Uri.parse(ChargepriceApi.getPoiUrl(charger)) Uri.parse(ChargepriceApi.getPoiUrl(charger))
) )
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
carContext.startActivity(intent) session.cas.startActivity(intent)
} }
} }
.build()) .build())
@@ -177,12 +192,12 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
Action.Builder() Action.Builder()
.setTitle(carContext.getString(R.string.open_in_app)) .setTitle(carContext.getString(R.string.open_in_app))
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
val intent = Intent(carContext, MapsActivity::class.java) val intent = Intent(session.cas, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_CHARGER_ID, chargerSparse.id) .putExtra(EXTRA_CHARGER_ID, chargerSparse.id)
.putExtra(EXTRA_LAT, chargerSparse.coordinates.lat) .putExtra(EXTRA_LAT, chargerSparse.coordinates.lat)
.putExtra(EXTRA_LON, chargerSparse.coordinates.lng) .putExtra(EXTRA_LON, chargerSparse.coordinates.lng)
carContext.startActivity(intent) session.cas.startActivity(intent)
CarToast.makeText( CarToast.makeText(
carContext, carContext,
R.string.opened_on_phone, R.string.opened_on_phone,
@@ -364,7 +379,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
var text = formatTeslaPricing(teslaPricing, carContext) as CharSequence var text = formatTeslaPricing(teslaPricing, carContext) as CharSequence
formatTeslaParkingFee(teslaPricing, carContext)?.let { text += "\n\n" + it } formatTeslaParkingFee(teslaPricing, carContext)?.let { text += "\n\n" + it }
addText(text) addText(text)
} ?: { } ?: run {
addText(carContext.getString(if (prediction != null) R.string.auto_no_data else R.string.loading)) addText(carContext.getString(if (prediction != null) R.string.auto_no_data else R.string.loading))
} }
}.build()) }.build())
@@ -509,7 +524,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
} else { } else {
append(nameForPlugType(carContext.stringProvider(), cp.type)) append(nameForPlugType(carContext.stringProvider(), cp.type))
} }
cp.formatPower()?.let { cp.formatPower(carContext.currentOrDefaultLocale)?.let {
append(" ") append(" ")
append(it) append(it)
} }
@@ -543,13 +558,55 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
} }
private fun navigateToCharger(charger: ChargeLocation) { private fun navigateToCharger(charger: ChargeLocation) {
var success = navigateCarApp(charger)
if (!success && BuildConfig.FLAVOR_automotive == "automotive") {
// on AAOS, some OEMs' navigation apps might not support
success = navigateRegularApp(charger)
}
if (!success) {
CarToast.makeText(carContext, R.string.no_maps_app_found, CarToast.LENGTH_SHORT).show()
}
}
private fun navigateCarApp(charger: ChargeLocation): Boolean {
val coord = charger.coordinates val coord = charger.coordinates
val intent = val intent =
Intent( Intent(
CarContext.ACTION_NAVIGATE, CarContext.ACTION_NAVIGATE,
Uri.parse("geo:${coord.lat},${coord.lng}") Uri.parse("geo:${coord.lat},${coord.lng}")
) )
carContext.startCarApp(intent) try {
carContext.startCarApp(intent)
return true
} catch (e: HostException) {
Log.w(TAG, "Could not start navigation using car app intent")
Log.w(TAG, intent.toString())
e.printStackTrace()
} catch (e: SecurityException) {
Log.w(TAG, "Could not start navigation using car app intent")
Log.w(TAG, intent.toString())
e.printStackTrace()
}
return false
}
private fun navigateRegularApp(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(carContext.packageManager) != null) {
carContext.startActivity(intent)
return true
} else {
Log.w(TAG, "Could not start navigation using regular intent")
Log.w(TAG, intent.toString())
}
return false
} }
private fun loadCharger() { private fun loadCharger() {
@@ -603,12 +660,12 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
) )
this@ChargerDetailScreen.photo = outImg this@ChargerDetailScreen.photo = outImg
} }
fronyxSupported = charger.chargepoints.any { fronyxSupported = false /*charger.chargepoints.any {
FronyxApi.isChargepointSupported( FronyxApi.isChargepointSupported(
charger, charger,
it it
) )
} && !availabilityRepo.isSupercharger(charger) } && !availabilityRepo.isSupercharger(charger)*/
teslaSupported = availabilityRepo.isTeslaSupported(charger) teslaSupported = availabilityRepo.isTeslaSupported(charger)
invalidate() invalidate()
@@ -617,7 +674,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
invalidate() invalidate()
prediction = predictionRepo.getPredictionData(charger, availability) //prediction = predictionRepo.getPredictionData(charger, availability)
invalidate() invalidate()
} else { } else {

View File

@@ -9,14 +9,36 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager 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.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map import androidx.lifecycle.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.vonforst.evmap.R 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.AppDatabase
import net.vonforst.evmap.storage.FilterProfile import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.storage.PreferenceDataSource
@@ -232,7 +254,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
), ),
CarToast.LENGTH_SHORT CarToast.LENGTH_SHORT
).show() ).show()
invalidate()
} }
} }
}.build()) }.build())
@@ -349,7 +370,6 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
), ),
CarToast.LENGTH_SHORT CarToast.LENGTH_SHORT
).show() ).show()
invalidate()
screenManager.pop() screenManager.pop()
} }
} }
@@ -381,7 +401,6 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
} }
if (!saveSuccess) return@pushForResult if (!saveSuccess) return@pushForResult
} }
invalidate()
} }
.build() .build()
) )

View File

@@ -15,14 +15,34 @@ import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.hardware.info.CarSensors import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.EnergyLevel import androidx.car.app.hardware.info.EnergyLevel
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.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.OnContentRefreshListener
import androidx.car.app.model.Place
import androidx.car.app.model.PlaceListMapTemplate
import androidx.car.app.model.PlaceMarker
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng import com.car2go.maps.model.LatLng
import kotlinx.coroutines.* 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.BuildConfig
import net.vonforst.evmap.R import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityRepository import net.vonforst.evmap.api.availability.AvailabilityRepository
@@ -386,7 +406,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
) )
setOnClickListener { setOnClickListener {
screenManager.push(ChargerDetailScreen(carContext, charger)) screenManager.push(ChargerDetailScreen(carContext, charger, session))
session.mapScreen = null session.mapScreen = null
} }
}.build() }.build()
@@ -472,13 +492,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
zoom = 16f, zoom = 16f,
filtersWithValue filtersWithValue
).awaitFinished() ).awaitFinished()
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data.isNullOrEmpty() else response.data == null) { if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data?.items.isNullOrEmpty() else response.data == null) {
loadingError = true loadingError = true
this@MapScreen.chargers = null this@MapScreen.chargers = null
invalidate() invalidate()
return@launch return@launch
} }
chargers = response.data?.filterIsInstance(ChargeLocation::class.java) chargers = response.data?.items?.filterIsInstance<ChargeLocation>()
if (prefs.placeSearchResultAndroidAutoName == null) { if (prefs.placeSearchResultAndroidAutoName == null) {
chargers = headingFilter( chargers = headingFilter(
chargers, chargers,

View File

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

View File

@@ -43,6 +43,7 @@ import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
import net.vonforst.evmap.api.chargeprice.ChargepriceApi import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff 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.OAuthLoginFragment
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
import net.vonforst.evmap.getPackageInfoCompat import net.vonforst.evmap.getPackageInfoCompat
@@ -78,7 +79,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
) )
setBrowsable(true) setBrowsable(true)
setOnClickListener { setOnClickListener {
screenManager.push(DataSettingsScreen(carContext)) screenManager.push(DataSettingsScreen(carContext, session))
} }
}.build()) }.build())
addItem(Row.Builder().apply { addItem(Row.Builder().apply {
@@ -143,7 +144,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
) )
.setBrowsable(true) .setBrowsable(true)
.setOnClickListener { .setOnClickListener {
screenManager.push(AboutScreen(carContext)) screenManager.push(AboutScreen(carContext, session))
} }
.build() .build()
) )
@@ -152,7 +153,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) {
val prefs = PreferenceDataSource(ctx) val prefs = PreferenceDataSource(ctx)
val encryptedPrefs = EncryptedPreferenceDataStore(ctx) val encryptedPrefs = EncryptedPreferenceDataStore(ctx)
val db = AppDatabase.getInstance(ctx) val db = AppDatabase.getInstance(ctx)
@@ -215,7 +217,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
} }
} }
}.build()) }.build())
addItem( /*addItem(
Row.Builder() Row.Builder()
.setTitle(carContext.getString(R.string.pref_prediction_enabled)) .setTitle(carContext.getString(R.string.pref_prediction_enabled))
.addText(carContext.getString(R.string.pref_prediction_enabled_summary)) .addText(carContext.getString(R.string.pref_prediction_enabled_summary))
@@ -223,7 +225,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
prefs.predictionEnabled = it prefs.predictionEnabled = it
}.setChecked(prefs.predictionEnabled).build()) }.setChecked(prefs.predictionEnabled).build())
.build() .build()
) )*/
addItem(Row.Builder().apply { addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_tesla_account)) setTitle(carContext.getString(R.string.pref_tesla_account))
addText( addText(
@@ -278,7 +280,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
} }
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT)) }, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
carContext.startActivity(intent) session.cas.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") { if (BuildConfig.FLAVOR_automotive != "automotive") {
CarToast.makeText( CarToast.makeText(
@@ -486,10 +488,9 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
addItem(Row.Builder().apply { addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_currency)) setTitle(carContext.getString(R.string.pref_chargeprice_currency))
val names =
carContext.resources.getStringArray(R.array.pref_chargeprice_currency_names)
val values = 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) val index = values.indexOf(prefs.chargepriceCurrency)
addText(if (index >= 0) names[index] else "") addText(if (index >= 0) names[index] else "")
@@ -629,8 +630,8 @@ class SelectCurrencyScreen(ctx: CarContext) : MultiSelectSearchScreen<Pair<Strin
override fun getLabel(it: Pair<String, String>): String = it.first override fun getLabel(it: Pair<String, String>): String = it.first
override suspend fun loadData(): List<Pair<String, String>> { 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_currencies)
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currency_values) val names = values.map(::currencyDisplayName)
return names.zip(values) return names.zip(values)
} }
} }
@@ -752,7 +753,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) val prefs = PreferenceDataSource(ctx)
var developerOptionsCounter = 0 var developerOptionsCounter = 0
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST) private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
@@ -797,7 +799,11 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setTitle(carContext.getString(R.string.faq)) .setTitle(carContext.getString(R.string.faq))
.setBrowsable(true) .setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.faq_link)) openUrl(
carContext,
session.cas,
carContext.getString(R.string.faq_link)
)
}).build() }).build()
) )
addItem( addItem(
@@ -808,12 +814,16 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
if (BuildConfig.FLAVOR_automotive == "automotive") { if (BuildConfig.FLAVOR_automotive == "automotive") {
// we can't open the donation page on the phone in this case // 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 { } else {
val intent = Intent(carContext, MapsActivity::class.java) val intent = Intent(carContext, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_DONATE, true) .putExtra(EXTRA_DONATE, true)
carContext.startActivity(intent) session.cas.startActivity(intent)
CarToast.makeText( CarToast.makeText(
carContext, carContext,
R.string.opened_on_phone, R.string.opened_on_phone,
@@ -825,39 +835,75 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
}.build(), carContext.getString(R.string.about))) }.build(), carContext.getString(R.string.about)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply { addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder() addItem(Row.Builder()
.setTitle(carContext.getString(R.string.twitter)) .setTitle(carContext.getString(R.string.mastodon))
.addText(carContext.getString(R.string.twitter_handle)) .addText(carContext.getString(R.string.mastodon_handle))
.setBrowsable(true) .setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.twitter_url)) openUrl(
carContext,
session.cas,
carContext.getString(R.string.mastodon_url)
)
}).build() }).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) { if (maxRows > 6) {
addItem(Row.Builder() addItem(Row.Builder()
.setTitle(carContext.getString(R.string.goingelectric_forum)) .setTitle(carContext.getString(R.string.goingelectric_forum))
.setBrowsable(true) .setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl( openUrl(
carContext, carContext, session.cas,
carContext.getString(R.string.goingelectric_forum_url) carContext.getString(R.string.goingelectric_forum_url)
) )
}).build() }).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))) }.build(), carContext.getString(R.string.contact)))
addSectionedList(SectionedItemList.create(ItemList.Builder().apply { addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
addItem(Row.Builder() addItem(Row.Builder()
.setTitle(carContext.getString(R.string.github_link_title)) .setTitle(carContext.getString(R.string.github_link_title))
.setBrowsable(true) .setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.github_link)) openUrl(carContext, session.cas, carContext.getString(R.string.github_link))
}).build() }).build()
) )
addItem(Row.Builder() addItem(Row.Builder()
.setTitle(carContext.getString(R.string.privacy)) .setTitle(carContext.getString(R.string.privacy))
.setBrowsable(true) .setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link)) openUrl(
carContext,
session.cas,
carContext.getString(R.string.privacy_link)
)
}).build() }).build()
) )
}.build(), carContext.getString(R.string.other))) }.build(), carContext.getString(R.string.other)))
@@ -865,7 +911,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) val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val textWithoutLink = HtmlCompat.fromHtml( val textWithoutLink = HtmlCompat.fromHtml(
@@ -886,7 +933,7 @@ class AcceptPrivacyScreen(ctx: CarContext) : Screen(ctx) {
addAction(Action.Builder() addAction(Action.Builder()
.setTitle(carContext.getString(R.string.privacy)) .setTitle(carContext.getString(R.string.privacy))
.setOnClickListener(ParkedOnlyOnClickListener.create { .setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link)) openUrl(carContext, session.cas, carContext.getString(R.string.privacy_link))
}).build() }).build()
) )
}.build() }.build()

View File

@@ -12,9 +12,14 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.* import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import androidx.car.app.versioning.CarAppApiLevels import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
@@ -26,7 +31,7 @@ import net.vonforst.evmap.getPackageInfoCompat
import net.vonforst.evmap.kmPerMile import net.vonforst.evmap.kmPerMile
import net.vonforst.evmap.shouldUseImperialUnits import net.vonforst.evmap.shouldUseImperialUnits
import net.vonforst.evmap.ydPerMile import net.vonforst.evmap.ydPerMile
import java.util.* import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor { fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
@@ -197,7 +202,7 @@ fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): Li
fun getAndroidAutoVersion(ctx: Context): List<String> { fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0) val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0)
return info.versionName.split(".") return info.versionName!!.split(".")
} }
fun supportsCarApiLevel3(ctx: CarContext): Boolean { fun supportsCarApiLevel3(ctx: CarContext): Boolean {
@@ -207,7 +212,9 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
val version = getAndroidAutoVersion(ctx) val version = getAndroidAutoVersion(ctx)
// Android Auto 6.7 is required. 6.6 reports supporting API Level 3, // Android Auto 6.7 is required. 6.6 reports supporting API Level 3,
// but crashes when using it. See: https://issuetracker.google.com/issues/199509584 // but crashes when using it. See: https://issuetracker.google.com/issues/199509584
if (version[0] < "6" || version[0] == "6" && version[1] < "7") { val major = version[0].toIntOrNull() ?: return false
val minor = version[1].toIntOrNull() ?: return false
if (major < 6 || major < 6 && minor < 7) {
return false return false
} }
} }
@@ -215,13 +222,14 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
return true return true
} }
fun openUrl(carContext: CarContext, url: String) { @ExperimentalCarApi
fun openUrl(carContext: CarContext, cas: CarAppService, url: String) {
val intent = CustomTabsIntent.Builder() val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams( .setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder() CustomTabColorSchemeParams.Builder()
.setToolbarColor( .setToolbarColor(
ContextCompat.getColor( ContextCompat.getColor(
carContext, cas,
R.color.colorPrimary R.color.colorPrimary
) )
) )
@@ -231,7 +239,7 @@ fun openUrl(carContext: CarContext, url: String) {
intent.data = Uri.parse(url) intent.data = Uri.parse(url)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try { try {
carContext.startActivity(intent) cas.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") { if (BuildConfig.FLAVOR_automotive != "automotive") {
// only show the toast "opened on phone" if we're running on a phone // only show the toast "opened on phone" if we're running on a phone
CarToast.makeText( CarToast.makeText(

View File

@@ -6,9 +6,18 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.hardware.CarHardwareManager import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.* import androidx.car.app.hardware.info.Compass
import androidx.car.app.model.* import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.hardware.info.Model
import androidx.car.app.hardware.info.Speed
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.GridItem
import androidx.car.app.model.GridTemplate
import androidx.car.app.model.ItemList
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
@@ -18,14 +27,14 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.ui.CompassNeedle import net.vonforst.evmap.ui.CompassNeedle
import net.vonforst.evmap.ui.Gauge import net.vonforst.evmap.ui.Gauge
import net.vonforst.evmap.utils.formatDecimal import net.vonforst.evmap.utils.formatDecimal
import patchedCarInfo
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.ExperimentalCarApi
class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx),
LocationAwareScreen, DefaultLifecycleObserver { LocationAwareScreen, DefaultLifecycleObserver {
private val carInfo = private val carInfo = carContext.patchedCarInfo
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
private val carSensors = carContext.patchedCarSensors private val carSensors = carContext.patchedCarSensors
private var model: Model? = null private var model: Model? = null
private var energyLevel: EnergyLevel? = null private var energyLevel: EnergyLevel? = null

View File

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

View File

@@ -100,9 +100,9 @@ class ChargepriceFragment : Fragment() {
inflater, inflater,
R.layout.fragment_chargeprice_header, container, false R.layout.fragment_chargeprice_header, container, false
) )
binding.lifecycleOwner = this binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm binding.vm = vm
headerBinding.lifecycleOwner = this headerBinding.lifecycleOwner = viewLifecycleOwner
headerBinding.vm = vm headerBinding.vm = vm
binding.toolbar.inflateMenu(R.menu.chargeprice) binding.toolbar.inflateMenu(R.menu.chargeprice)
@@ -141,7 +141,7 @@ class ChargepriceFragment : Fragment() {
val chargepriceAdapter = ChargepriceAdapter().apply { val chargepriceAdapter = ChargepriceAdapter().apply {
onClickListener = { onClickListener = {
(requireActivity() as MapsActivity).openUrl(it.url) (requireActivity() as MapsActivity).openUrl(it.url, binding.root)
} }
} }
val joinedAdapter = ConcatAdapter( val joinedAdapter = ConcatAdapter(
@@ -194,7 +194,10 @@ class ChargepriceFragment : Fragment() {
} }
binding.imgChargepriceLogo.setOnClickListener { binding.imgChargepriceLogo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(ChargepriceApi.getPoiUrl(charger)) (requireActivity() as MapsActivity).openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
} }
binding.btnSettings.setOnClickListener { binding.btnSettings.setOnClickListener {
@@ -213,11 +216,19 @@ class ChargepriceFragment : Fragment() {
} }
false false
} }
headerBinding.tvChargeFromTo.setOnClickListener {
it.postDelayed({
vm.resetBatteryRangeToDefault()
}, 250)
}
binding.toolbar.setOnMenuItemClickListener { binding.toolbar.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_help -> { 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 true
} }
else -> false 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.DialogConnectorDetailsBinding
import net.vonforst.evmap.databinding.DialogConnectorDetailsHeaderBinding import net.vonforst.evmap.databinding.DialogConnectorDetailsHeaderBinding
import net.vonforst.evmap.model.Chargepoint import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
class ConnectorDetailsDialog( class ConnectorDetailsDialog(
val binding: DialogConnectorDetailsBinding, binding: DialogConnectorDetailsBinding,
context: Context, context: Context,
onClose: () -> Unit onClose: () -> Unit
) { ) {
private val headerBinding: DialogConnectorDetailsHeaderBinding private var headerBinding_: DialogConnectorDetailsHeaderBinding? = null
private val headerBinding get() = headerBinding_!!
private val detailsAdapter = ConnectorDetailsAdapter() private val detailsAdapter = ConnectorDetailsAdapter()
init { init {
@@ -30,7 +30,7 @@ class ConnectorDetailsDialog(
layoutManager = layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
} }
headerBinding = DataBindingUtil.inflate( headerBinding_ = DataBindingUtil.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
R.layout.dialog_connector_details_header, binding.list, false 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.divider.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
headerBinding.item = ConnectorAdapter.ChargepointWithAvailability(cp, cpStatus) headerBinding.item = ConnectorAdapter.ChargepointWithAvailability(cp, cpStatus)
} }
fun onDestroy() {
headerBinding_ = null
}
} }

View File

@@ -1,29 +1,26 @@
package net.vonforst.evmap.fragment package net.vonforst.evmap.fragment
import android.content.Intent
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
abstract class DonateFragmentBase : Fragment() { abstract class DonateFragmentBase : Fragment() {
fun setupReferrals(referrals: FragmentDonateReferralBinding) { fun setupReferrals(referrals: FragmentDonateReferralBinding) {
referrals.referralTesla.setOnClickListener { referrals.referralWebView.loadUrl(getString(R.string.referral_link))
(requireActivity() as MapsActivity).openUrl(getString(R.string.tesla_referral_link)) referrals.referralWebView.webViewClient = object : WebViewClient() {
} override fun shouldOverrideUrlLoading(
referrals.referralJuicify.setOnClickListener { view: WebView,
(requireActivity() as MapsActivity).openUrl(getString(R.string.juicify_referral_link)) request: WebResourceRequest
} ): Boolean {
referrals.referralGeldfuereauto.setOnClickListener { Intent(Intent.ACTION_VIEW, request.url).apply {
(requireActivity() as MapsActivity).openUrl(getString(R.string.geldfuereauto_referral_link)) startActivity(this)
} }
referrals.referralMaingau.setOnClickListener { return true
(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))
} }
} }
} }

View File

@@ -65,7 +65,7 @@ class FavoritesFragment : Fragment() {
inflater, inflater,
R.layout.fragment_favorites, container, false R.layout.fragment_favorites, container, false
) )
binding.lifecycleOwner = this binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm binding.vm = vm
return binding.root return binding.root

View File

@@ -45,7 +45,7 @@ class FilterFragment : Fragment(), MenuProvider {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false) binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false)
binding.lifecycleOwner = this binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm binding.vm = vm
vm.filterProfile.observe(viewLifecycleOwner) {} vm.filterProfile.observe(viewLifecycleOwner) {}

View File

@@ -57,7 +57,7 @@ class FilterProfilesFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentFilterProfilesBinding.inflate(inflater, container, false) binding = FragmentFilterProfilesBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm binding.vm = vm
return binding.root return binding.root
@@ -188,9 +188,17 @@ class FilterProfilesFragment : Fragment() {
dialog.setTitle(R.string.rename) dialog.setTitle(R.string.rename)
.setMessage(R.string.save_profile_enter_name) .setMessage(R.string.save_profile_enter_name)
}, { }, { newName ->
lifecycleScope.launch { lifecycleScope.launch {
vm.update(fp.copy(name = it)) if (vm.filterProfiles.value?.find { it.name == newName } != null) {
Snackbar.make(
view,
R.string.filterprofile_name_not_unique,
Snackbar.LENGTH_LONG
).show()
} else {
vm.update(fp.copy(name = newName))
}
} }
}) })
}) })

View File

@@ -24,6 +24,7 @@ import android.widget.AdapterView
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.BackEventCompat
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
@@ -35,7 +36,6 @@ import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -55,12 +55,18 @@ import androidx.transition.TransitionManager
import coil.load import coil.load
import coil.memory.MemoryCache import coil.memory.MemoryCache
import com.car2go.maps.AnyMap import com.car2go.maps.AnyMap
import com.car2go.maps.MapFactory
import com.car2go.maps.MapFragment import com.car2go.maps.MapFragment
import com.car2go.maps.OnMapReadyCallback import com.car2go.maps.OnMapReadyCallback
import com.car2go.maps.model.BitmapDescriptor import com.car2go.maps.model.BitmapDescriptor
import com.car2go.maps.model.LatLng import com.car2go.maps.model.LatLng
import com.car2go.maps.model.Marker import com.car2go.maps.model.Marker
import com.car2go.maps.model.MarkerOptions import com.car2go.maps.model.MarkerOptions
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_SETTLING
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialArcMotion import com.google.android.material.transition.MaterialArcMotion
@@ -68,14 +74,6 @@ import com.google.android.material.transition.MaterialContainerTransform
import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.from
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.stfalcon.imageviewer.StfalconImageViewer import com.stfalcon.imageviewer.StfalconImageViewer
import io.michaelrocks.bimap.HashBiMap import io.michaelrocks.bimap.HashBiMap
import io.michaelrocks.bimap.MutableBiMap import io.michaelrocks.bimap.MutableBiMap
@@ -89,6 +87,7 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.chargeprice.ChargepriceApi import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.autocomplete.ApiUnavailableException import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds import net.vonforst.evmap.autocomplete.PlaceWithBounds
@@ -136,16 +135,16 @@ import kotlin.collections.set
import kotlin.math.min import kotlin.math.min
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback, MenuProvider { class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
private lateinit var binding: FragmentMapBinding private var _binding: FragmentMapBinding? = null
private val binding get() = _binding!!
private val vm: MapViewModel by viewModels() private val vm: MapViewModel by viewModels()
private val galleryVm: GalleryViewModel by activityViewModels() private val galleryVm: GalleryViewModel by activityViewModels()
private var mapFragment: MapFragment? = null private var mapFragment: MapFragment? = null
private var map: AnyMap? = null private var map: AnyMap? = null
private lateinit var locationEngine: LocationEngine private lateinit var locationEngine: LocationEngine
private var requestingLocationUpdates = false private var requestingLocationUpdates = false
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View> private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
private lateinit var detailsDialog: ConnectorDetailsDialog private lateinit var detailsDialog: ConnectorDetailsDialog
private lateinit var prefs: PreferenceDataSource private lateinit var prefs: PreferenceDataSource
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap() private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
@@ -153,6 +152,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private var searchResultMarker: Marker? = null private var searchResultMarker: Marker? = null
private var searchResultIcon: BitmapDescriptor? = null private var searchResultIcon: BitmapDescriptor? = null
private var connectionErrorSnackbar: Snackbar? = null private var connectionErrorSnackbar: Snackbar? = null
private var zoomInSnackbar: Snackbar? = null
private var previousChargepointIds: Set<Long>? = null private var previousChargepointIds: Set<Long>? = null
private var mapTopPadding: Int = 0 private var mapTopPadding: Int = 0
private var popupMenu: PopupMenu? = null private var popupMenu: PopupMenu? = null
@@ -161,6 +161,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private lateinit var chargerIconGenerator: ChargerIconGenerator private lateinit var chargerIconGenerator: ChargerIconGenerator
private lateinit var animator: MarkerAnimator private lateinit var animator: MarkerAnimator
private lateinit var favToggle: MenuItem private lateinit var favToggle: MenuItem
private val bottomSheetBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
val state = bottomSheetBehavior.state
when (state) {
STATE_COLLAPSED -> vm.chargerSparse.value = null
STATE_HIDDEN -> return
else -> if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
}
bottomSheetBehavior.cancelBackProgress()
}
override fun handleOnBackStarted(backEvent: BackEventCompat) {
bottomSheetBehavior.startBackProgress(backEvent)
}
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
bottomSheetBehavior.updateBackProgress(backEvent)
}
override fun handleOnBackCancelled() {
bottomSheetBehavior.cancelBackProgress()
}
}
private val backPressedCallback = object : OnBackPressedCallback(false) { private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
val value = vm.layersMenuOpen.value val value = vm.layersMenuOpen.value
@@ -177,18 +206,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (binding.search.hasFocus()) { if (binding.search.hasFocus()) {
removeSearchFocus() removeSearchFocus()
return
} }
val state = bottomSheetBehavior.state vm.searchResult.value = null
when (state) {
STATE_COLLAPSED -> vm.chargerSparse.value = null
STATE_HIDDEN -> vm.searchResult.value = null
else -> if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
}
} }
} }
@@ -211,9 +232,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false) _binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false)
println(binding.detailView.sourceButton) println(binding.detailView.sourceButton)
binding.lifecycleOwner = this binding.lifecycleOwner = viewLifecycleOwner
binding.vm = vm binding.vm = vm
val provider = prefs.mapProvider val provider = prefs.mapProvider
@@ -221,16 +242,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
mapFragment = mapFragment =
childFragmentManager.findFragmentByTag(mapFragmentTag) as MapFragment? childFragmentManager.findFragmentByTag(mapFragmentTag) as MapFragment?
} }
if (mapFragment == null || mapFragment!!.priority[0] != provider) { if (mapFragment == null || mapFragment!!.priority[0] != getMapProvider(provider)) {
mapFragment = MapFragment() mapFragment = MapFragment()
mapFragment!!.priority = arrayOf( mapFragment!!.priority = arrayOf(
when (provider) { getMapProvider(provider),
"mapbox" -> MapFragment.MAPBOX MapFactory.GOOGLE,
"google" -> MapFragment.GOOGLE MapFactory.MAPLIBRE
else -> null
},
MapFragment.GOOGLE,
MapFragment.MAPBOX
) )
childFragmentManager childFragmentManager
.beginTransaction() .beginTransaction()
@@ -245,7 +262,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
searchResultIcon = null searchResultIcon = null
} }
binding.detailAppBar.toolbar.popupTheme = binding.detailView.toolbar.popupTheme =
com.google.android.material.R.style.ThemeOverlay_AppCompat_DayNight com.google.android.material.R.style.ThemeOverlay_AppCompat_DayNight
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
@@ -254,9 +271,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
ViewCompat.onApplyWindowInsets(binding.root, insets) ViewCompat.onApplyWindowInsets(binding.root, insets)
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> { /*binding.detailView.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemWindowInsetTop topMargin = systemWindowInsetTop
} }*/
val insetsBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom + insetsBottom
// margin of layers button: status bar height + toolbar height + margin // margin of layers button: status bar height + toolbar height + margin
val density = resources.displayMetrics.density val density = resources.displayMetrics.density
@@ -275,7 +295,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
// set map padding so that compass is not obstructed by toolbar // set map padding so that compass is not obstructed by toolbar
mapTopPadding = systemWindowInsetTop + (48 * density).toInt() + (16 * density).toInt() mapTopPadding = systemWindowInsetTop + (48 * density).toInt() + (16 * density).toInt()
// if we actually use map.setPadding here, Mapbox will re-trigger onApplyWindowInsets // if we actually use map.setPadding here, MapLibre will re-trigger onApplyWindowInsets
// and cause an infinite loop. So we rely on onMapReady being called later than // and cause an infinite loop. So we rely on onMapReady being called later than
// onApplyWindowInsets. // onApplyWindowInsets.
@@ -289,33 +309,45 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
viewLifecycleOwner, viewLifecycleOwner,
backPressedCallback backPressedCallback
) )
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
bottomSheetBackPressedCallback
)
return binding.root return binding.root
} }
private fun getMapProvider(provider: String) = when (provider) {
"mapbox" -> MapFactory.MAPLIBRE
"google" -> MapFactory.GOOGLE
else -> null
}
val bottomSheetCollapsible val bottomSheetCollapsible
get() = resources.getBoolean(R.bool.bottom_sheet_collapsible) get() = resources.getBoolean(R.bool.bottom_sheet_collapsible)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet || !prefs.privacyAccepted) {
findNavController().navigate(R.id.onboarding)
}
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
mapFragment!!.getMapAsync(this) mapFragment!!.getMapAsync(this)
bottomSheetBehavior = from(binding.bottomSheet) bottomSheetBehavior = BottomSheetBehavior.from(binding.detailView.root)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar) //detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail) binding.detailView.toolbar.inflateMenu(R.menu.detail)
favToggle = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_fav) favToggle = binding.detailView.toolbar.menu.findItem(R.id.menu_fav)
vm.apiName.observe(viewLifecycleOwner) { vm.apiName.observe(viewLifecycleOwner) {
binding.detailAppBar.toolbar.menu.findItem(R.id.menu_edit).title = binding.detailView.toolbar.menu.findItem(R.id.menu_edit).title =
getString(R.string.edit_at_datasource, it) getString(R.string.edit_at_datasource, it)
} }
binding.detailView.topPart.doOnNextLayout { vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom bottomSheetBehavior.skipCollapsed = !bottomSheetCollapsible
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it } bottomSheetBehavior.state = STATE_HIDDEN
}
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
binding.detailView.connectorDetails binding.detailView.connectorDetails
setupObservers() setupObservers()
@@ -358,9 +390,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.appLogo.root.animate().alpha(1f) binding.appLogo.root.animate().alpha(1f)
.withEndAction { .withEndAction {
if (_binding == null) return@withEndAction
binding.appLogo.root.animate().alpha(0f).apply { binding.appLogo.root.animate().alpha(0f).apply {
startDelay = 1000 startDelay = 1000
}.withEndAction { }.withEndAction {
if (_binding == null) return@withEndAction
binding.appLogo.root.visibility = View.GONE binding.appLogo.root.visibility = View.GONE
binding.search.visibility = View.VISIBLE binding.search.visibility = View.VISIBLE
binding.search.alpha = 0f binding.search.alpha = 0f
@@ -384,9 +418,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
val hostActivity = activity as? MapsActivity ?: return
hostActivity.fragmentCallback = this
vm.reloadPrefs() vm.reloadPrefs()
if (requestingLocationUpdates && requireContext().checkAnyLocationPermission() if (requestingLocationUpdates && requireContext().checkAnyLocationPermission()
) { ) {
@@ -418,7 +449,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.charger.value?.data val charger = vm.charger.value?.data
if (charger != null) { if (charger != null) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
(requireActivity() as MapsActivity).navigateTo(charger) (requireActivity() as MapsActivity).navigateTo(charger, binding.root)
} }
} }
} }
@@ -431,7 +462,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.detailView.sourceButton.setOnClickListener { binding.detailView.sourceButton.setOnClickListener {
val charger = vm.charger.value?.data val charger = vm.charger.value?.data
if (charger != null) { if (charger != null) {
(activity as? MapsActivity)?.openUrl(charger.url) (activity as? MapsActivity)?.openUrl(charger.url, binding.root, true)
} }
} }
binding.detailView.btnChargeprice.setOnClickListener { binding.detailView.btnChargeprice.setOnClickListener {
@@ -444,12 +475,15 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
extras extras
) )
} else { } else {
(activity as? MapsActivity)?.openUrl(ChargepriceApi.getPoiUrl(charger), false) (activity as? MapsActivity)?.openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
} }
} }
binding.detailView.btnChargerWebsite.setOnClickListener { binding.detailView.btnChargerWebsite.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener val charger = vm.charger.value?.data ?: return@setOnClickListener
charger.chargerUrl?.let { (activity as? MapsActivity)?.openUrl(it) } charger.chargerUrl?.let { (activity as? MapsActivity)?.openUrl(it, binding.root) }
} }
binding.detailView.btnLogin.setOnClickListener { binding.detailView.btnLogin.setOnClickListener {
findNavController().safeNavigate( findNavController().safeNavigate(
@@ -457,7 +491,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
) )
} }
binding.detailView.imgPredictionSource.setOnClickListener { binding.detailView.imgPredictionSource.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.fronyx_url)) (activity as? MapsActivity)?.openUrl(getString(R.string.fronyx_url), binding.root)
} }
binding.detailView.btnPredictionHelp.setOnClickListener { binding.detailView.btnPredictionHelp.setOnClickListener {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
@@ -466,7 +500,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
.show() .show()
} }
binding.detailView.topPart.setOnClickListener { binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = STATE_ANCHOR_POINT bottomSheetBehavior.state = STATE_HALF_EXPANDED
} }
binding.detailView.topPart.setOnLongClickListener { binding.detailView.topPart.setOnLongClickListener {
val charger = vm.charger.value?.data ?: return@setOnLongClickListener false val charger = vm.charger.value?.data ?: return@setOnLongClickListener false
@@ -474,14 +508,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return@setOnLongClickListener true return@setOnLongClickListener true
} }
setupSearchAutocomplete() setupSearchAutocomplete()
binding.detailAppBar.toolbar.setNavigationOnClickListener { binding.detailView.toolbar.setNavigationOnClickListener {
if (bottomSheetCollapsible) { if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED bottomSheetBehavior.state = STATE_COLLAPSED
} else { } else {
vm.chargerSparse.value = null vm.chargerSparse.value = null
} }
} }
binding.detailAppBar.toolbar.setOnMenuItemClickListener { binding.detailView.toolbar.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_fav -> { R.id.menu_fav -> {
toggleFavorite() toggleFavorite()
@@ -497,7 +531,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
R.id.menu_edit -> { R.id.menu_edit -> {
val charger = vm.charger.value?.data val charger = vm.charger.value?.data
if (charger?.editUrl != null) { if (charger?.editUrl != null) {
(activity as? MapsActivity)?.openUrl(charger.editUrl) (activity as? MapsActivity)?.openUrl(charger.editUrl, binding.root, true)
if (vm.apiId.value == "goingelectric") { if (vm.apiId.value == "goingelectric") {
// instructions specific to GoingElectric // instructions specific to GoingElectric
Toast.makeText( Toast.makeText(
@@ -642,7 +676,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun setupObservers() { private fun setupObservers() {
bottomSheetBehavior.addBottomSheetCallback(object : bottomSheetBehavior.addBottomSheetCallback(object :
BottomSheetCallback() { BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) { override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (bottomSheetBehavior.state == STATE_HIDDEN) { if (bottomSheetBehavior.state == STATE_HIDDEN) {
map?.setPadding(0, mapTopPadding, 0, 0) map?.setPadding(0, mapTopPadding, 0, 0)
@@ -659,7 +693,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onStateChanged(bottomSheet: View, newState: Int) { override fun onStateChanged(bottomSheet: View, newState: Int) {
vm.bottomSheetState.value = newState vm.bottomSheetState.value = newState
updateBackPressedCallback() bottomSheetBackPressedCallback.isEnabled = newState != STATE_HIDDEN
if (vm.layersMenuOpen.value!! && newState !in listOf( if (vm.layersMenuOpen.value!! && newState !in listOf(
STATE_SETTLING, STATE_SETTLING,
@@ -671,7 +705,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
if (vm.selectedChargepoint.value != null && newState in listOf( if (vm.selectedChargepoint.value != null && newState in listOf(
STATE_ANCHOR_POINT, STATE_COLLAPSED STATE_HALF_EXPANDED, STATE_COLLAPSED
) )
) { ) {
closeConnectorDetailsDialog() closeConnectorDetailsDialog()
@@ -681,13 +715,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}) })
vm.chargerSparse.observe(viewLifecycleOwner) { vm.chargerSparse.observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
if (vm.bottomSheetState.value != STATE_ANCHOR_POINT) { if (vm.bottomSheetState.value != STATE_HALF_EXPANDED) {
bottomSheetBehavior.state = bottomSheetBehavior.state =
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_HALF_EXPANDED
} }
removeSearchFocus() removeSearchFocus()
binding.fabDirections.show() binding.fabDirections.show()
detailAppBarBehavior.setToolbarTitle(it.name) //detailAppBarBehavior.setToolbarTitle(it.name)
updateFavoriteToggle() updateFavoriteToggle()
highlightMarker(it) highlightMarker(it)
} else { } else {
@@ -698,12 +732,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.chargepoints.observe(viewLifecycleOwner, Observer { res -> vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
val chargepoints = res.data val chargepoints = res.data
if (chargepoints != null) { if (chargepoints != null) {
updateMap(chargepoints) updateMap(chargepoints.items)
} }
val view = view ?: return@Observer
when (res.status) { when (res.status) {
Status.ERROR -> { Status.ERROR -> {
val view = view ?: return@Observer zoomInSnackbar?.dismiss()
connectionErrorSnackbar?.dismiss() connectionErrorSnackbar?.dismiss()
connectionErrorSnackbar = Snackbar connectionErrorSnackbar = Snackbar
.make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE) .make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE)
@@ -715,13 +749,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
Status.SUCCESS -> { Status.SUCCESS -> {
connectionErrorSnackbar?.dismiss() connectionErrorSnackbar?.dismiss()
if (res.data != null && !res.data.isComplete) {
zoomInSnackbar?.dismiss()
zoomInSnackbar = Snackbar
.make(view, R.string.zoom_in_to_see_more, Snackbar.LENGTH_INDEFINITE)
zoomInSnackbar!!.show()
}
} }
Status.LOADING -> { Status.LOADING -> {
zoomInSnackbar?.dismiss()
} }
} }
}) })
vm.useMiniMarkers.observe(viewLifecycleOwner) { vm.useMiniMarkers.observe(viewLifecycleOwner) {
vm.chargepoints.value?.data?.let { updateMap(it) } vm.chargepoints.value?.data?.let { updateMap(it.items) }
} }
vm.favorites.observe(viewLifecycleOwner) { vm.favorites.observe(viewLifecycleOwner) {
updateFavoriteToggle() updateFavoriteToggle()
@@ -747,7 +788,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (it != null) { if (it != null) {
detailsDialog.setData(it, vm.availability.value?.data) detailsDialog.setData(it, vm.availability.value?.data)
} }
updateBackPressedCallback()
} }
updateBackPressedCallback() updateBackPressedCallback()
@@ -788,12 +828,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
private fun updateBackPressedCallback() { private fun updateBackPressedCallback() {
backPressedCallback.isEnabled = backPressedCallback.isEnabled = vm.searchResult.value != null
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|| vm.searchResult.value != null
|| (vm.layersMenuOpen.value ?: false) || (vm.layersMenuOpen.value ?: false)
|| binding.search.hasFocus() || binding.search.hasFocus()
|| vm.selectedChargepoint.value != null
} }
private fun unhighlightAllMarkers() { private fun unhighlightAllMarkers() {
@@ -864,6 +901,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (photo == photos[position] && imageCacheKey != null) { if (photo == photos[position] && imageCacheKey != null) {
placeholderMemoryCacheKey(imageCacheKey) placeholderMemoryCacheKey(imageCacheKey)
} }
allowHardware(false)
} }
} }
.withTransitionFrom(view as ImageView) .withTransitionFrom(view as ImageView)
@@ -912,10 +950,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (charger != null) { if (charger != null) {
when (it.icon) { when (it.icon) {
R.drawable.ic_location, R.drawable.ic_address -> { R.drawable.ic_location, R.drawable.ic_address -> {
(activity as? MapsActivity)?.showLocation(charger) (activity as? MapsActivity)?.showLocation(charger, binding.root)
} }
R.drawable.ic_fault_report -> { R.drawable.ic_fault_report -> {
(activity as? MapsActivity)?.openUrl(charger.url) (activity as? MapsActivity)?.openUrl(
charger.url,
binding.root,
true
)
} }
R.drawable.ic_payment -> { R.drawable.ic_payment -> {
@@ -923,7 +965,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
R.drawable.ic_network -> { R.drawable.ic_network -> {
charger.networkUrl?.let { (activity as? MapsActivity)?.openUrl(it) } charger.networkUrl?.let {
(activity as? MapsActivity)?.openUrl(
it,
binding.root
)
}
} }
} }
} }
@@ -1042,17 +1089,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
.setTitle(R.string.charge_cards) .setTitle(R.string.charge_cards)
.setItems(names.toTypedArray()) { _, i -> .setItems(names.toTypedArray()) { _, i ->
val card = data[i] val card = data[i]
(activity as? MapsActivity)?.openUrl("https:${card.url}") (activity as? MapsActivity)?.openUrl("https:${card.url}", binding.root)
}.show() }.show()
} }
override fun onMapReady(map: AnyMap) { override fun onMapReady(map: AnyMap) {
this.map = map this.map = map
vm.mapProjection = map.projection
val context = this.context ?: return val context = this.context ?: return
view ?: return
chargerIconGenerator = ChargerIconGenerator(context, map.bitmapDescriptorFactory) chargerIconGenerator = ChargerIconGenerator(context, map.bitmapDescriptorFactory)
if (BuildConfig.FLAVOR.contains("google") && mapFragment!!.priority[0] == MapFragment.GOOGLE) { vm.mapTrafficSupported.value =
mapFragment?.let { AnyMap.Feature.TRAFFIC_LAYER in it.supportedFeatures } ?: false
if (BuildConfig.FLAVOR.contains("google") && mapFragment!!.priority[0] == MapFactory.GOOGLE) {
// Google Maps: icons can be generated in background thread // Google Maps: icons can be generated in background thread
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
@@ -1060,7 +1111,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
} }
} else { } else {
// Mapbox: needs to be run on main thread // MapLibre: needs to be run on main thread
chargerIconGenerator.preloadCache() chargerIconGenerator.preloadCache()
} }
@@ -1073,14 +1124,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.uiSettings.setIndoorLevelPickerEnabled(false) map.uiSettings.setIndoorLevelPickerEnabled(false)
map.setOnCameraIdleListener { map.setOnCameraIdleListener {
vm.mapProjection = map.projection
vm.mapPosition.value = MapPosition( vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
) )
vm.reloadChargepoints() vm.reloadChargepoints()
} }
map.setOnCameraMoveListener { map.setOnCameraMoveListener {
vm.mapProjection = map.projection
vm.mapPosition.value = MapPosition( vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
) )
@@ -1103,7 +1152,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
} }
vm.mapPosition.observe(viewLifecycleOwner) { vm.mapPosition.observe(viewLifecycleOwner) {
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude) val target = map.cameraPosition.target ?: return@observe
binding.scaleView.update(map.cameraPosition.zoom, target.latitude)
} }
map.setOnCameraMoveStartedListener { reason -> map.setOnCameraMoveStartedListener { reason ->
@@ -1120,6 +1170,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
} }
map.setOnMarkerClickListener { marker -> map.setOnMarkerClickListener { marker ->
val map = this@MapFragment.map ?: return@setOnMarkerClickListener false
when (marker) { when (marker) {
in markers -> { in markers -> {
vm.chargerSparse.value = markers[marker] vm.chargerSparse.value = markers[marker]
@@ -1135,6 +1186,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
) )
true true
} }
searchResultMarker -> true
else -> false else -> false
} }
@@ -1142,6 +1194,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.setOnMapClickListener { map.setOnMapClickListener {
if (backPressedCallback.isEnabled) { if (backPressedCallback.isEnabled) {
backPressedCallback.handleOnBackPressed() backPressedCallback.handleOnBackPressed()
} else if (bottomSheetBackPressedCallback.isEnabled) {
bottomSheetBackPressedCallback.handleOnBackPressed()
} }
} }
map.setMapType(vm.mapType.value) map.setMapType(vm.mapType.value)
@@ -1196,10 +1250,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
// show charger detail after chargers were loaded // show charger detail after chargers were loaded
vm.chargepoints.observe( vm.chargepoints.observe(
viewLifecycleOwner, viewLifecycleOwner,
object : Observer<Resource<List<ChargepointListItem>>> { object : Observer<Resource<ChargepointList>> {
override fun onChanged(value: Resource<List<ChargepointListItem>>) { override fun onChanged(value: Resource<ChargepointList>) {
if (value.data == null) return if (value.data == null) return
for (item in value.data) { for (item in value.data.items) {
if (item is ChargeLocation && item.id == chargerId) { if (item is ChargeLocation && item.id == chargerId) {
vm.chargerSparse.value = item vm.chargerSparse.value = item
vm.chargepoints.removeObserver(this) vm.chargepoints.removeObserver(this)
@@ -1219,9 +1273,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.search.requestFocus() binding.search.requestFocus()
binding.search.setSelection(locationName.length) binding.search.setSelection(locationName.length)
} }
if (context.checkAnyLocationPermission() && prefs.currentMapMyLocationEnabled) { if (context.checkAnyLocationPermission()) {
enableLocation(!positionSet, false) if (prefs.currentMapMyLocationEnabled && !positionSet) {
positionSet = true enableLocation(true, false)
positionSet = true
} else {
enableLocation(false, false)
}
} }
if (!positionSet) { if (!positionSet) {
// use position saved in preferences, fall back to default (Europe) // use position saved in preferences, fall back to default (Europe)
@@ -1526,10 +1584,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
else -> false else -> false
} }
override fun getRootView(): View {
return binding.root
}
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION]) @RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
private fun requestLocationUpdates() { private fun requestLocationUpdates() {
locationEngine.requestLocationUpdates( locationEngine.requestLocationUpdates(
@@ -1579,8 +1633,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
} }
} }
override fun onDestroy() { override fun onDestroyView() {
super.onDestroy() super.onDestroyView()
detailsDialog.onDestroy()
map = null
mapFragment = null
_binding = null
markers.clear()
clusterMarkers = emptyList()
searchResultMarker = null
searchResultIcon = null
/* if we don't dismiss the popup menu, it will be recreated in some cases /* if we don't dismiss the popup menu, it will be recreated in some cases
(split-screen mode) and then have references to a destroyed fragment. */ (split-screen mode) and then have references to a destroyed fragment. */
popupMenu?.dismiss() popupMenu?.dismiss()

View File

@@ -6,8 +6,6 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -15,15 +13,21 @@ import android.view.animation.DecelerateInterpolator
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import net.vonforst.evmap.R import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.* import net.vonforst.evmap.databinding.FragmentOnboardingAndroidAutoBinding
import net.vonforst.evmap.databinding.FragmentOnboardingBinding
import net.vonforst.evmap.databinding.FragmentOnboardingDataSourceBinding
import net.vonforst.evmap.databinding.FragmentOnboardingIconsBinding
import net.vonforst.evmap.databinding.FragmentOnboardingWelcomeBinding
import net.vonforst.evmap.model.FILTERS_DISABLED import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.navigation.safeNavigate import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.waitForLayout
class OnboardingFragment : Fragment() { class OnboardingFragment : Fragment() {
private lateinit var binding: FragmentOnboardingBinding private lateinit var binding: FragmentOnboardingBinding
@@ -59,7 +63,6 @@ class OnboardingFragment : Fragment() {
} }
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
binding.pageIndicatorView.selection = position
binding.forward?.visibility = binding.forward?.visibility =
if (position == adapter.itemCount - 1) View.INVISIBLE else View.VISIBLE if (position == adapter.itemCount - 1) View.INVISIBLE else View.VISIBLE
binding.backward?.visibility = if (position == 0) View.INVISIBLE else View.VISIBLE binding.backward?.visibility = if (position == 0) View.INVISIBLE else View.VISIBLE
@@ -76,9 +79,13 @@ class OnboardingFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (prefs.welcomeDialogShown) { binding.root.waitForLayout {
// skip to last page for selecting data source or accepting the privacy policy binding.viewPager.currentItem = if (prefs.welcomeDialogShown) {
binding.viewPager.currentItem = adapter.itemCount - 1 // skip to last page for selecting data source or accepting the privacy policy
adapter.itemCount - 1
} else {
0
}
} }
} }
@@ -234,7 +241,7 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
), HtmlCompat.FROM_HTML_MODE_LEGACY ), HtmlCompat.FROM_HTML_MODE_LEGACY
) )
binding.cbAcceptPrivacy.linksClickable = true binding.cbAcceptPrivacy.linksClickable = true
binding.cbAcceptPrivacy.movementMethod = LinkMovementMethod.getInstance() binding.cbAcceptPrivacy.movementMethod = LinkMovementMethodCompat.getInstance()
binding.btnGetStarted.visibility = View.INVISIBLE binding.btnGetStarted.visibility = View.INVISIBLE
for (rb in listOf( for (rb in listOf(

View File

@@ -78,22 +78,25 @@ class AboutFragment : PreferenceFragmentCompat() {
} }
"website" -> { "website" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.website_url)) (activity as? MapsActivity)?.openUrl(getString(R.string.website_url), requireView())
true true
} }
"github_link" -> { "github_link" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.github_link)) (activity as? MapsActivity)?.openUrl(getString(R.string.github_link), requireView())
true true
} }
"privacy" -> { "privacy" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.privacy_link)) (activity as? MapsActivity)?.openUrl(
getString(R.string.privacy_link),
requireView()
)
true true
} }
"faq" -> { "faq" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.faq_link)) (activity as? MapsActivity)?.openUrl(getString(R.string.faq_link), requireView())
true true
} }
"oss_licenses" -> { "oss_licenses" -> {
@@ -115,12 +118,29 @@ class AboutFragment : PreferenceFragmentCompat() {
findNavController().safeNavigate(AboutFragmentDirections.actionAboutToGithubSponsors()) findNavController().safeNavigate(AboutFragmentDirections.actionAboutToGithubSponsors())
true true
} }
"mastodon" -> {
(activity as? MapsActivity)?.openUrl(
getString(R.string.mastodon_url),
requireView()
)
true
}
"twitter" -> { "twitter" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.twitter_url)) (activity as? MapsActivity)?.openUrl(getString(R.string.twitter_url), requireView())
true true
} }
"goingelectric" -> { "goingelectric" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.goingelectric_forum_url)) (activity as? MapsActivity)?.openUrl(
getString(R.string.goingelectric_forum_url),
requireView()
)
true
}
"tffforum" -> {
(activity as? MapsActivity)?.openUrl(
getString(R.string.tff_forum_url),
requireView()
)
true true
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)

View File

@@ -8,7 +8,9 @@ import android.text.style.RelativeSizeSpan
import android.view.View import android.view.View
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import net.vonforst.evmap.R import net.vonforst.evmap.R
import net.vonforst.evmap.currencyDisplayName
import net.vonforst.evmap.ui.MultiSelectDialogPreference import net.vonforst.evmap.ui.MultiSelectDialogPreference
import net.vonforst.evmap.viewmodel.SettingsViewModel import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -73,6 +75,11 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
} }
} }
updateNativeIntegrationState() updateNativeIntegrationState()
val currencyPreference = findPreference<ListPreference>("chargeprice_currency")!!
currencyPreference.entries = currencyPreference.entryValues.map {
currencyDisplayName(it.toString()).replaceFirstChar { it.uppercase() }
}.toTypedArray()
} }
private fun updateNativeIntegrationState() { private fun updateNativeIntegrationState() {

View File

@@ -9,9 +9,11 @@ import android.provider.Settings
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import com.github.erfansn.localeconfigx.configuredLocales
import net.vonforst.evmap.R import net.vonforst.evmap.R
import net.vonforst.evmap.isAppInstalled import net.vonforst.evmap.isAppInstalled
import net.vonforst.evmap.ui.getAppLocale import net.vonforst.evmap.ui.getAppLocale
import net.vonforst.evmap.ui.map
import net.vonforst.evmap.ui.updateAppLocale import net.vonforst.evmap.ui.updateAppLocale
import net.vonforst.evmap.ui.updateNightMode import net.vonforst.evmap.ui.updateNightMode
@@ -23,11 +25,7 @@ class UiSettingsFragment : BaseSettingsFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_ui, rootKey) setPreferencesFromResource(R.xml.settings_ui, rootKey)
langPref = findPreference("language")!! setupLangPref()
langPref.setOnPreferenceChangeListener { _, newValue ->
updateAppLocale(newValue as String)
true
}
val appLinkPref = findPreference<Preference>("applink_associate")!! val appLinkPref = findPreference<Preference>("applink_associate")!!
appLinkPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S appLinkPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
@@ -36,6 +34,31 @@ class UiSettingsFragment : BaseSettingsFragment() {
immediateNavPref.isVisible = isGoogleMapsInstalled() immediateNavPref.isVisible = isGoogleMapsInstalled()
} }
private fun setupLangPref() {
langPref = findPreference("language")!!
val configuredLocales = requireContext().configuredLocales
val numLocalesByLang = configuredLocales.map { it.language }.groupingBy { it }.eachCount()
val localeNames = configuredLocales.map {
val name = if (numLocalesByLang[it.language]!! > 1) {
it.getDisplayName(it)
} else {
it.getDisplayLanguage(it)
}
name.replaceFirstChar { c -> c.uppercase(it) }
}
val localeTags = configuredLocales.map { it.toLanguageTag() }
langPref.entries =
(listOf(getString(R.string.pref_language_device_default)) + localeNames).toTypedArray()
langPref.entryValues =
(listOf("default") + localeTags).toTypedArray()
langPref.setOnPreferenceChangeListener { _, newValue ->
updateAppLocale(newValue as String)
true
}
}
private fun isGoogleMapsInstalled() = private fun isGoogleMapsInstalled() =
requireContext().packageManager.isAppInstalled("com.google.android.apps.maps") requireContext().packageManager.isAppInstalled("com.google.android.apps.maps")

View File

@@ -44,7 +44,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
try { try {
return locationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER) return locationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER)
} catch (e: SecurityException) { } catch (e: SecurityException) {
Log.e(TAG, "Permissions not granted for fused provider", e) Log.w(TAG, "Permissions not granted for fused provider", e)
} }
} }
@@ -68,7 +68,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
} }
} }
} catch (e: SecurityException) { } catch (e: SecurityException) {
Log.e(TAG, "Permissions not granted for provider: $provider", e) Log.w(TAG, "Permissions not granted for provider: $provider", e)
} }
} }
return bestLocation return bestLocation
@@ -103,7 +103,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
enableFused(gpsInterval) enableFused(gpsInterval)
checkLastKnownFused() checkLastKnownFused()
} catch (e: SecurityException) { } catch (e: SecurityException) {
Log.e(TAG, "Permissions not granted for fused provider", e) Log.w(TAG, "Permissions not granted for fused provider", e)
} }
} }
@@ -159,7 +159,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
looper looper
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for GPS updates.", e) Log.w(TAG, "Unable to register for GPS updates.", e)
} }
} }
@@ -174,7 +174,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
looper looper
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for network updates.", e) Log.w(TAG, "Unable to register for network updates.", e)
} }
} }
@@ -189,7 +189,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
looper looper
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for passive updates.", e) Log.w(TAG, "Unable to register for passive updates.", e)
} }
} }
@@ -205,7 +205,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
looper looper
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for passive updates.", e) Log.w(TAG, "Unable to register for passive updates.", e)
} }
} }

View File

@@ -18,6 +18,7 @@ import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
sealed class ChargepointListItem sealed class ChargepointListItem
@@ -140,9 +141,9 @@ data class ChargeLocation(
val totalChargepoints: Int val totalChargepoints: Int
get() = chargepoints.sumOf { it.count } get() = chargepoints.sumOf { it.count }
fun formatChargepoints(sp: StringProvider): String { fun formatChargepoints(sp: StringProvider, locale: Locale): String {
return chargepointsMerged.joinToString(" · ") { return chargepointsMerged.joinToString(" · ") {
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower()}" "${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower(locale)}"
} }
} }
} }
@@ -413,12 +414,12 @@ data class Chargepoint(
* If chargepoint power is defined, format it into a string. * If chargepoint power is defined, format it into a string.
* Otherwise, return null. * Otherwise, return null.
*/ */
fun formatPower(): String? { fun formatPower(locale: Locale): String? {
if (power == null) return null if (power == null) return null
val powerFmt = if (abs(power - power.toInt()) < 0.1) { val powerFmt = if (abs(power - power.toInt()) < 0.1) {
"%.0f".format(power) "%.0f".format(locale, power)
} else { } else {
"%.1f".format(power) "%.1f".format(locale, power)
} }
return "$powerFmt kW" return "$powerFmt kW"
} }

View File

@@ -19,6 +19,8 @@ import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.model.* import net.vonforst.evmap.model.*
import net.vonforst.evmap.ui.cluster import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.crossesAntimeridian
import net.vonforst.evmap.utils.splitAtAntimeridian
import net.vonforst.evmap.viewmodel.Resource import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.Status import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.await import net.vonforst.evmap.viewmodel.await
@@ -144,7 +146,15 @@ class ChargeLocationsRepository(
zoom: Float, zoom: Float,
filters: FilterValues?, filters: FilterValues?,
overrideCache: Boolean = false overrideCache: Boolean = false
): LiveData<Resource<List<ChargepointListItem>>> { ): LiveData<Resource<ChargepointList>> {
if (bounds.crossesAntimeridian()) {
val (a, b) = bounds.splitAtAntimeridian()
val liveDataA = getChargepoints(a, zoom, filters, overrideCache)
val liveDataB = getChargepoints(b, zoom, filters, overrideCache)
return combineLiveData(liveDataA, liveDataB)
}
val api = api.value!! val api = api.value!!
val dbResult = if (filters == null) { val dbResult = if (filters == null) {
@@ -158,7 +168,7 @@ class ChargeLocationsRepository(
) )
} else { } else {
queryWithFilters(api, filters, bounds) queryWithFilters(api, filters, bounds)
}.map { applyLocalClustering(it, zoom) } }.map { ChargepointList(applyLocalClustering(it, zoom), true) }
val filtersSerialized = val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() } filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize() ?.serialize()
@@ -208,12 +218,41 @@ class ChargeLocationsRepository(
} }
} }
private fun combineLiveData(
liveDataA: LiveData<Resource<ChargepointList>>,
liveDataB: LiveData<Resource<ChargepointList>>
) = MediatorLiveData<Resource<ChargepointList>>().apply {
listOf(liveDataA, liveDataB).forEach {
addSource(it) {
val valA = liveDataA.value
val valB = liveDataB.value
val combinedList = if (valA?.data != null && valB?.data != null) {
ChargepointList(
valA.data.items + valB.data.items,
valA.data.isComplete && valB.data.isComplete
)
} else if (valA?.data != null) {
ChargepointList(valA.data.items, false)
} else if (valB?.data != null) {
ChargepointList(valB.data.items, false)
} else null
if (valA?.status == Status.SUCCESS && valB?.status == Status.SUCCESS) {
Resource.success(combinedList)
} else if (valA?.status == Status.ERROR || valB?.status == Status.ERROR) {
Resource.error(valA?.message ?: valB?.message, combinedList)
} else {
Resource.loading(combinedList)
}
}
}
}
fun getChargepointsRadius( fun getChargepointsRadius(
location: LatLng, location: LatLng,
radius: Int, radius: Int,
zoom: Float, zoom: Float,
filters: FilterValues? filters: FilterValues?
): LiveData<Resource<List<ChargepointListItem>>> { ): LiveData<Resource<ChargepointList>> {
val api = api.value!! val api = api.value!!
val radiusMeters = radius.toDouble() * 1000 val radiusMeters = radius.toDouble() * 1000
@@ -227,7 +266,7 @@ class ChargeLocationsRepository(
) )
} else { } else {
queryWithFilters(api, filters, location, radiusMeters) queryWithFilters(api, filters, location, radiusMeters)
}.map { applyLocalClustering(it, zoom) } }.map { ChargepointList(applyLocalClustering(it, zoom), true) }
val filtersSerialized = val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() } filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize() ?.serialize()
@@ -277,18 +316,18 @@ class ChargeLocationsRepository(
private fun applyLocalClustering( private fun applyLocalClustering(
result: Resource<ChargepointList>, result: Resource<ChargepointList>,
zoom: Float zoom: Float
): Resource<List<ChargepointListItem>> { ): Resource<ChargepointList> {
val list = result.data ?: return Resource(result.status, null, result.message) val list = result.data ?: return Resource(result.status, null, result.message)
val chargers = list.items.filterIsInstance<ChargeLocation>() val chargers = list.items.filterIsInstance<ChargeLocation>()
if (chargers.size != list.items.size) return Resource( if (chargers.size != list.items.size) return Resource(
result.status, result.status,
list.items, list,
result.message result.message
) // list already contains clusters ) // list already contains clusters
val clustered = applyLocalClustering(chargers, zoom) val clustered = applyLocalClustering(chargers, zoom)
return Resource(result.status, clustered, result.message) return Resource(result.status, ChargepointList(clustered, list.isComplete), result.message)
} }
private fun applyLocalClustering( private fun applyLocalClustering(

View File

@@ -117,6 +117,16 @@ class Converters {
return stringSetAdapter.fromJson(value) return stringSetAdapter.fromJson(value)
} }
@TypeConverter
fun fromStringMutableSet(value: MutableSet<String>?): String {
return stringSetAdapter.toJson(value)
}
@TypeConverter
fun toStringMutableSet(value: String): MutableSet<String>? {
return stringSetAdapter.fromJson(value)?.toMutableSet()
}
@TypeConverter @TypeConverter
fun fromStringList(value: List<String>?): String { fun fromStringList(value: List<String>?): String {
return stringListAdapter.toJson(value) return stringListAdapter.toJson(value)

View File

@@ -6,8 +6,9 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.text.SpannableString
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.widget.ImageView import android.widget.ImageView
@@ -215,21 +216,25 @@ fun setTopMargin(view: View, topMargin: Float) {
/** /**
* Linkify is already possible using the autoLink and linksClickable attributes, but this does not * Linkify is already possible using the autoLink and linksClickable attributes, but this does not
* remove spans correctly. So we implement a new version that manually removes the spans. * remove spans correctly after autoLink is set to false.
* So we implement a new version that manually uses Linkify to create links if necessary.
*/ */
@BindingAdapter("linkify") @BindingAdapter(value = ["linkify", "android:text"])
fun setLinkify(textView: TextView, oldValue: Int, newValue: Int) { fun setLinkify(
if (oldValue == newValue) return textView: TextView,
oldLinkify: Int,
oldText: CharSequence?,
newLinkify: Int,
newText: CharSequence?
) {
if (oldLinkify == newLinkify && oldText == newText) return
textView.autoLinkMask = newValue textView.text = newText
textView.linksClickable = newValue != 0 if (newLinkify != 0) {
Linkify.addLinks(textView, newLinkify)
// remove spans textView.movementMethod = LinkMovementMethod.getInstance()
val text = textView.text } else {
if (newValue == 0 && text != null && text is SpannableString) { textView.movementMethod = null
text.getSpans(0, text.length, Any::class.java).forEach {
text.removeSpan(it)
}
} }
} }

View File

@@ -3,8 +3,9 @@ package net.vonforst.evmap.ui
import android.content.Context import android.content.Context
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import net.vonforst.evmap.R import com.github.erfansn.localeconfigx.configuredLocales
import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.Locale
fun updateNightMode(prefs: PreferenceDataSource) { fun updateNightMode(prefs: PreferenceDataSource) {
@@ -33,8 +34,11 @@ fun getAppLocale(context: Context): String? {
"default" "default"
} else { } else {
val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() } val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() }
val choices = val choices = context.configuredLocales
context.resources.getStringArray(R.array.pref_language_values).joinToString(",") choices.getFirstMatch(arr)?.toLanguageTag()
LocaleListCompat.forLanguageTags(choices).getFirstMatch(arr)?.toLanguageTag()
} }
} }
inline fun <R> LocaleListCompat.map(transform: (Locale) -> R): List<R> = List(size()) {
transform(get(it)!!)
}

View File

@@ -1,112 +0,0 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.widget.NestedScrollView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
class HideOnExpandFabBehavior(context: Context, attrs: AttributeSet) :
FloatingActionButton.Behavior(context, attrs) {
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(
coordinatorLayout,
child,
directTargetChild,
target,
axes,
type
)
}
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: FloatingActionButton,
dependency: View
): Boolean {
if (dependency is NestedScrollView) {
try {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
behavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
onDependentViewChanged(parent, child, dependency)
}
})
return true
} catch (e: IllegalArgumentException) {
}
}
return false
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: FloatingActionButton,
dependency: View
): Boolean {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
when (behavior.state) {
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> {
}
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
if (child.tag as? Boolean != false) child.show()
}
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED -> {
if (child.tag as? Boolean != false) child.show()
}
else -> {
child.hide()
}
}
return false
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
super.onNestedScroll(
coordinatorLayout,
child,
target,
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
type,
consumed
)
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
child.hide()
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
child.show()
}
}
}

View File

@@ -6,8 +6,8 @@ import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) : class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
@@ -45,9 +45,9 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
): Boolean { ): Boolean {
if (dependency is NestedScrollView) { if (dependency is NestedScrollView) {
try { try {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency) val behavior = BottomSheetBehavior.from<View>(dependency)
behavior.addBottomSheetCallback(object : behavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() { BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) { override fun onSlide(bottomSheet: View, slideOffset: Float) {
} }
@@ -68,12 +68,13 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
child: FloatingActionButton, child: FloatingActionButton,
dependency: View dependency: View
): Boolean { ): Boolean {
val behavior = BottomSheetBehaviorGoogleMapsLike.from(dependency) val behavior = BottomSheetBehavior.from(dependency)
when (behavior.state) { when (behavior.state) {
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> { BottomSheetBehavior.STATE_SETTLING -> {
} }
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
BottomSheetBehavior.STATE_HIDDEN -> {
if (!hidden) child.show() if (!hidden) child.show()
} }
else -> { else -> {

View File

@@ -5,12 +5,20 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.text.BidiFormatter
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.car2go.maps.model.LatLng import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds import com.car2go.maps.model.LatLngBounds
import net.vonforst.evmap.model.Coordinate import net.vonforst.evmap.model.Coordinate
import java.util.* import java.util.Locale
import kotlin.math.* import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
/** /**
* Adds a certain distance in meters to a location. Approximate calculation. * Adds a certain distance in meters to a location. Approximate calculation.
@@ -147,9 +155,32 @@ private fun dms(value: Double, lon: Boolean): String {
} }
fun Coordinate.formatDecimal(accuracy: Int = 6): String { fun Coordinate.formatDecimal(accuracy: Int = 6): String {
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, lat, lng) return BidiFormatter.getInstance()
.unicodeWrap("%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, lat, lng))
} }
fun Location.formatDecimal(accuracy: Int = 6): String { fun Location.formatDecimal(accuracy: Int = 6): String {
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, latitude, longitude) return BidiFormatter.getInstance()
.unicodeWrap("%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, latitude, longitude))
}
fun LatLngBounds.normalize() = LatLngBounds(
LatLng(southwest.latitude, normalizeLongitude(southwest.longitude)),
LatLng(northeast.latitude, normalizeLongitude(northeast.longitude)),
)
private fun normalizeLongitude(long: Double) =
if (-180.0 <= long && long <= 180.0) long else (long + 180) % 360 - 180
fun LatLngBounds.crossesAntimeridian() = southwest.longitude > 0 && northeast.longitude < 0
fun LatLngBounds.splitAtAntimeridian(): Pair<LatLngBounds, LatLngBounds> {
if (!crossesAntimeridian()) throw IllegalArgumentException("does not cross antimeridian")
return LatLngBounds(
LatLng(southwest.latitude, southwest.longitude),
LatLng(northeast.latitude, 180.0),
) to LatLngBounds(
LatLng(southwest.latitude, -180.0),
LatLng(northeast.latitude, northeast.longitude),
)
} }

View File

@@ -1,14 +1,29 @@
package net.vonforst.evmap.viewmodel package net.vonforst.evmap.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.* import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.viewModelScope
import jsonapi.Meta import jsonapi.Meta
import jsonapi.Relationship import jsonapi.Relationship
import jsonapi.Relationships import jsonapi.Relationships
import jsonapi.ResourceIdentifier import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
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.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint import net.vonforst.evmap.model.Chargepoint
@@ -298,4 +313,8 @@ class ChargepriceViewModel(
} }
} }
} }
fun resetBatteryRangeToDefault() {
batteryRange.value = prefs.chargepriceBatteryRangeAndroidAuto
}
} }

View File

@@ -1,30 +1,49 @@
package net.vonforst.evmap.viewmodel package net.vonforst.evmap.viewmodel
import android.app.Application import android.app.Application
import android.graphics.Point
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.* import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import com.car2go.maps.AnyMap import com.car2go.maps.AnyMap
import com.car2go.maps.Projection
import com.car2go.maps.model.LatLng import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds import com.car2go.maps.model.LatLngBounds
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.availability.AvailabilityRepository import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.tesla.Pricing import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.createApi import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.fronyx.PredictionData import net.vonforst.evmap.api.fronyx.PredictionData
import net.vonforst.evmap.api.fronyx.PredictionRepository
import net.vonforst.evmap.api.goingelectric.GEChargepoint import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.openchargemap.OCMConnection import net.vonforst.evmap.api.openchargemap.OCMConnection
import net.vonforst.evmap.api.openchargemap.OCMReferenceData import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.stringProvider import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.autocomplete.PlaceWithBounds import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.model.* import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.getMultipleChoiceValue
import net.vonforst.evmap.model.getSliderValue
import net.vonforst.evmap.storage.AppDatabase import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
@@ -32,7 +51,8 @@ import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.cluster import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.distanceBetween import net.vonforst.evmap.utils.distanceBetween
import kotlin.math.roundToInt import net.vonforst.evmap.utils.normalize
import kotlin.math.cos
@Parcelize @Parcelize
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
@@ -58,7 +78,6 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
prefs prefs
) )
private val availabilityRepo = AvailabilityRepository(application) private val availabilityRepo = AvailabilityRepository(application)
var mapProjection: Projection? = null
val apiId = repo.api.map { it.id } val apiId = repo.api.map { it.id }
@@ -76,12 +95,13 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val bottomSheetExpanded = MediatorLiveData<Boolean>().apply { val bottomSheetExpanded = MediatorLiveData<Boolean>().apply {
addSource(bottomSheetState) { addSource(bottomSheetState) {
when (it) { when (it) {
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED, STATE_COLLAPSED,
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> { STATE_HIDDEN -> {
value = false value = false
} }
BottomSheetBehaviorGoogleMapsLike.STATE_EXPANDED,
BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT -> { STATE_EXPANDED,
STATE_HALF_EXPANDED -> {
value = true value = true
} }
} }
@@ -125,10 +145,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
} }
} }
} }
val chargepoints: MediatorLiveData<Resource<List<ChargepointListItem>>> by lazy { val chargepoints: MediatorLiveData<Resource<ChargepointList>> by lazy {
MediatorLiveData<Resource<List<ChargepointListItem>>>() MediatorLiveData<Resource<ChargepointList>>()
.apply { .apply {
value = Resource.loading(emptyList()) value = Resource.loading(ChargepointList(emptyList(), false))
// this is not automatically updated with mapPosition, as we only want to update // this is not automatically updated with mapPosition, as we only want to update
// when map is idle. // when map is idle.
listOf(filtersWithValue, repo.api).forEach { listOf(filtersWithValue, repo.api).forEach {
@@ -247,13 +267,14 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
it.data?.extraData as? Pricing it.data?.extraData as? Pricing
} }
private val predictionRepository = PredictionRepository(application) //private val predictionRepository = PredictionRepository(application)
val predictionData: LiveData<PredictionData> = availability.switchMap { av -> val predictionData: LiveData<PredictionData> = availability.switchMap { av ->
liveData { /*liveData {
val charger = charger.value?.data ?: return@liveData val charger = charger.value?.data ?: return@liveData
emit(predictionRepository.getPredictionData(charger, av.data, filteredConnectors.value)) emit(predictionRepository.getPredictionData(charger, av.data, filteredConnectors.value))
} }*/
MutableLiveData()
} }
val myLocationEnabled: MutableLiveData<Boolean> by lazy { val myLocationEnabled: MutableLiveData<Boolean> by lazy {
@@ -282,6 +303,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
} }
} }
val mapTrafficSupported: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = false
}
}
val mapTrafficEnabled: MutableLiveData<Boolean> by lazy { val mapTrafficEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply { MutableLiveData<Boolean>().apply {
value = prefs.mapTrafficEnabled value = prefs.mapTrafficEnabled
@@ -368,7 +395,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
} }
}.distinctUntilChanged() }.distinctUntilChanged()
private var chargepointsInternal: LiveData<Resource<List<ChargepointListItem>>>? = null private var chargepointsInternal: LiveData<Resource<ChargepointList>>? = null
private var chargepointLoader = private var chargepointLoader =
throttleLatest( throttleLatest(
500L, 500L,
@@ -395,13 +422,13 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
filteredConnectors.value = null filteredConnectors.value = null
filteredMinPower.value = null filteredMinPower.value = null
filteredChargeCards.value = null filteredChargeCards.value = null
chargepoints.value = Resource.success(chargersClustered) chargepoints.value = Resource.success(ChargepointList(chargersClustered, true))
return@throttleLatest return@throttleLatest
} }
val result = repo.getChargepoints(bounds, mapPosition.zoom, filters, overrideCache) val result = repo.getChargepoints(bounds, mapPosition.zoom, filters, overrideCache)
chargepointsInternal?.let { chargepoints.removeSource(it) } chargepointsInternal?.let { chargepoints.removeSource(it) }
chargepointsInternal = result chargepointsInternal
chargepoints.addSource(result) { chargepoints.addSource(result) {
val apiId = apiId.value val apiId = apiId.value
when (apiId) { when (apiId) {
@@ -446,14 +473,26 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
* expands LatLngBounds beyond the viewport (1.5x the width and height) * expands LatLngBounds beyond the viewport (1.5x the width and height)
*/ */
private fun extendBounds(bounds: LatLngBounds): LatLngBounds { private fun extendBounds(bounds: LatLngBounds): LatLngBounds {
val mapProjection = mapProjection ?: return bounds val sw = bounds.southwest
val swPoint = mapProjection.toScreenLocation(bounds.southwest) val ne = bounds.northeast
val nePoint = mapProjection.toScreenLocation(bounds.northeast)
val dx = ((nePoint.x - swPoint.x) * 0.25).roundToInt() // do not expand bounds if the map area shown is very large
val dy = ((nePoint.y - swPoint.y) * 0.25).roundToInt() val expansion = if (ne.longitude - sw.longitude > 10) 1.0 else 1.5
val newSw = mapProjection.fromScreenLocation(Point(swPoint.x - dx, swPoint.y - dy)) val factor = (expansion - 1.0) * 0.5
val newNe = mapProjection.fromScreenLocation(Point(nePoint.x + dx, nePoint.y + dy))
return LatLngBounds(newSw, newNe) var west = sw.longitude - (ne.longitude - sw.longitude) * factor
var east = ne.longitude + (ne.longitude - sw.longitude) * factor
val south =
sw.latitude - (ne.latitude - sw.latitude) * factor * cos(Math.toRadians(sw.latitude))
val north =
ne.latitude + (ne.latitude - sw.latitude) * factor * cos(Math.toRadians(ne.latitude))
if (east - west >= 360) {
west = -180.0
east = 180.0
}
return LatLngBounds(LatLng(south, west), LatLng(north, east)).normalize()
} }
fun reloadAvailability() { fun reloadAvailability() {

View File

@@ -32,6 +32,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
app:piv_rtl_mode="auto"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/card" app:layout_constraintTop_toBottomOf="@+id/card"

View File

@@ -59,8 +59,6 @@
app:layout_constraintBottom_toTopOf="@+id/welcomeTitle" app:layout_constraintBottom_toTopOf="@+id/welcomeTitle"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.7"
app:srcCompat="@drawable/android_auto" /> app:srcCompat="@drawable/android_auto" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -19,6 +19,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/app_name" android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Material3.HeadlineLarge" android:textAppearance="@style/TextAppearance.Material3.HeadlineLarge"
android:textAlignment="viewStart"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"

View File

@@ -6,7 +6,7 @@
<RadioButton <RadioButton
android:id="@+id/rbGoingElectric" android:id="@+id/rbGoingElectric"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/data_source_goingelectric" android:text="@string/data_source_goingelectric"
android:textColor="#098ac7" android:textColor="#098ac7"
@@ -21,6 +21,7 @@
android:layout_marginTop="-8dp" android:layout_marginTop="-8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:textAlignment="viewStart"
android:text="@string/data_source_goingelectric_desc" /> android:text="@string/data_source_goingelectric_desc" />
<RadioButton <RadioButton
@@ -39,6 +40,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="-8dp" android:layout_marginTop="-8dp"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:textAlignment="viewStart"
android:text="@string/data_source_openchargemap_desc" /> android:text="@string/data_source_openchargemap_desc" />
</RadioGroup> </RadioGroup>

View File

@@ -7,6 +7,8 @@
<import type="java.util.Map" /> <import type="java.util.Map" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="java.time.ZonedDateTime" /> <import type="java.time.ZonedDateTime" />
<import type="net.vonforst.evmap.model.ChargeLocation" /> <import type="net.vonforst.evmap.model.ChargeLocation" />
@@ -90,495 +92,517 @@
android:paddingBottom="@dimen/detail_corner_radius" android:paddingBottom="@dimen/detail_corner_radius"
app:cardElevation="6dp"> app:cardElevation="6dp">
<androidx.constraintlayout.widget.ConstraintLayout <com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/dragHandle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:colorBackground" android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"
android:paddingTop="8dp" android:background="@null"
android:paddingBottom="16dp"> app:liftOnScroll="true">
<TextView <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/txtName" android:id="@+id/toolbar"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" app:layout_scrollFlags="scroll|exitUntilCollapsed">
android:hyphenationFrequency="normal"
android:maxLines="@{expanded ? 3 : 1}"
android:text="@{charger.data.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/imgFaultReport"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
<TextView <TextView
android:id="@+id/textView2" android:id="@+id/txtName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{charger.data.address.toString()}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnless="@{charger.data.address != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtName"
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/txtDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:minWidth="50dp"
android:text="@{BindingAdaptersKt.distance(distance, context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
tools:text="10 km" />
<TextView
android:id="@+id/txtAvailability"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="72dp"
android:background="@drawable/rounded_rect"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(filteredAvailability.data.status.values())), filteredAvailability.data.totalChargepoints)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/txtName"
tools:backgroundTint="@color/available"
tools:text="2/2" />
<TextView
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{charger.data.formatChargepoints(ChargepointApiKt.stringProvider(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/connectors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:data="@{DataBindingAdaptersKt.chargepointWithAvailability(charger.data.chargepointsMerged, availability.data.status)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView7"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
tools:orientation="horizontal" />
<TextView
android:id="@+id/textView7"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/connectors"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
<TextView
android:id="@+id/textView12"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/amenities"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/details" />
<TextView
android:id="@+id/textView11"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.amenities}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView12"
tools:text="Toilet" />
<TextView
android:id="@+id/textView10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/general_info"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView11" />
<TextView
android:id="@+id/textView4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.generalInformation}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/textView10"
tools:text="Only for guests" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, teslaPricing, context)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider3"
tools:itemCount="3"
tools:listitem="@layout/item_detail" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<Button
android:id="@+id/sourceButton"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@{@string/source(apiName)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView4"
tools:text="Source: DataSource" />
<TextView
android:id="@+id/textView13"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="right|end"
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : availability.message == &quot;not signed in&quot; ? @string/realtime_data_login_needed : @string/realtime_data_unavailable}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/btnLogin"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/connectors"
tools:text="Echtzeitdaten nicht verfügbar" />
<View
android:id="@+id/topPart"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="-10dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/txtName" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@+id/textView13" />
<View
android:id="@+id/divider3"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:layout_constraintTop_toBottomOf="@+id/buttonsScroller" />
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{predictionData.isPercentage ? @string/average_utilization : @string/utilization_prediction}"
tools:text="@string/utilization_prediction"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider2" />
<TextView
android:id="@+id/textView29"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@{predictionData.description}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
app:layout_constraintStart_toEndOf="@+id/textView8"
tools:text="(DC plugs only)" />
<Button
android:id="@+id/btnPredictionHelp"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/help"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:icon="@drawable/ic_help"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView8" />
<net.vonforst.evmap.ui.BarGraphView
android:id="@+id/prediction"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
app:data="@{predictionData.predictionGraph}"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView8"
app:maxValue="@{predictionData.maxValue}"
app:isPercentage="@{predictionData.isPercentage}"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
tools:orientation="horizontal" />
<ImageView
android:id="@+id/imgPredictionSource"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:adjustViewBounds="true"
android:background="?selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/prediction"
app:srcCompat="@drawable/ic_powered_by_fronyx"
app:tint="@color/logo_tint_night" />
<View
android:id="@+id/divider1"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintTop_toBottomOf="@+id/imgPredictionSource" />
<ImageView
android:id="@+id/imgVerified"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/verified"
app:goneUnless="@{ charger.data.verified }"
app:layout_constraintEnd_toStartOf="@+id/txtAvailability"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/imgFaultReport"
app:layout_constraintTop_toTopOf="@+id/txtName"
app:srcCompat="@drawable/ic_verified"
app:tint="@color/available"
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
tools:targetApi="o" />
<ImageView
android:id="@+id/imgFaultReport"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:contentDescription="@string/fault_report"
app:goneUnless="@{ charger.data.faultReport != null }"
app:layout_constraintEnd_toStartOf="@+id/imgVerified"
app:layout_constraintStart_toEndOf="@+id/txtName"
app:layout_constraintTop_toTopOf="@+id/txtName"
app:srcCompat="@drawable/ic_map_marker_fault"
app:tooltipTextCompat="@{@string/fault_report}"
tools:targetApi="o" />
<TextView
android:id="@+id/txtTimeRetrieved"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:breakStrategy="balanced"
android:text="@{@string/data_retrieved_at(DateUtils.getRelativeTimeSpanString(charger.data.timeRetrieved.toEpochMilli(), Instant.now().toEpochMilli(), 0))}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.timeRetrieved == null || Duration.between(charger.data.timeRetrieved, Instant.now()).compareTo(Duration.ofHours(1)) > 0}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/sourceButton"
tools:text="Data retrieved 4 hours ago" />
<TextView
android:id="@+id/txtLicense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:breakStrategy="balanced"
android:text="@{charger.data.license}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.license != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtTimeRetrieved"
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
<Button
android:id="@+id/btnRefreshLiveData"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/refresh_live_data"
android:enabled="@{availability.status != Status.LOADING}"
app:icon="@drawable/ic_refresh"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView7" />
<HorizontalScrollView
android:id="@+id/buttonsScroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider1"
app:layout_constrainedWidth="true"
android:fillViewport="true"
app:goneUnless="@{charger.data != null &amp;&amp; (ChargepriceApi.isChargerSupported(charger.data) || charger.data.chargerUrl != null)}">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnChargeprice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/go_to_chargeprice" android:ellipsize="end"
android:transitionName="@string/shared_element_chargeprice" android:hyphenationFrequency="normal"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}" android:maxLines="@{expanded ? 3 : 1}"
app:icon="@drawable/ic_chargeprice" /> android:text="@{charger.data.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
tools:text="Parkhaus" />
<ImageView
android:id="@+id/imgVerified"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:contentDescription="@string/verified"
app:goneUnless="@{ charger.data.verified }"
app:srcCompat="@drawable/ic_verified"
app:tint="@color/available"
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
tools:targetApi="o" />
<ImageView
android:id="@+id/imgFaultReport"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:contentDescription="@string/fault_report"
app:goneUnless="@{ charger.data.faultReport != null }"
app:srcCompat="@drawable/ic_map_marker_fault"
app:tooltipTextCompat="@{@string/fault_report}"
tools:targetApi="o" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical"
android:clipToPadding="false"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp">
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:text="@{charger.data.address.toString()}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnless="@{charger.data.address != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/txtDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:textAlignment="viewEnd"
android:maxLines="1"
android:minWidth="50dp"
android:text="@{BindingAdaptersKt.distance(distance, context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
tools:text="10 km" />
<TextView
android:id="@+id/txtAvailability"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="72dp"
android:background="@drawable/rounded_rect"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:padding="2dp"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(filteredAvailability.data.status.values())), filteredAvailability.data.totalChargepoints)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent"
tools:backgroundTint="@color/available"
tools:text="2/2" />
<TextView
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:text="@{charger.data.formatChargepoints(ChargepointApiKt.stringProvider(context), LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/connectors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:data="@{DataBindingAdaptersKt.chargepointWithAvailability(charger.data.chargepointsMerged, availability.data.status)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView7"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
tools:orientation="horizontal" />
<TextView
android:id="@+id/textView7"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/connectors"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
<TextView
android:id="@+id/textView12"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/amenities"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/details" />
<TextView
android:id="@+id/textView11"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.amenities}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView12"
tools:text="Toilet" />
<TextView
android:id="@+id/textView10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/general_info"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView11" />
<TextView
android:id="@+id/textView4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.generalInformation}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/textView10"
tools:text="Only for guests" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, teslaPricing, context)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider3"
tools:itemCount="3"
tools:listitem="@layout/item_detail" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<Button <Button
android:id="@+id/btnChargerWebsite" android:id="@+id/sourceButton"
style="@style/Widget.Material3.Button.OutlinedButton" style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@{@string/source(apiName)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView4"
tools:text="Source: DataSource" />
<TextView
android:id="@+id/textView13"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="end"
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : availability.message == &quot;not signed in&quot; ? @string/realtime_data_login_needed : @string/realtime_data_unavailable}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/btnLogin"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/connectors"
tools:text="Echtzeitdaten nicht verfügbar" />
<View
android:id="@+id/topPart"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="-10dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@+id/textView13" />
<View
android:id="@+id/divider3"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:layout_constraintTop_toBottomOf="@+id/buttonsScroller" />
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{predictionData.isPercentage ? @string/average_utilization : @string/utilization_prediction}"
tools:text="@string/utilization_prediction"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider2" />
<TextView
android:id="@+id/textView29"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:text="@string/charger_website" android:layout_marginEnd="8dp"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargerUrl != null}" android:text="@{predictionData.description}"
app:icon="@drawable/ic_link" /> android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
app:layout_constraintStart_toEndOf="@+id/textView8"
tools:text="(DC plugs only)" />
</LinearLayout> <Button
</HorizontalScrollView> android:id="@+id/btnPredictionHelp"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/help"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:icon="@drawable/ic_help"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView8" />
<Button <net.vonforst.evmap.ui.BarGraphView
android:id="@+id/btnLogin" android:id="@+id/prediction"
style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="0dp"
android:layout_width="wrap_content" android:layout_height="100dp"
android:layout_height="40dp" android:layout_marginTop="8dp"
android:text="@string/login" app:data="@{predictionData.predictionGraph}"
app:goneUnless="@{availability.status == Status.ERROR &amp;&amp; availability.message == &quot;not signed in&quot;}" app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintBottom_toBottomOf="@+id/textView13" app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintEnd_toStartOf="@+id/guideline2" app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/textView13" /> app:layout_constraintTop_toBottomOf="@+id/textView8"
<com.google.android.material.card.MaterialCardView app:maxValue="@{predictionData.maxValue}"
style="?attr/materialCardViewElevatedStyle" app:isPercentage="@{predictionData.isPercentage}"
android:id="@+id/connector_details_card" tools:itemCount="3"
app:layout_constraintStart_toStartOf="@+id/guideline" tools:layoutManager="LinearLayoutManager"
app:layout_constraintEnd_toStartOf="@+id/guideline2" tools:listitem="@layout/item_connector"
app:layout_constraintTop_toTopOf="@id/connectors" tools:orientation="horizontal" />
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardCornerRadius="24dp"
android:layout_marginBottom="@dimen/detail_corner_radius_negative"
android:paddingBottom="@dimen/detail_corner_radius"
app:cardElevation="6dp"
android:visibility="gone">
<include <ImageView
layout="@layout/dialog_connector_details" android:id="@+id/imgPredictionSource"
android:id="@+id/connector_details" /> android:layout_width="wrap_content"
</com.google.android.material.card.MaterialCardView> android:layout_height="24dp"
android:layout_marginTop="4dp"
android:adjustViewBounds="true"
android:background="?selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/prediction"
app:srcCompat="@drawable/ic_powered_by_fronyx"
app:tint="@color/logo_tint_night" />
</androidx.constraintlayout.widget.ConstraintLayout> <View
android:id="@+id/divider1"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintTop_toBottomOf="@+id/imgPredictionSource" />
<TextView
android:id="@+id/txtTimeRetrieved"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:breakStrategy="balanced"
android:text="@{@string/data_retrieved_at(DateUtils.getRelativeTimeSpanString(charger.data.timeRetrieved.toEpochMilli(), Instant.now().toEpochMilli(), 0))}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.timeRetrieved == null || Duration.between(charger.data.timeRetrieved, Instant.now()).compareTo(Duration.ofHours(1)) > 0}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/sourceButton"
tools:text="Data retrieved 4 hours ago" />
<TextView
android:id="@+id/txtLicense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:breakStrategy="balanced"
android:text="@{charger.data.license}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.license != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtTimeRetrieved"
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
<Button
android:id="@+id/btnRefreshLiveData"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/refresh_live_data"
android:enabled="@{availability.status != Status.LOADING}"
app:icon="@drawable/ic_refresh"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView7" />
<HorizontalScrollView
android:id="@+id/buttonsScroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider1"
app:layout_constrainedWidth="true"
android:fillViewport="true"
app:goneUnless="@{charger.data != null &amp;&amp; (ChargepriceApi.isChargerSupported(charger.data) || charger.data.chargerUrl != null)}">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnChargeprice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/go_to_chargeprice"
android:transitionName="@string/shared_element_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:icon="@drawable/ic_chargeprice" />
<Button
android:id="@+id/btnChargerWebsite"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/charger_website"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargerUrl != null}"
app:icon="@drawable/ic_link" />
</LinearLayout>
</HorizontalScrollView>
<Button
android:id="@+id/btnLogin"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/login"
app:goneUnless="@{availability.status == Status.ERROR &amp;&amp; availability.message == &quot;not signed in&quot;}"
app:layout_constraintBottom_toBottomOf="@+id/textView13"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/textView13" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:id="@+id/connector_details_card"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@id/connectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardCornerRadius="24dp"
android:layout_marginBottom="@dimen/detail_corner_radius_negative"
android:paddingBottom="@dimen/detail_corner_radius"
app:cardElevation="6dp"
android:visibility="gone">
<include
layout="@layout/dialog_connector_details"
android:id="@+id/connector_details" />
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
</layout> </layout>

View File

@@ -7,6 +7,8 @@
<import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" /> <import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" /> <import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="net.vonforst.evmap.api.UtilsKt" /> <import type="net.vonforst.evmap.api.UtilsKt" />
@@ -47,7 +49,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="38dp" android:layout_marginStart="38dp"
android:layout_marginTop="38dp" android:layout_marginTop="38dp"
android:text="@{String.format(&quot;\u00D7 %d&quot;, item.chargepoint.count)}" android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;\u00D7 %d&quot;, item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.status == null}" app:goneUnless="@{item.status == null}"
app:layout_constraintStart_toStartOf="@+id/imageView" app:layout_constraintStart_toStartOf="@+id/imageView"
@@ -63,7 +65,7 @@
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:background="@drawable/rounded_rect" android:background="@drawable/rounded_rect"
android:padding="2dp" android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}" android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@android:color/white" android:textColor="@android:color/white"
app:backgroundTintAvailability="@{item.status}" app:backgroundTintAvailability="@{item.status}"
@@ -79,7 +81,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="36dp" android:layout_marginStart="36dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:text="@{item != null ? UtilsKt.nameForPlugType(ChargepointApiKt.stringProvider(context), item.chargepoint.type) + &quot; · &quot; + item.chargepoint.formatPower() : null}" android:text="@{item != null ? UtilsKt.nameForPlugType(ChargepointApiKt.stringProvider(context), item.chargepoint.type) + &quot; · &quot; + item.chargepoint.formatPower(LocaleConfigXKt.getCurrentOrDefaultLocale(context)) : null}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{item.chargepoint.hasKnownPower()}" app:goneUnless="@{item.chargepoint.hasKnownPower()}"
app:layout_constraintBottom_toTopOf="@id/textView8" app:layout_constraintBottom_toTopOf="@id/textView8"

View File

@@ -48,11 +48,14 @@
tools:orientation="horizontal" /> tools:orientation="horizontal" />
<TextView <TextView
android:id="@+id/textView2" android:id="@+id/tvChargeFromTo"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:clickable="true"
android:focusable="true"
android:background="?selectableItemBackground"
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}" android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall" android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary" android:textColor="?colorPrimary"
@@ -68,8 +71,8 @@
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}" android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging &amp;&amp; vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}" app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging &amp;&amp; vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
app:layout_constraintStart_toStartOf="@+id/textView2" app:layout_constraintStart_toStartOf="@+id/tvChargeFromTo"
app:layout_constraintTop_toBottomOf="@+id/textView2" app:layout_constraintTop_toBottomOf="@+id/tvChargeFromTo"
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" /> tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
<TextView <TextView

View File

@@ -8,7 +8,8 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"> android:layout_marginBottom="16dp"
tools:ignore="WebViewLayout">
<TextView <TextView
android:id="@+id/textView20" android:id="@+id/textView20"
@@ -18,75 +19,27 @@
android:text="@string/referrals" android:text="@string/referrals"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall" android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary" android:textColor="?colorPrimary"
app:layout_constraintBottom_toTopOf="@+id/textView21" app:layout_constraintBottom_toTopOf="@+id/referralWebView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintVertical_chainStyle="packed" />
<TextView <TextView
android:id="@+id/textView21" android:id="@+id/textView21"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/referrals_info" android:text="@string/referrals_info"
app:layout_constraintBottom_toTopOf="@+id/referral_tesla" app:layout_constraintBottom_toTopOf="@+id/referralWebView"
app:layout_constraintStart_toStartOf="@+id/textView20"
app:layout_constraintTop_toBottomOf="@+id/textView20" />
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:constraint_referenced_ids="referral_tesla,referral_juicify,referral_geldfuereauto,referral_maingau,referral_eprimo,referral_ewieeinfach"
app:flow_horizontalGap="16dp"
app:flow_horizontalStyle="packed"
app:flow_verticalAlign="baseline"
app:flow_wrapMode="chain"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView21" /> app:layout_constraintTop_toBottomOf="@+id/textView20" />
<Button <WebView
android:id="@+id/referral_tesla" android:id="@+id/referralWebView"
style="@style/Widget.Material3.Button.TonalButton" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/referral_tesla" android:layout_marginTop="8dp"
app:icon="@drawable/ic_tesla" /> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
<Button app:layout_constraintTop_toBottomOf="@id/textView21" />
android:id="@+id/referral_juicify"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/referral_juicify" />
<Button
android:id="@+id/referral_geldfuereauto"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/referral_geldfuereauto" />
<Button
android:id="@+id/referral_maingau"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/referral_maingau" />
<Button
android:id="@+id/referral_eprimo"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/referral_eprimo" />
<Button
android:id="@+id/referral_ewieeinfach"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/referral_ewieeinfach" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -9,8 +9,6 @@
<import type="net.vonforst.evmap.viewmodel.Status" /> <import type="net.vonforst.evmap.viewmodel.Status" />
<import type="com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike" />
<variable <variable
name="vm" name="vm"
type="net.vonforst.evmap.viewmodel.MapViewModel" /> type="net.vonforst.evmap.viewmodel.MapViewModel" />
@@ -46,7 +44,7 @@
android:layout_width="@dimen/map_toolbar_width" android:layout_width="@dimen/map_toolbar_width"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:layout_behavior="@string/ScrollingAppBarLayoutBehavior"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle" style="?attr/materialCardViewElevatedStyle"
@@ -146,7 +144,7 @@
android:layout_width="@dimen/map_toolbar_width" android:layout_width="@dimen/map_toolbar_width"
android:layout_height="@dimen/gallery_height_with_margin" android:layout_height="@dimen/gallery_height_with_margin"
android:background="?android:colorBackground" android:background="?android:colorBackground"
app:layout_behavior="@string/BackDropBottomSheetBehavior"> android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/gallery" android:id="@+id/gallery"
@@ -177,35 +175,23 @@
app:isFabActive="@{ vm.myLocationEnabled }" app:isFabActive="@{ vm.myLocationEnabled }"
app:layout_behavior="@string/hide_on_scroll_fab_behavior" /> app:layout_behavior="@string/hide_on_scroll_fab_behavior" />
<androidx.core.widget.NestedScrollView <include
android:id="@+id/bottom_sheet" android:id="@+id/detail_view"
layout="@layout/detail_view"
android:layout_width="@dimen/map_toolbar_width" android:layout_width="@dimen/map_toolbar_width"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:fillViewport="true" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
android:orientation="vertical"
app:bottomsheetbehavior_anchorPoint="@dimen/gallery_height"
app:behavior_hideable="true" app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/peek_height" app:charger="@{vm.charger}"
app:bottomsheetbehavior_defaultState="stateHidden" app:availability="@{vm.availability}"
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike" app:filteredAvailability="@{vm.filteredAvailability}"
android:clipToPadding="false" app:predictionData="@{vm.predictionData}"
tools:bottomsheetbehavior_defaultState="stateCollapsed"> app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
<include app:distance="@{vm.chargerDistance}"
android:id="@+id/detail_view" app:expanded="@{vm.bottomSheetExpanded}"
layout="@layout/detail_view" app:apiName="@{vm.apiName}"
app:charger="@{vm.charger}" app:teslaPricing="@{vm.teslaPricing}" />
app:availability="@{vm.availability}"
app:filteredAvailability="@{vm.filteredAvailability}"
app:predictionData="@{vm.predictionData}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}"
app:expanded="@{vm.bottomSheetExpanded}"
app:apiName="@{vm.apiName}"
app:teslaPricing="@{vm.teslaPricing}" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_directions" android:id="@+id/fab_directions"
@@ -218,16 +204,8 @@
android:translationX="@dimen/directions_fab_translationx" android:translationX="@dimen/directions_fab_translationx"
app:layout_anchor="@id/bottom_sheet" app:layout_anchor="@id/bottom_sheet"
app:layout_anchorGravity="top|right|end" app:layout_anchorGravity="top|right|end"
app:layout_behavior="@string/ScrollAwareFABBehavior"
android:theme="@style/NoElevationOverlay" /> android:theme="@style/NoElevationOverlay" />
<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
android:id="@+id/detail_app_bar"
android:layout_width="@dimen/map_toolbar_width"
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.Material3.FloatingActionButton.Small.Surface" style="@style/Widget.Material3.FloatingActionButton.Small.Surface"
android:id="@+id/fab_layers" android:id="@+id/fab_layers"

View File

@@ -21,6 +21,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="24dp" android:layout_margin="24dp"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
app:piv_rtl_mode="auto"
app:piv_animationType="worm" app:piv_animationType="worm"
app:piv_dynamicCount="true" app:piv_dynamicCount="true"
app:piv_interactiveAnimation="true" app:piv_interactiveAnimation="true"

View File

@@ -96,6 +96,7 @@
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:paddingStart="16dp" android:paddingStart="16dp"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted" app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"

View File

@@ -4,6 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" /> <import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" /> <import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
@@ -40,7 +42,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="38dp" android:layout_marginStart="38dp"
android:layout_marginTop="38dp" android:layout_marginTop="38dp"
android:text="@{String.format(&quot;\u00D7 %d&quot;, item.chargepoint.count)}" android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;\u00D7 %d&quot;, item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintStart_toStartOf="@+id/imageView" app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView" app:layout_constraintTop_toTopOf="@+id/imageView"
@@ -54,7 +56,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}" android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:background="@drawable/rounded_rect" android:background="@drawable/rounded_rect"
android:padding="2dp" android:padding="2dp"
@@ -72,7 +74,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:text="@{item.chargepoint.formatPower()}" android:text="@{item.chargepoint.formatPower(LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.chargepoint.hasKnownPower()}" app:goneUnless="@{item.chargepoint.hasKnownPower()}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -9,6 +9,8 @@
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" /> <import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<variable <variable
name="item" name="item"
type="Chargepoint" /> type="Chargepoint" />
@@ -51,7 +53,7 @@
android:layout_marginStart="38dp" android:layout_marginStart="38dp"
android:layout_marginTop="38dp" android:layout_marginTop="38dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:text="@{String.format(&quot;× %d&quot;, item.count)}" android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;× %d&quot;, item.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}" android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -65,7 +67,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:text="@{item.formatPower()}" android:text="@{item.formatPower(LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}" android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -26,6 +26,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="14dp" android:layout_marginBottom="14dp"
android:text="@{item.text}" android:text="@{item.text}"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -55,6 +56,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="14dp" android:layout_marginBottom="14dp"
android:text="@{item.detailText}" android:text="@{item.detailText}"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:linkify="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}" app:linkify="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
app:goneUnless="@{item.detailText != null}" app:goneUnless="@{item.detailText != null}"

View File

@@ -7,6 +7,8 @@
<import type="net.vonforst.evmap.api.UtilsKt" /> <import type="net.vonforst.evmap.api.UtilsKt" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="net.vonforst.evmap.viewmodel.Status" /> <import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" /> <import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
@@ -61,6 +63,7 @@
app:layout_constraintEnd_toStartOf="@+id/textView16" app:layout_constraintEnd_toStartOf="@+id/textView16"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
android:textAlignment="viewStart"
tools:text="Nikola-Tesla-Parkhaus mit extra langem Namen, der auf mehrere Zeilen umbricht" /> tools:text="Nikola-Tesla-Parkhaus mit extra langem Namen, der auf mehrere Zeilen umbricht" />
<TextView <TextView
@@ -72,6 +75,7 @@
android:maxLines="1" android:maxLines="1"
android:text="@{item.charger.address.toString()}" android:text="@{item.charger.address.toString()}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textAlignment="viewStart"
app:invisibleUnless="@{item.charger.address != null}" app:invisibleUnless="@{item.charger.address != null}"
app:layout_constraintEnd_toStartOf="@+id/textView7" app:layout_constraintEnd_toStartOf="@+id/textView7"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -85,8 +89,9 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:text="@{item.charger.formatChargepoints(ChargepointApiKt.stringProvider(context))}" android:text="@{item.charger.formatChargepoints(ChargepointApiKt.stringProvider(context), LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/textView7" app:layout_constraintEnd_toStartOf="@+id/textView7"
app:layout_constraintStart_toStartOf="@+id/textView2" app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2" app:layout_constraintTop_toBottomOf="@+id/textView2"
@@ -111,7 +116,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="@drawable/rounded_rect" android:background="@drawable/rounded_rect"
android:padding="2dp" android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.available.data), item.total)}" android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/white" android:textColor="@android:color/white"
app:backgroundTintAvailability="@{item.available.data}" app:backgroundTintAvailability="@{item.available.data}"

View File

@@ -29,6 +29,7 @@
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:text="@{item.filter.name}" android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textAlignment="viewStart"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch1" app:layout_constraintEnd_toStartOf="@+id/switch1"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@@ -32,6 +32,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@{item.filter.name}" android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textAlignment="viewStart"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Connectors" /> tools:text="Connectors" />

View File

@@ -33,6 +33,7 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:text="@{item.filter.name}" android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/btnEdit" app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -61,6 +62,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:text="@{item.value.all ? @string/all_selected : @string/number_selected(item.value.values.size())}" android:text="@{item.value.all ? @string/all_selected : @string/number_selected(item.value.values.size())}"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/btnEdit" app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView17" app:layout_constraintTop_toBottomOf="@+id/textView17"

View File

@@ -30,6 +30,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@string/map_type" android:text="@string/map_type"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall" android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@id/btnClose" app:layout_constraintEnd_toStartOf="@id/btnClose"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@@ -52,7 +53,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(AnyMap.Type.NORMAL)}" android:checked="@{vm.mapType.equals(AnyMap.Type.NORMAL)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.NORMAL)}" android:onClick="@{() -> vm.setMapType(AnyMap.Type.NORMAL)}"
android:text="@string/map_type_normal" /> android:text="@string/map_type_normal"
android:textAlignment="viewStart" />
<RadioButton <RadioButton
android:id="@+id/rbSatellite" android:id="@+id/rbSatellite"
@@ -60,7 +62,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(AnyMap.Type.HYBRID)}" android:checked="@{vm.mapType.equals(AnyMap.Type.HYBRID)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.HYBRID)}" android:onClick="@{() -> vm.setMapType(AnyMap.Type.HYBRID)}"
android:text="@string/map_type_satellite" /> android:text="@string/map_type_satellite"
android:textAlignment="viewStart" />
<RadioButton <RadioButton
android:id="@+id/rbTerrain" android:id="@+id/rbTerrain"
@@ -68,7 +71,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(AnyMap.Type.TERRAIN)}" android:checked="@{vm.mapType.equals(AnyMap.Type.TERRAIN)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.TERRAIN)}" android:onClick="@{() -> vm.setMapType(AnyMap.Type.TERRAIN)}"
android:text="@string/map_type_terrain" /> android:text="@string/map_type_terrain"
android:textAlignment="viewStart" />
</RadioGroup> </RadioGroup>
<TextView <TextView
@@ -80,6 +84,8 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@string/map_details" android:text="@string/map_details"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall" android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textAlignment="viewStart"
app:goneUnless="@{vm.mapTrafficSupported}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -94,6 +100,8 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@string/map_traffic" android:text="@string/map_traffic"
android:checked="@={vm.mapTrafficEnabled}" android:checked="@={vm.mapTrafficEnabled}"
android:textAlignment="viewStart"
app:goneUnless="@{vm.mapTrafficSupported}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView23" /> app:layout_constraintTop_toBottomOf="@+id/textView23" />

View File

@@ -2,6 +2,7 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/map"
android:id="@+id/nav_graph"> android:id="@+id/nav_graph">
<navigation <navigation

View File

@@ -3,14 +3,14 @@
<string name="no_browser_app_found">Nejprve si nainstalujte webový prohlížeč</string> <string name="no_browser_app_found">Nejprve si nainstalujte webový prohlížeč</string>
<string name="address">Adresa</string> <string name="address">Adresa</string>
<string name="hours">Otevírací doba</string> <string name="hours">Otevírací doba</string>
<string name="open_247"><b>Otevřeno 24/7</b></string> <string name="open_247"><![CDATA[<b>Otevřeno 24/7</b>]]></string>
<string name="closed"><b>Zavřeno</b></string> <string name="closed"><![CDATA[<b>Zavřeno</b>]]></string>
<string name="open_closesat"><b>Otevřeno</b> · Zavírá v %s</string> <string name="open_closesat"><![CDATA[<b>Otevřeno</b> · Zavírá v %s]]></string>
<string name="closed_opensat"><b>Zavřeno</b> · Otevírá v %s</string> <string name="closed_opensat"><![CDATA[<b>Zavřeno</b> · Otevírá v %s]]></string>
<string name="cost">Cena</string> <string name="cost">Cena</string>
<string name="cost_detail"><b>Nabíjení:</b> %1$s · <b>Parkování:</b> %2$s</string> <string name="cost_detail"><![CDATA[<b>Nabíjení:</b> %1$s · <b>Parkování:</b> %2$s]]></string>
<string name="cost_detail_charging"><b>%s nabíjení</b></string> <string name="cost_detail_charging"><![CDATA[<b>%s nabíjení</b>]]></string>
<string name="cost_detail_parking"><b>%s parkování</b></string> <string name="cost_detail_parking"><![CDATA[<b>%s parkování</b>]]></string>
<string name="charging_free">Bezplatné</string> <string name="charging_free">Bezplatné</string>
<string name="charging_paid">Placené</string> <string name="charging_paid">Placené</string>
<string name="parking_free">Bezplatné</string> <string name="parking_free">Bezplatné</string>
@@ -177,7 +177,7 @@
<string name="crash_report_comment_prompt">Níže můžete přidat komentář:</string> <string name="crash_report_comment_prompt">Níže můžete přidat komentář:</string>
<string name="powered_by_mapbox">používá službu Mapbox</string> <string name="powered_by_mapbox">používá službu Mapbox</string>
<string name="pref_search_provider">Poskytovatel vyhledávání</string> <string name="pref_search_provider">Poskytovatel vyhledávání</string>
<string name="pref_search_provider_info">Načtení dat pro vyhledávání bývá drahé, obzvláště z Map Google. Zvažte prosím poslání finančního daru v nabídce „O aplikaci“ → „Přispět“.</string> <string name="pref_search_provider_info"><![CDATA[Načtení dat pro vyhledávání bývá drahé, obzvláště z Map Google. Zvažte prosím poslání finančního daru v nabídce „O aplikaci“ → „Přispět“.]]></string>
<string name="github_sponsors">GitHub Sponsors</string> <string name="github_sponsors">GitHub Sponsors</string>
<string name="donate_desc">Podpořte vývoj aplikace EVMap jednorázovým darem</string> <string name="donate_desc">Podpořte vývoj aplikace EVMap jednorázovým darem</string>
<string name="github_sponsors_desc">Podpořte EVMap ve službě GitHub Sponsors</string> <string name="github_sponsors_desc">Podpořte EVMap ve službě GitHub Sponsors</string>
@@ -199,8 +199,6 @@
<string name="autocomplete_connection_error">Nepodařilo se načíst návrhy</string> <string name="autocomplete_connection_error">Nepodařilo se načíst návrhy</string>
<string name="pref_language_device_default">Podle zařízení</string> <string name="pref_language_device_default">Podle zařízení</string>
<string name="pref_darkmode_device_default">Podle zařízení</string> <string name="pref_darkmode_device_default">Podle zařízení</string>
<string name="pref_chargeprice_currency_sek">Švédská koruna (SEK)</string>
<string name="pref_chargeprice_currency_usd">Americký dolar (USD)</string>
<string name="pref_provider_google_maps">Mapy Google</string> <string name="pref_provider_google_maps">Mapy Google</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string> <string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Přispěvatelé</string> <string name="about_contributors">Přispěvatelé</string>
@@ -283,7 +281,7 @@
<string name="loading">Načítání…</string> <string name="loading">Načítání…</string>
<string name="auto_multipage_goto">Stránka %d</string> <string name="auto_multipage_goto">Stránka %d</string>
<string name="reload">Obnovit</string> <string name="reload">Obnovit</string>
<string name="accept_privacy"><![CDATA[Přečetl/a jsem si a souhlasím se <a href="%s">zásadami ochrany osobních údajů</a> aplikace EVMap.]]></string> <string name="accept_privacy"><![CDATA[Přečetl/a jsem si a souhlasím se <a href=\"%s\">zásadami ochrany osobních údajů</a> aplikace EVMap.]]></string>
<string name="referrals">Referenční odkazy</string> <string name="referrals">Referenční odkazy</string>
<string name="referrals_info">Pro podpoření vývojáře svým nákupem můžete také použít jeden z referenčních odkazů níže.</string> <string name="referrals_info">Pro podpoření vývojáře svým nákupem můžete také použít jeden z referenčních odkazů níže.</string>
<string name="generic_connection_error">Nepodařilo se načíst data</string> <string name="generic_connection_error">Nepodařilo se načíst data</string>
@@ -346,21 +344,11 @@
<string name="chargeprice_connection_error">Nepodařilo se načíst ceny</string> <string name="chargeprice_connection_error">Nepodařilo se načíst ceny</string>
<string name="unknown_operator">Neznámý operátor</string> <string name="unknown_operator">Neznámý operátor</string>
<string name="data_source_goingelectric">GoingElectric.de</string> <string name="data_source_goingelectric">GoingElectric.de</string>
<string name="data_source_openchargemap_desc">Celosvětové, s různou kvalitou. Popisy jsou v angličtině nebo v místním jazyce. Spravováno komunitou, v některých zemích obsahuje vládní data (např. Severní Amerika, Spojené království, Francie, Norsko).</string> <string name="data_source_openchargemap_desc"><![CDATA[Celosvětové, s různou kvalitou. Popisy jsou v angličtině nebo v místním jazyce. Spravováno komunitou, v některých zemích obsahuje vládní data (např. Severní Amerika, Spojené království, Francie, Norsko).]]></string>
<string name="privacy_link">https://ev-map.app/privacypolicy/</string> <string name="privacy_link">https://ev-map.app/privacypolicy/</string>
<string name="chargeprice_faq_link">https://ev-map.app/faq/#price-comparison-feature</string> <string name="chargeprice_faq_link">https://ev-map.app/faq/#price-comparison-feature</string>
<string name="pref_darkmode_always_on">vždy zapnut</string> <string name="pref_darkmode_always_on">vždy zapnut</string>
<string name="pref_darkmode_always_off">vždy vypnut</string> <string name="pref_darkmode_always_off">vždy vypnut</string>
<string name="pref_chargeprice_currency_chf">Švýcarský frank (CHF)</string>
<string name="pref_chargeprice_currency_czk">Česká koruna (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Dánská koruna (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Britská libra (GBP)</string>
<string name="pref_chargeprice_currency_huf">Maďarský forint (HUF)</string>
<string name="pref_chargeprice_currency_hrk">Chorvatská kuna (HRK)</string>
<string name="pref_chargeprice_currency_isk">Islandská koruna (ISK)</string>
<string name="pref_chargeprice_currency_nok">Norská koruna (NOK)</string>
<string name="pref_chargeprice_currency_pln">Polský zlotý (PLN)</string>
<string name="chargeprice_header_other_tariffs">Ostatní plány</string> <string name="chargeprice_header_other_tariffs">Ostatní plány</string>
<string name="charger_website">Webové stránky</string> <string name="charger_website">Webové stránky</string>
<string name="compass">Kompas</string> <string name="compass">Kompas</string>
@@ -383,4 +371,6 @@
<string name="pref_chargeprice_native_integration">Porovnání cen v EVMap</string> <string name="pref_chargeprice_native_integration">Porovnání cen v EVMap</string>
<string name="pref_chargeprice_native_integration_on">Data o cenách budou zobrazena přímo v EVMap</string> <string name="pref_chargeprice_native_integration_on">Data o cenách budou zobrazena přímo v EVMap</string>
<string name="pref_chargeprice_native_integration_off">Tlačítko porovnání cen bude odkazovat na aplikaci nebo web Chargeprice</string> <string name="pref_chargeprice_native_integration_off">Tlačítko porovnání cen bude odkazovat na aplikaci nebo web Chargeprice</string>
<string name="pref_provider_osm">OpenStreetMap</string>
<string name="filterprofile_name_not_unique">Již existuje profil filtru s tímto názvem</string>
</resources> </resources>

View File

@@ -101,6 +101,7 @@
<string name="pref_language">App-Sprache</string> <string name="pref_language">App-Sprache</string>
<string name="pref_darkmode">Dunkles Design</string> <string name="pref_darkmode">Dunkles Design</string>
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string> <string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
<string name="zoom_in_to_see_more">Hineinzoomen um alle Ladestationen zu sehen</string>
<string name="location_error">Standort nicht erkannt. Bitte Systemeinstellungen prüfen</string> <string name="location_error">Standort nicht erkannt. Bitte Systemeinstellungen prüfen</string>
<string name="retry">Wiederholen</string> <string name="retry">Wiederholen</string>
<string name="filter_open_247">24 Stunden geöffnet</string> <string name="filter_open_247">24 Stunden geöffnet</string>
@@ -110,7 +111,9 @@
<string name="and_n_others">und %d weitere</string> <string name="and_n_others">und %d weitere</string>
<string name="pref_map_provider">Kartenanbieter</string> <string name="pref_map_provider">Kartenanbieter</string>
<string name="twitter">Twitter</string> <string name="twitter">Twitter</string>
<string name="mastodon">Mastodon</string>
<string name="goingelectric_forum">Forenthread bei GoingElectric.de</string> <string name="goingelectric_forum">Forenthread bei GoingElectric.de</string>
<string name="tff_forum">Forenthread im TFF-Forum</string>
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>
<string name="menu_report_new_charger">Ladesäule melden</string> <string name="menu_report_new_charger">Ladesäule melden</string>
<string name="edit_at_datasource">Bei %s bearbeiten</string> <string name="edit_at_datasource">Bei %s bearbeiten</string>
@@ -151,6 +154,7 @@
<string name="delete">Löschen</string> <string name="delete">Löschen</string>
<string name="save_as_profile">Als Profil speichern</string> <string name="save_as_profile">Als Profil speichern</string>
<string name="save_profile_enter_name">Gib den Namen des Filterprofils ein:</string> <string name="save_profile_enter_name">Gib den Namen des Filterprofils ein:</string>
<string name="filterprofile_name_not_unique">Ein Filterprofil mit diesem Namen existiert bereits</string>
<string name="filterprofiles_empty_state">Du hast keine Filterprofile gespeichert</string> <string name="filterprofiles_empty_state">Du hast keine Filterprofile gespeichert</string>
<string name="welcome_to_evmap">Willkommen bei EVMap</string> <string name="welcome_to_evmap">Willkommen bei EVMap</string>
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe</string> <string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe</string>
@@ -232,7 +236,7 @@
<string name="crash_report_comment_prompt">Du kannst unten noch einen Kommentar hinzufügen:</string> <string name="crash_report_comment_prompt">Du kannst unten noch einen Kommentar hinzufügen:</string>
<string name="powered_by_mapbox">powered by Mapbox</string> <string name="powered_by_mapbox">powered by Mapbox</string>
<string name="pref_search_provider">Anbieter für Ortssuche</string> <string name="pref_search_provider">Anbieter für Ortssuche</string>
<string name="pref_search_provider_info">Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Über eine Spende unter \"Über EVMap -&gt; Spenden\" würde ich mich sehr freuen.</string> <string name="pref_search_provider_info"><![CDATA[Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Über eine Spende unter Über EVMap Spenden würde ich mich sehr freuen.]]></string>
<string name="github_sponsors">GitHub Sponsors</string> <string name="github_sponsors">GitHub Sponsors</string>
<string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string> <string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string>
<string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string> <string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string>
@@ -240,6 +244,7 @@
<string name="privacy_link">https://ev-map.app/de/privacypolicy/</string> <string name="privacy_link">https://ev-map.app/de/privacypolicy/</string>
<string name="faq_link">https://ev-map.app/de/faq/</string> <string name="faq_link">https://ev-map.app/de/faq/</string>
<string name="chargeprice_faq_link">https://ev-map.app/de/faq/#preisvergleichsfunktion</string> <string name="chargeprice_faq_link">https://ev-map.app/de/faq/#preisvergleichsfunktion</string>
<string name="referral_link">https://ev-map.app/de/referrals/</string>
<string name="required">erforderlich</string> <string name="required">erforderlich</string>
<string name="edit_filter_profile">„%s“ bearbeiten</string> <string name="edit_filter_profile">„%s“ bearbeiten</string>
<string name="pref_search_delete_recent">Suchverlauf löschen</string> <string name="pref_search_delete_recent">Suchverlauf löschen</string>
@@ -258,19 +263,8 @@
<string name="pref_darkmode_device_default">Geräteeinstellung verwenden</string> <string name="pref_darkmode_device_default">Geräteeinstellung verwenden</string>
<string name="pref_darkmode_always_on">immer an</string> <string name="pref_darkmode_always_on">immer an</string>
<string name="pref_darkmode_always_off">immer aus</string> <string name="pref_darkmode_always_off">immer aus</string>
<string name="pref_chargeprice_currency_chf">Schweizer Franken (CHF)</string>
<string name="pref_chargeprice_currency_czk">Tschechische Krone (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Dänische Krone (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Britisches Pfund (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Ungarischer Forint (HUF)</string>
<string name="pref_chargeprice_currency_isk">Isländische Krone (ISK)</string>
<string name="pref_chargeprice_currency_nok">Norwegische Krone (NOK)</string>
<string name="pref_chargeprice_currency_pln">Polnischer Złoty (PLN)</string>
<string name="pref_chargeprice_currency_sek">Schwedische Krone (SEK)</string>
<string name="pref_chargeprice_currency_usd">US-Dollar (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string> <string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm">OpenStreetMap</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string> <string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Mitwirkende</string> <string name="about_contributors">Mitwirkende</string>
<string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string> <string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string>

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