Compare commits

..

1 Commits

Author SHA1 Message Date
johan12345
a22b347edc implement chargeprice feedback dialog
#195
2022-09-04 11:11:46 +02:00
88 changed files with 1341 additions and 2596 deletions

View File

@@ -4,5 +4,4 @@
<string name="goingelectric_key" translatable="false">ci</string>
<string name="chargeprice_key" translatable="false">ci</string>
<string name="openchargemap_key" translatable="false">ci</string>
<string name="fronyx_key" translatable="false">ci</string>
</resources>

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 663.6 219.8" style="enable-background:new 0 0 663.6 219.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFB300;}
.st1{fill:#90A4AE;}
.st2{fill:#546E7A;}
.st3{fill:#00E676;}
.st4{fill:#FFFFFF;fill-opacity:0.2;}
.st5{fill:#3E2723;fill-opacity:0.2;}
.st6{opacity:0.45;enable-background:new ;}
.st7{enable-background:new ;}
.st8{fill:#1D1D1B;}
</style>
<g id="Ebene_2_1_">
<g>
<g>
<g>
<path class="st0"
d="M19.4,161.7l-4-35.1l-6.1,0.6l4,35.1L19.4,161.7z M41.2,159.1l-4-35.1l-6.1,0.6l4,35.1L41.2,159.1z" />
<path class="st1" d="M52.6,206.9c-1.9,2.3-3.4,3.8-3.6,4c-5.5,4.4-9.9,5.7-13.5,4c-6.3-3.2-5.9-15-5.7-16.3l4.4,0.2
c-0.2,3.4,0.4,10.6,3.4,12c1.7,0.8,4.6-0.2,8.5-3.4l0,0c0,0,12.3-12.3,9.7-22c-3-11.6,10.6-28.3,15-34l0.6-0.6l3.6,2.7l-0.6,0.8
c-13.7,16.9-15.2,25.6-14.2,30C62.3,192.9,56.6,202,52.6,206.9z" />
<path class="st1"
d="M5.9,161.2l1.7,14.4l13.3,8.9l18-1.9l11-11.6l-1.7-14.4L5.9,161.2z" />
<g>
<path class="st2" d="M38.6,182.6l-18,1.9l3.8,15.8l14.2-1.7V182.6L38.6,182.6z M51.5,144.5l1.5,13.1l-51.5,5.9L0,150.4
L51.5,144.5z" />
</g>
</g>
<g>
<g>
<path class="st3" d="M91.9,0c-37,0-67,30-67,67c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6
c6.8-72,63.2-98.4,63.2-148.9C158.8,29.8,128.8,0,91.9,0z" />
<path class="st4" d="M91.9,1.5c36.8,0,66.5,29.6,67,66.1c0-0.2,0-0.4,0-0.6c0-37-30-67-67-67s-67,29.8-67,67c0,0.2,0,0.4,0,0.6
C25.3,31.1,55.1,1.5,91.9,1.5L91.9,1.5z" />
<path class="st5" d="M95.9,214.3c-0.2,2.1-1.9,3.6-4,3.6s-3.8-1.5-4-3.6c-6.5-71.8-62.5-98.2-63-148.1c0,0.4,0,0.6,0,1.1
c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6c6.8-72,63.2-98.4,63.2-148.9c0-0.4,0-0.6,0-1.1
C158.4,116,102.4,142.4,95.9,214.3L95.9,214.3z" />
</g>
<path class="st6"
d="M76.5,34.3v40.6h11v33.2l25.8-44.4H98.5l14.8-29.6C113.4,34.3,76.5,34.3,76.5,34.3z" />
</g>
</g>
<g class="st7">
<path class="st8"
d="M307.9,102.9h-45.4v39.6h52.2v6.9h-60.4V52.3h60.1v6.9h-51.9v36.7h45.4V102.9z" />
<path class="st8"
d="M361.2,137.4l0.5,2.1l0.6-2.1l30.5-85.1h9l-36.1,97.1h-7.9l-36.1-97.1h8.9L361.2,137.4z" />
<path class="st8" d="M427,52.4l35.8,85.7l35.9-85.7h10.9v97.1h-8.2v-42.3l0.7-43.3L466,149.5h-6.3l-36-85.3l0.7,42.7v42.5h-8.2
V52.3H427V52.4z" />
<path class="st8" d="M578,149.4c-0.8-2.3-1.3-5.6-1.5-10.1c-2.8,3.6-6.4,6.5-10.7,8.4c-4.3,2-8.9,3-13.8,3
c-6.9,0-12.5-1.9-16.8-5.8s-6.4-8.8-6.4-14.7c0-7,2.9-12.6,8.8-16.7c5.8-4.1,14-6.1,24.4-6.1h14.5v-8.2c0-5.2-1.6-9.2-4.8-12.2
s-7.8-4.4-13.9-4.4c-5.6,0-10.2,1.4-13.8,4.3c-3.6,2.8-5.5,6.3-5.5,10.3l-8-0.1c0-5.7,2.7-10.7,8-14.9s11.9-6.3,19.7-6.3
c8,0,14.4,2,19,6s7,9.6,7.2,16.8v34.1c0,7,0.7,12.2,2.2,15.7v0.8H578V149.4z M552.9,143.7c5.3,0,10.1-1.3,14.3-3.9
s7.3-6,9.2-10.3v-15.9h-14.3c-8,0.1-14.2,1.5-18.7,4.4c-4.5,2.8-6.7,6.7-6.7,11.6c0,4,1.5,7.4,4.5,10.1S548.1,143.7,552.9,143.7z
" />
<path class="st8" d="M663.6,114.1c0,11.2-2.5,20.2-7.5,26.8s-11.6,9.9-20,9.9c-9.9,0-17.4-3.5-22.7-10.4v36.8h-7.9V77.3h7.4
l0.4,10.2C618.5,79.8,626,76,635.9,76c8.6,0,15.4,3.3,20.3,9.8c4.9,6.5,7.4,15.6,7.4,27.2V114.1z M655.6,112.7
c0-9.2-1.9-16.5-5.7-21.8s-9-8-15.8-8c-4.9,0-9.1,1.2-12.6,3.5c-3.5,2.4-6.2,5.8-8.1,10.3v34.6c1.9,4.1,4.6,7.3,8.2,9.5
c3.6,2.2,7.8,3.3,12.6,3.3c6.7,0,11.9-2.7,15.7-8C653.7,130.6,655.6,122.9,655.6,112.7z" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 566.9 254.9" style="enable-background:new 0 0 566.9 254.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000044;}
</style>
<path class="st0" d="M60.8,86.3c-5.6,0-10,0.5-13.3,1.4c-3.3,0.9-5.7,2.7-7.1,5.2c-0.4,0.6-0.7,1.3-0.9,2c-1.1,3.3-1.4,6.8-1.4,10.2
c0,2.1,0,4.2,0,6.2c0,0.1,0,0.2,0,0.3h22.3v20.9H38.1v71.2v7.6H14.5v-7.6v-71.2H0v-20.9h14.5V104c0-13.4,3.9-23.8,11.2-31.3
c7.3-7.4,19-10.8,35.1-10V86.3 M157.1,161.2c0-15.5,11.2-27.3,25.7-27.3c14.5,0,25.5,11.8,25.5,27.3c0,15.6-11,27.3-25.5,27.3
C168.3,188.5,157.1,176.9,157.1,161.2 M182.8,110.1c-27.9,0-49.7,22.2-49.7,51.1c0,29.1,21.8,51.3,49.7,51.3
c27.9,0,49.4-22.2,49.4-51.3C232.1,132.3,210.7,110.1,182.8,110.1 M541.4,161.5c14.1,0,25.6-11.5,25.6-25.6
c0-14.1-11.5-25.6-25.6-25.6c-14.1,0-25.6,11.5-25.6,25.6C515.8,150,527.2,161.5,541.4,161.5 M129.6,110.5c-2.2-0.2-4.3-0.4-6.6-0.4
c-4.4,0-9.1,0.6-13.9,1.9c-4.8,1.3-8.5,3.4-10.7,6.5v-7H74.7V211h23.8v-58.2c0-5.4,1.4-9.5,4.1-12.3c4.4-4.5,10.6-6.5,16.7-6.8
c1.7-0.1,3.5,0,5.3,0.1c1.6,0.2,3.2,0.5,4.7,0.7c0-1,0-2,0-3c0-2,0-3.9,0-5.9c0-2.2,0-4.5,0-6.7c0-1.9,0.2-3.8,0.2-5.7
c0-0.9,0.2-1.7,0.2-2.6C129.6,110.6,129.6,110.5,129.6,110.5z M475.8,160.6l29.7-49h-28.7l-16.3,31.7l-16.5-31.7H415l30.1,49
l-30.7,50.6h28.7l17.3-33.1l17.7,33.1h28.7L475.8,160.6z M356.9,254.8c14.9,0.5,26.7-3.1,34.7-10.9c7.6-7.4,11.6-18.1,12-33l0,0
v-2.4v-97.1h-24.5c0,0,0,14.3,0,28.8v14.9v0.1v14.5c0,5.3-1.2,9.7-5.8,13.8c-4.3,3.9-10.9,5.1-15.4,5.1c-2.7,0-10.7-0.9-15.4-7.5
c-2.4-3.4-3.9-9.1-4.2-14c-0.3-5-0.3-7.4-0.6-14.7c-1.1-30.2-26-47.6-55-40.3c-5,1.3-9,3.4-11.2,6.5v-7.2h-23.8V211h23.8v-52v-6
c0-5.4,1.5-9.5,4.3-12.3c2.8-2.7,5.8-4.2,9.5-5.1c3.7-0.9,6.6-1,8.5-1c2.9,0,6.5,0.8,8,1.2c2.8,0.8,6.3,3.3,8.3,6.2
c3.7,5.3,3.3,12.8,3.5,18.9c0.2,7.6,0.4,15.3,2.7,22.7c3.2,10.3,9.7,20,19.7,24.6c9,4.1,20,5.2,29.7,3.4c3-0.6,6.3-1.5,9.1-3
c1.6-0.9,3.2-1.9,4.5-3.2c0.4,10.7,0.6,17.6-6,22.5c-1.8,1.4-4.3,2.5-6.5,2.9c-3.1,0.6-6.5,1-9.7,0.9L356.9,254.8z" />
<g>
<g>
<path class="st0" d="M97.6,89.8V39.2c0-1.6-0.1-3.2-0.2-4.8c-0.1-1.7-0.3-3.3-0.5-4.9h6.6l0.9,10h-1c0.9-3.3,2.7-5.9,5.5-7.9
c2.7-1.9,6-2.9,9.8-2.9c3.8,0,7.1,0.9,9.9,2.6c2.8,1.7,4.9,4.2,6.5,7.5c1.6,3.2,2.4,7.2,2.4,11.8c0,4.5-0.8,8.4-2.3,11.7
c-1.5,3.2-3.7,5.8-6.5,7.5c-2.8,1.8-6.1,2.6-9.9,2.6c-3.8,0-7-1-9.7-2.9c-2.7-1.9-4.6-4.5-5.5-7.8h0.9v28.1H97.6z M117.3,66.8
c4,0,7.2-1.4,9.6-4.2c2.4-2.8,3.6-6.8,3.6-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2
c-2.4,2.8-3.6,6.8-3.6,12.2c0,5.3,1.2,9.4,3.6,12.1C110.2,65.4,113.4,66.8,117.3,66.8z" />
<path class="st0" d="M165.4,72.4c-4.1,0-7.6-0.9-10.6-2.6c-3-1.8-5.3-4.3-6.9-7.6c-1.6-3.3-2.4-7.2-2.4-11.6
c0-4.5,0.8-8.4,2.4-11.7c1.6-3.2,3.9-5.8,6.9-7.5c3-1.8,6.5-2.6,10.5-2.6c4.1,0,7.6,0.9,10.6,2.6c3,1.8,5.3,4.3,7,7.5
c1.7,3.2,2.5,7.1,2.5,11.7c0,4.5-0.8,8.4-2.5,11.6c-1.7,3.3-4,5.8-7,7.6C172.9,71.5,169.4,72.4,165.4,72.4z M165.4,66.8
c4,0,7.1-1.4,9.5-4.2c2.4-2.8,3.5-6.8,3.5-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.5-4.2c-4,0-7.1,1.4-9.5,4.2
c-2.4,2.8-3.5,6.8-3.5,12.2c0,5.3,1.2,9.4,3.5,12.1C158.2,65.4,161.4,66.8,165.4,66.8z" />
<path class="st0" d="M207.2,71.6l-15.6-42.2h7.2l13,37.1h-2.2l13.4-37.1h5.9l13.2,37.1h-2.1l13.1-37.1h6.9l-15.7,42.2h-6.6
l-13.6-37.7h3.4l-13.7,37.7H207.2z" />
<path class="st0" d="M287.7,72.4c-6.6,0-11.8-1.9-15.6-5.8c-3.8-3.8-5.7-9.2-5.7-16c0-4.4,0.8-8.3,2.5-11.5c1.7-3.3,4-5.8,7.1-7.6
c3-1.8,6.5-2.7,10.4-2.7c3.9,0,7.1,0.8,9.7,2.4c2.6,1.6,4.6,3.9,6,6.9c1.4,3,2.1,6.5,2.1,10.6v2.5h-32.7v-4.3h28.2l-1.4,1.1
c0-4.5-1-8-3-10.5c-2-2.5-5-3.8-9-3.8c-4.2,0-7.5,1.5-9.8,4.4C274.2,41.1,273,45,273,50v0.8c0,5.3,1.3,9.3,3.9,12
c2.6,2.7,6.3,4.1,11,4.1c2.5,0,4.9-0.4,7.1-1.1c2.2-0.8,4.3-2,6.3-3.7l2.4,4.8c-1.8,1.8-4.2,3.2-7,4.2
C293.8,71.9,290.8,72.4,287.7,72.4z" />
<path class="st0" d="M314.5,71.6v-32c0-1.7,0-3.4-0.1-5.1c-0.1-1.7-0.2-3.4-0.4-5h6.6l0.8,10.2l-1.2,0.1c0.6-2.5,1.5-4.6,2.9-6.2
c1.4-1.6,3.1-2.8,5-3.7c1.9-0.8,3.9-1.2,6-1.2c0.8,0,1.6,0,2.2,0.1c0.6,0.1,1.2,0.2,1.8,0.4l-0.1,6c-0.8-0.3-1.6-0.5-2.3-0.5
s-1.5-0.1-2.4-0.1c-2.5,0-4.6,0.6-6.4,1.8c-1.8,1.2-3.2,2.7-4.1,4.5s-1.4,3.8-1.4,5.9v24.9H314.5z" />
<path class="st0" d="M363.9,72.4c-6.6,0-11.8-1.9-15.6-5.8c-3.8-3.8-5.7-9.2-5.7-16c0-4.4,0.8-8.3,2.5-11.5c1.7-3.3,4-5.8,7.1-7.6
c3-1.8,6.5-2.7,10.4-2.7c3.9,0,7.1,0.8,9.7,2.4c2.6,1.6,4.6,3.9,6,6.9c1.4,3,2.1,6.5,2.1,10.6v2.5h-32.7v-4.3H376l-1.4,1.1
c0-4.5-1-8-3-10.5c-2-2.5-5-3.8-9-3.8c-4.2,0-7.5,1.5-9.8,4.4c-2.4,2.9-3.5,6.9-3.5,11.9v0.8c0,5.3,1.3,9.3,3.9,12
c2.6,2.7,6.3,4.1,11,4.1c2.5,0,4.9-0.4,7.1-1.1c2.2-0.8,4.3-2,6.3-3.7l2.4,4.8c-1.8,1.8-4.2,3.2-7,4.2
C370,71.9,367,72.4,363.9,72.4z" />
<path class="st0" d="M406.8,72.4c-3.7,0-6.9-0.9-9.7-2.6c-2.8-1.8-5-4.3-6.5-7.5c-1.5-3.2-2.3-7.1-2.3-11.7
c0-4.6,0.8-8.5,2.3-11.8c1.5-3.2,3.7-5.7,6.5-7.5c2.8-1.7,6-2.6,9.7-2.6c3.8,0,7.1,1,9.9,2.9c2.8,1.9,4.6,4.5,5.6,7.7h-1V9.8h6.8
v61.8h-6.7V61.5h0.9c-0.9,3.4-2.7,6-5.5,7.9C413.9,71.4,410.6,72.4,406.8,72.4z M408.2,66.8c4,0,7.2-1.4,9.6-4.2
c2.4-2.8,3.6-6.8,3.6-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2c-2.4,2.8-3.6,6.8-3.6,12.2
c0,5.3,1.2,9.4,3.6,12.1C401.1,65.4,404.3,66.8,408.2,66.8z" />
<path class="st0" d="M484.3,72.4c-3.8,0-7.1-1-9.8-2.9c-2.7-1.9-4.6-4.6-5.5-7.9h0.9v10.1h-6.7V9.8h6.8v29.5h-1
c1-3.2,2.8-5.8,5.5-7.7c2.7-1.9,6-2.9,9.8-2.9c3.8,0,7.1,0.9,9.9,2.6c2.8,1.8,4.9,4.3,6.5,7.5c1.5,3.2,2.3,7.1,2.3,11.7
s-0.8,8.4-2.4,11.7c-1.6,3.2-3.7,5.8-6.5,7.5C491.4,71.5,488.1,72.4,484.3,72.4z M482.9,66.8c4,0,7.2-1.4,9.6-4.1
c2.4-2.7,3.6-6.8,3.6-12.2s-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2c-2.4,2.8-3.6,6.8-3.6,12.2
c0,5.3,1.2,9.4,3.6,12.1C475.8,65.4,478.9,66.8,482.9,66.8z" />
<path class="st0" d="M511.9,90.7l-1.6-5.6c2.6-0.6,4.8-1.3,6.6-2.1c1.8-0.8,3.2-1.9,4.4-3.2c1.2-1.3,2.2-3,3-5l2.2-5l-0.2,2.9
l-18.4-43.1h7.4l15.2,37h-2.2l15-37h7.1L531,75.1c-1.1,2.7-2.4,4.9-3.7,6.8c-1.3,1.8-2.8,3.3-4.3,4.5c-1.5,1.1-3.2,2.1-5.1,2.7
S514.1,90.3,511.9,90.7z" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -21,8 +21,8 @@ android {
minSdkVersion 21
targetSdkVersion 31
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 138
versionName "1.4.0"
versionCode 106
versionName "1.3.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(",")
@@ -107,7 +107,6 @@ android {
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
namespace 'net.vonforst.evmap'
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all { variant ->
@@ -141,13 +140,6 @@ android {
if (chargepriceKey != null) {
variant.resValue "string", "chargeprice_key", chargepriceKey
}
def fronyxKey = env.FRONYX_API_KEY ?: project.findProperty("FRONYX_API_KEY")
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
fronyxKey = decode(project.findProperty("FRONYX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (fronyxKey != null) {
variant.resValue "string", "fronyx_key", fronyxKey
}
}
}
@@ -159,11 +151,11 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.appcompat:appcompat:1.6.0-beta01'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation "androidx.fragment:fragment-ktx:1.5.1"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.6.1'
@@ -171,14 +163,17 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:8e3de307f2'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:dd0167dbff'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
implementation 'com.squareup.moshi:moshi-adapters:1.13.0'
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
implementation 'com.github.johan12345:jsonapi:50d72e7e55' // patched version for jsonapi-adapters
implementation('com.markomilos.jsonapi:jsonapi-retrofit:1.0.1') {
exclude group: 'com.markomilos.jsonapi', module: 'jsonapi-adapters'
}
implementation 'io.coil-kt:coil:1.1.0'
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
@@ -190,7 +185,7 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
def carAppVersion = '1.3.0-beta01'
def carAppVersion = '1.2.0-rc01'
googleImplementation "androidx.car.app:app:$carAppVersion"
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
@@ -262,7 +257,7 @@ dependencies {
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}
private static String decode(String s, String key) {

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
package="net.vonforst.evmap">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />

View File

@@ -31,7 +31,6 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.location.FusionEngine
import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.location.Priority
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.checkFineLocationPermission
@@ -112,49 +111,27 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
}
private val prefs: PreferenceDataSource by lazy {
PreferenceDataSource(carContext)
}
init {
lifecycle.addObserver(this)
}
override fun onCreateScreen(intent: Intent): Screen {
val mapScreen = MapScreen(carContext, this)
val screens = mutableListOf<Screen>(mapScreen)
if (!prefs.dataSourceSet) {
screens.add(
ChooseDataSourceScreen(
carContext,
ChooseDataSourceScreen.Type.CHARGER_DATA_SOURCE,
initialChoice = true,
extraDesc = R.string.data_sources_description
)
)
}
if (!locationPermissionGranted()) {
screens.add(
PermissionScreen(
carContext,
R.string.auto_location_permission_needed,
listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
val screenManager = carContext.getCarService(ScreenManager::class.java)
screenManager.push(mapScreen)
return PermissionScreen(
carContext,
R.string.auto_location_permission_needed,
listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
if (screens.size > 1) {
val screenManager = carContext.getCarService(ScreenManager::class.java)
for (i in 0 until screens.size - 1) {
screenManager.push(screens[i])
}
}
return screens.last()
return mapScreen
}
private fun locationPermissionGranted() = carContext.checkFineLocationPermission()

View File

@@ -22,20 +22,13 @@ import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import net.vonforst.evmap.ui.time
import java.io.IOException
import kotlin.math.roundToInt
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
@@ -48,7 +41,6 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}
private var prices: List<ChargePrice>? = null
private var meta: ChargepriceChargepointMeta? = null
private var chargepoint: Chargepoint? = null
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
@@ -70,24 +62,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
if (prices == null && errorMessage == null) {
setLoading(true)
} else {
val header = meta?.let { meta ->
chargepoint?.let { chargepoint ->
"${
nameForPlugType(
carContext.stringProvider(),
chargepoint.type
)
} ${chargepoint.formatPower()} " + carContext.getString(
R.string.chargeprice_stats,
meta.energy,
time(meta.duration.roundToInt()),
meta.energy / meta.duration * 60
)
}
}
val myTariffs = prefs.chargepriceMyTariffs
val myTariffsAll = prefs.chargepriceMyTariffsAll
val list = ItemList.Builder().apply {
setSingleList(ItemList.Builder().apply {
setNoItemsMessage(
errorMessage ?: carContext.getString(R.string.chargeprice_no_tariffs_found)
)
@@ -95,17 +70,9 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
addItem(Row.Builder().apply {
setTitle(formatProvider(price))
addText(formatPrice(price))
if (carContext.carAppApiLevel >= 5) {
setEnabled(myTariffsAll || myTariffs != null && price.tariffId in myTariffs)
}
}.build())
}
}.build()
if (header != null && list.items.isNotEmpty()) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
}
}.build())
}
setActionStrip(
ActionStrip.Builder().addAction(
@@ -134,14 +101,11 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
carContext.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
// only show the toast "opened on phone" if we're running on a phone
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
}
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
} catch (e: ActivityNotFoundException) {
CarToast.makeText(
carContext,
@@ -215,14 +179,6 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
try {
val car = determineVehicle(manufacturer, modelName)
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
if (cpStation.chargePoints.isEmpty()) {
errorMessage =
carContext.getString(R.string.chargeprice_no_compatible_connectors)
invalidate()
return@launch
}
val result = api.getChargePrices(
ChargepriceRequest(
dataAdapter = dataAdapter,
@@ -257,14 +213,10 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint
// (we have already filtered so that only compatible ones are included)
val chargepoint = cpStation.chargePoints.maxByOrNull { it.power }
val index = cpStation.chargePoints.indexOf(chargepoint)
this@ChargepriceScreen.chargepoint =
charger.chargepoints.filter { equivalentPlugTypes(it.type).any { it in car.compatibleEvmapConnectors } }[index]
// choose the highest power chargepoint compatible with the car
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull { it.power }
if (chargepoint == null) {
errorMessage =
carContext.getString(R.string.chargeprice_no_compatible_connectors)
@@ -274,7 +226,11 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
val metaMapped =
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
meta = metaMapped.chargePoints.maxByOrNull { it.power }
meta = metaMapped.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
prices = result.data!!.map { cp ->
val filteredPrices =

View File

@@ -1,10 +1,10 @@
package net.vonforst.evmap.auto
import android.content.Intent
import android.graphics.*
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
@@ -27,27 +27,22 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.awaitFinished
import net.vonforst.evmap.viewmodel.getReferenceData
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import kotlin.math.roundToInt
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
var charger: ChargeLocation? = null
var photo: Bitmap? = null
@@ -55,8 +50,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val imageSize = 128 // images should be 128dp according to docs
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
@@ -74,7 +71,9 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private var favoriteUpdateJob: Job? = null
init {
loadCharger()
referenceData.observe(this) {
loadCharger()
}
}
override fun onGetTemplate(): Template {
@@ -100,29 +99,29 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
).build()
)
.setTitle(carContext.getString(R.string.navigate))
.setFlags(Action.FLAG_PRIMARY)
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener {
navigateToCharger(charger)
}
.build())
if (ChargepriceApi.isChargerSupported(charger)) {
addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_chargeprice
)
).build()
)
.setTitle(carContext.getString(R.string.auto_prices))
charger.chargepriceData?.country?.let { country ->
if (ChargepriceApi.isCountrySupported(country, charger.dataSource)) {
addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_chargeprice
)
).build()
)
.setTitle(carContext.getString(R.string.auto_prices))
.setOnClickListener {
screenManager.push(ChargepriceScreen(carContext, charger))
}
.build())
}
}
} ?: setLoading(true)
}.build()
).apply {
@@ -245,9 +244,15 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val operatorText = generateOperatorText(charger)
setTitle(operatorText)
charger.cost?.let { addText(generateCostStatusText(it)) }
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
charger.faultReport?.let { fault ->
addText(generateFaultReportTitle(fault))
addText(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
}
}.build())
} else {
@@ -262,14 +267,20 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val operatorText = generateOperatorText(charger)
setTitle(operatorText)
charger.cost?.let {
addText(generateCostStatusText(it))
addText(it.getStatusText(carContext, emoji = true))
it.getDetailText()?.let { addText(it) }
}
}.build())
// row 3: fault report (if exists)
charger.faultReport?.let { fault ->
rows.add(Row.Builder().apply {
setTitle(generateFaultReportTitle(fault))
setTitle(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
fault.description?.let {
addText(
HtmlCompat.fromHtml(
@@ -294,79 +305,18 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
return rows
}
private fun generateCostStatusText(cost: Cost): CharSequence {
val string = SpannableString(cost.getStatusText(carContext, emoji = true))
// replace emoji with CarIcon
string.indexOf('⚡').takeIf { it >= 0 }?.let { index ->
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_lightning
)
).setTint(CarColor.YELLOW).build()
), index, index + 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
}
string.indexOf('\uD83C').takeIf { it >= 0 }?.let { index ->
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_parking
)
).setTint(CarColor.BLUE).build()
), index, index + 2, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
}
return string
}
private fun generateFaultReportTitle(fault: FaultReport): CharSequence {
val string = SpannableString(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
// replace emoji with CarIcon
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_fault_report
)
).setTint(CarColor.YELLOW).build()
), 0, 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
return string
}
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
if (i > 0) chargepointsText.append(" · ")
chargepointsText.append(
"${cp.count}× "
).append(
nameForPlugType(carContext.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
iconForPlugType(cp.type)
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
).append(" ").append(cp.formatPower())
"${cp.count}× ${
nameForPlugType(
carContext.stringProvider(),
cp.type
)
} ${cp.formatPower()}"
)
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",
@@ -406,21 +356,24 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
}
private fun loadCharger() {
val referenceData = referenceData.value ?: return
lifecycleScope.launch {
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
val response = repo.getChargepointDetail(chargerSparse.id).awaitFinished()
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
if (response.status == Status.SUCCESS) {
val charger = response.data!!
val photo = charger.photos?.firstOrNull()
photo?.let {
val density = carContext.resources.displayMetrics.density
val size =
(density * if (largeImageSupported) imageSizeLarge else imageSize).roundToInt()
val url = photo.getUrl(size = size)
val url = if (largeImageSupported) {
photo.getUrl(size = (imageSizeLarge * density).roundToInt())
} else {
photo.getUrl(size = (imageSize * density).roundToInt())
}
val request = ImageRequest.Builder(carContext).data(url).build()
val img =
var img =
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
// draw icon on top of image
@@ -430,29 +383,19 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
multi = charger.isMulti()
)
val outImg = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
img = img.copy(Bitmap.Config.ARGB_8888, true)
val iconSmall = icon.scale(
(size * 0.4 / icon.height * icon.width).roundToInt(),
(size * 0.4).roundToInt()
)
val canvas = Canvas(outImg)
val m = Matrix()
m.setRectToRect(
RectF(0f, 0f, img.width.toFloat(), img.height.toFloat()),
RectF(0f, 0f, size.toFloat(), size.toFloat()),
Matrix.ScaleToFit.CENTER
)
canvas.drawBitmap(
img.copy(Bitmap.Config.ARGB_8888, false), m, null
(img.height * 0.4 / icon.height * icon.width).roundToInt(),
(img.height * 0.4).roundToInt()
)
val canvas = Canvas(img)
canvas.drawBitmap(
iconSmall,
0f,
(size - iconSmall.height * 1.1).toFloat(),
(img.height - iconSmall.height * 1.1).toFloat(),
null
)
this@ChargerDetailScreen.photo = outImg
this@ChargerDetailScreen.photo = img
}
this@ChargerDetailScreen.charger = charger

View File

@@ -47,6 +47,30 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
setHeaderAction(Action.BACK)
setActionStrip(
ActionStrip.Builder().apply {
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (prefs.placeSearchResultAndroidAuto != null) {
R.drawable.ic_search_off
} else {
R.drawable.ic_search
}
)
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
screenManager.pop()
} else {
screenManager.push(PlaceSearchScreen(carContext, session))
}
})
}.build())
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(

View File

@@ -2,24 +2,18 @@ package net.vonforst.evmap.auto
import android.content.pm.PackageManager
import android.location.Location
import android.os.Handler
import android.os.Looper
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.*
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
import net.vonforst.evmap.BuildConfig
@@ -30,25 +24,19 @@ import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterWithValue
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.bearingBetween
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.utils.headingDiff
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.awaitFinished
import net.vonforst.evmap.viewmodel.filtersWithValue
import net.vonforst.evmap.viewmodel.getFilterValues
import net.vonforst.evmap.viewmodel.getReferenceData
import java.io.IOException
import java.time.Duration
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
@@ -72,33 +60,33 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private var location: Location? = null
private var lastDistanceUpdateTime: Instant? = null
private var chargers: List<ChargeLocation>? = null
private var loadingError = false
private var locationError = false
private var prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
private val searchRadius = 5 // kilometers
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
HashMap()
private val maxRows = if (ctx.carAppApiLevel >= 2) {
min(
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST),
25
)
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST)
} else 6
private var filterStatus = prefs.filterStatus
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
private val carInfo: CarInfo by lazy {
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData<Long>().apply {
value = prefs.filterStatus
}
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val filters =
Transformations.map(referenceData) { api.getFilters(it, carContext.stringProvider()) }
private val filtersWithValue = filtersWithValue(filters, filterValues)
private val hardwareMan: CarHardwareManager by lazy {
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
}
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
private var energyLevel: EnergyLevel? = null
private var heading: Compass? = null
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
listOf(
"android.car.permission.CAR_ENERGY",
@@ -119,36 +107,32 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
override fun onGetTemplate(): Template {
session.requestLocationUpdates()
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
setTitle(
prefs.placeSearchResultAndroidAutoName?.let {
carContext.getString(R.string.auto_chargers_near_location, it)
} ?: carContext.getString(
if (filterStatus == FILTERS_FAVORITES) {
if (filterStatus.value == FILTERS_FAVORITES) {
R.string.auto_favorites
} else {
R.string.auto_chargers_closeby
}
)
)
if (prefs.placeSearchResultAndroidAutoName != null) {
searchLocation?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
if (prefs.placeSearchResultAndroidAutoName != null) {
setMarker(
PlaceMarker.Builder()
.setColor(CarColor.PRIMARY)
.build()
)
}
}.build())
}
} else {
location?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
}
}
searchLocation?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
if (prefs.placeSearchResultAndroidAutoName != null) {
setMarker(
PlaceMarker.Builder()
.setColor(CarColor.PRIMARY)
.build()
)
}
}.build())
} ?: setLoading(true)
chargers?.take(maxRows)?.let { chargerList ->
val builder = ItemList.Builder()
// only show the city if not all chargers are in the same city
@@ -158,7 +142,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
builder.setNoItemsMessage(
carContext.getString(
if (filterStatus == FILTERS_FAVORITES) {
if (filterStatus.value == FILTERS_FAVORITES) {
R.string.auto_no_favorites_found
} else {
R.string.auto_no_chargers_found
@@ -167,38 +151,21 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
builder.setOnItemsVisibilityChangedListener(this@MapScreen)
setItemList(builder.build())
} ?: run {
if (loadingError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.connection_error)
)
setItemList(builder.build())
} else if (locationError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.location_error)
)
setItemList(builder.build())
} else {
setLoading(true)
}
}
} ?: setLoading(true)
setCurrentLocationEnabled(true)
setHeaderAction(Action.APP_ICON)
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
filtersWithValue?.count {
val filtersCount = if (filterStatus.value == FILTERS_FAVORITES) 1 else {
filtersWithValue.value?.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
}
}
setActionStrip(
ActionStrip.Builder()
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
.addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_settings
)
@@ -209,42 +176,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
session.mapScreen = null
}
.build())
.addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (prefs.placeSearchResultAndroidAuto != null) {
R.drawable.ic_search_off
} else {
R.drawable.ic_search
}
)
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
screenManager.pushForResult(DummyReturnScreen(carContext)) {
chargers = null
loadChargers()
}
} else {
screenManager.pushForResult(
PlaceSearchScreen(
carContext,
session
)
) {
chargers = null
loadChargers()
}
session.mapScreen = null
}
})
}.build())
.addAction(
Action.Builder()
.setIcon(
@@ -258,14 +189,14 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
.build()
)
.setOnClickListener {
screenManager.push(FilterScreen(carContext, session))
screenManager.pushForResult(FilterScreen(carContext, session)) {
filterStatus.value = prefs.filterStatus
}
session.mapScreen = null
}
.build())
.build())
if (carContext.carAppApiLevel >= 5) {
setOnContentRefreshListener(this@MapScreen)
}
setOnContentRefreshListener(this@MapScreen)
}.build()
}
@@ -358,10 +289,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
) {
return
}
val previousLocation = this.location
this.location = location
if (previousLocation == null) {
loadChargers()
if (updateCoroutine != null) {
// don't update while still loading last update
return
}
val now = Instant.now()
@@ -376,22 +307,17 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private fun loadChargers() {
val location = location ?: return
val referenceData = referenceData.value ?: return
val filters = filtersWithValue.value ?: return
val searchLocation =
prefs.placeSearchResultAndroidAuto ?: LatLng.fromLocation(location)
this.searchLocation = searchLocation
updateCoroutine = lifecycleScope.launch {
loadingError = false
try {
filterStatus = prefs.filterStatus
val filterValues =
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
val filters = repo.getFiltersAsync(carContext.stringProvider())
filtersWithValue = filtersWithValue(filters, filterValues)
// load chargers
if (filterStatus == FILTERS_FAVORITES) {
if (filterStatus.value == FILTERS_FAVORITES) {
chargers =
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
distanceBetween(
@@ -400,102 +326,60 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
}
} else {
// try multiple search radii until we have enough chargers
var chargers: List<ChargeLocation>? = null
for (radius in listOf(searchRadius, searchRadius * 10, searchRadius * 50)) {
val response = repo.getChargepointsRadius(
searchLocation,
radius,
zoom = 16f,
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR) {
loadingError = true
invalidate()
return@launch
}
chargers = headingFilter(
response.data?.filterIsInstance(ChargeLocation::class.java),
searchLocation
)
if (chargers == null || chargers.size >= maxRows) {
break
val response = api.getChargepointsRadius(
referenceData,
searchLocation,
searchRadius,
zoom = 16f,
filters
)
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
chargers?.let {
if (it.size < maxRows) {
// try again with larger radius
val response = api.getChargepointsRadius(
referenceData,
searchLocation,
searchRadius * 10,
zoom = 16f,
filters
)
chargers =
response.data?.filterIsInstance(ChargeLocation::class.java)
}
}
this@MapScreen.chargers = chargers
}
updateCoroutine = null
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
loadingError = true
invalidate()
withContext(Dispatchers.Main) {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()
}
}
}
}
/**
* Filters by heading if heading available and enabled
*/
private fun headingFilter(
chargers: List<ChargeLocation>?,
searchLocation: LatLng
): List<ChargeLocation>? =
heading?.orientations?.value?.get(0)?.let { heading ->
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
chargers?.filter {
val bearing = bearingBetween(
searchLocation.latitude,
searchLocation.longitude,
it.coordinates.lat,
it.coordinates.lng
)
val diff = headingDiff(bearing, heading.toDouble())
abs(diff) < 30
}
} ?: chargers
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
val isUpdate = this.energyLevel == null
this.energyLevel = energyLevel
if (isUpdate) invalidate()
}
private fun onCompassUpdated(compass: Compass) {
this.heading = compass
}
override fun onStart(owner: LifecycleOwner) {
setupListeners()
session.requestLocationUpdates()
locationError = false
Handler(Looper.getMainLooper()).postDelayed({
if (location == null) {
locationError = true
invalidate()
}
}, 5000)
// Reloading chargers in onStart does not seem to count towards content limit.
// So let's do this so the user gets fresh chargers when re-entering the app.
if (prefs.dataSource != repo.api.value?.id) {
repo.api.value = createApi(prefs.dataSource, carContext)
}
invalidate()
loadChargers()
filtersWithValue.observe(this@MapScreen) {
loadChargers()
}
}
private fun setupListeners() {
val exec = ContextCompat.getMainExecutor(carContext)
if (supportsCarApiLevel3(carContext)) {
carSensors.addCompassListener(
CarSensors.UPDATE_RATE_NORMAL,
exec,
::onCompassUpdated
)
}
if (!permissions.all {
ContextCompat.checkSelfPermission(
carContext,
@@ -506,7 +390,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
if (supportsCarApiLevel3(carContext)) {
println("Setting up energy level listener")
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
val exec = ContextCompat.getMainExecutor(carContext)
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
}
}
@@ -517,15 +402,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
// (i.e. onGetTemplate is not called while the old data is still there)
chargers = null
availabilities.clear()
location = null
removeListeners()
}
private fun removeListeners() {
if (supportsCarApiLevel3(carContext)) {
println("Removing energy level listener")
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
carSensors.removeCompassListener(::onCompassUpdated)
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
}
}

View File

@@ -19,8 +19,6 @@ import kotlin.math.max
import kotlin.math.min
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.auto_settings))
@@ -75,14 +73,6 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
}
.build()
)
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
.setToggle(Toggle.Builder {
prefs.showChargersAheadAndroidAuto = it
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
.build()
)
}
}.build())
}.build()
@@ -92,9 +82,12 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
val db = AppDatabase.getInstance(ctx)
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
val dataSourceValues = carContext.resources.getStringArray(R.array.pref_data_source_values)
val dataSourceDescriptions = listOf(
carContext.getString(R.string.data_source_goingelectric_desc),
carContext.getString(R.string.data_source_openchargemap_desc)
)
val searchProviderNames =
carContext.resources.getStringArray(R.array.pref_search_provider_names)
val searchProviderValues =
@@ -115,9 +108,14 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
screenManager.push(
ChooseDataSourceScreen(
carContext,
ChooseDataSourceScreen.Type.CHARGER_DATA_SOURCE
)
)
R.string.pref_data_source,
dataSourceNames,
dataSourceValues,
prefs.dataSource,
dataSourceDescriptions
) {
prefs.dataSource = it
})
}
}.build())
addItem(Row.Builder().apply {
@@ -131,9 +129,13 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
screenManager.push(
ChooseDataSourceScreen(
carContext,
ChooseDataSourceScreen.Type.SEARCH_PROVIDER
)
)
R.string.pref_search_provider,
searchProviderNames,
searchProviderValues,
prefs.searchProvider
) {
prefs.searchProvider = it
})
}
}.build())
addItem(Row.Builder().apply {
@@ -156,85 +158,34 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
class ChooseDataSourceScreen(
ctx: CarContext,
val type: Type,
val initialChoice: Boolean = false,
@StringRes val extraDesc: Int? = null
@StringRes val title: Int,
val names: Array<String>,
val values: Array<String>,
val currentValue: String,
val descriptions: List<String>? = null,
val callback: (String) -> Unit
) : Screen(ctx) {
enum class Type {
CHARGER_DATA_SOURCE, SEARCH_PROVIDER
}
val prefs = PreferenceDataSource(carContext)
val title = when (type) {
Type.CHARGER_DATA_SOURCE -> R.string.pref_data_source
Type.SEARCH_PROVIDER -> R.string.pref_search_provider
}
val names = when (type) {
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_names)
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_names)
}
val values = when (type) {
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_values)
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_values)
}
val currentValue: String = when (type) {
Type.CHARGER_DATA_SOURCE -> prefs.dataSource
Type.SEARCH_PROVIDER -> prefs.searchProvider
}
val descriptions = when (type) {
Type.CHARGER_DATA_SOURCE -> listOf(
carContext.getString(R.string.data_source_goingelectric_desc),
carContext.getString(R.string.data_source_openchargemap_desc)
)
Type.SEARCH_PROVIDER -> null
}
val callback: (String) -> Unit = when (type) {
Type.CHARGER_DATA_SOURCE -> { it ->
prefs.dataSourceSet = true
prefs.dataSource = it
}
Type.SEARCH_PROVIDER -> { it ->
prefs.searchProvider = it
}
}
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(title))
setHeaderAction(if (initialChoice) Action.APP_ICON else Action.BACK)
val list = ItemList.Builder().apply {
setHeaderAction(Action.BACK)
setSingleList(ItemList.Builder().apply {
for (i in names.indices) {
addItem(Row.Builder().apply {
setTitle(names[i])
descriptions?.let { addText(it[i]) }
if (initialChoice) {
setBrowsable(true)
setOnClickListener {
itemSelected(i)
}
}
}.build())
}
if (!initialChoice) {
setOnSelectedListener {
itemSelected(it)
}
setSelectedIndex(values.indexOf(currentValue))
setOnSelectedListener {
callback(values[it])
screenManager.pop()
}
}.build()
if (extraDesc != null) {
addSectionedList(SectionedItemList.create(list, carContext.getString(extraDesc)))
} else {
setSingleList(list)
}
setSelectedIndex(values.indexOf(currentValue))
}.build())
}.build()
}
private fun itemSelected(i: Int) {
callback(values[i])
screenManager.pop()
}
}
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {

View File

@@ -3,13 +3,13 @@ package net.vonforst.evmap.auto
import android.content.Context
import android.graphics.Bitmap
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
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.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import java.util.*
import kotlin.math.roundToInt
@@ -152,17 +152,4 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
}
}
return true
}
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
/*
Dummy screen to get around template refresh limitations.
It immediately pops back to the previous screen.
*/
override fun onGetTemplate(): Template {
screenManager.pop()
return MessageTemplate.Builder(carContext.getString(R.string.loading)).setLoading(true)
.build()
}
}

View File

@@ -28,10 +28,8 @@
<string name="auto_chargeprice_vehicle_unavailable">EVMap konnte das Fahrzeugmodell nicht erkennen.</string>
<string name="auto_chargeprice_vehicle_unknown">Keins der in der App ausgewählten Fahrzeuge passt zu diesem Fahrzeug (%1$s %2$s).</string>
<string name="auto_chargeprice_vehicle_ambiguous">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%1$s %2$s).</string>
<string name="auto_chargers_ahead">Nur Ladestationen in Fahrtrichtung</string>
<string name="settings_android_auto_chargeprice_range">Ladebereich für Preisvergleich</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="selecting_all">alle Einträge ausgewählt</string>
<string name="selecting_none">alle Einträge abgewählt</string>
<string name="loading">Lade…</string>
</resources>

View File

@@ -28,10 +28,8 @@
<string name="auto_chargeprice_vehicle_unavailable">EVMap could not determine your vehicle model.</string>
<string name="auto_chargeprice_vehicle_unknown">None of the vehicles selected in the app matches this vehicle (%1$s %2$s).</string>
<string name="auto_chargeprice_vehicle_ambiguous">Multiple vehicles selected in the app match this vehicle (%1$s %2$s).</string>
<string name="auto_chargers_ahead">Only chargers along driving direction</string>
<string name="settings_android_auto_chargeprice_range">Charging range for price comparison</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="selecting_all">selected all items</string>
<string name="selecting_none">deselected all items</string>
<string name="loading">Loading…</string>
</resources>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
package="net.vonforst.evmap">
<uses-permission android:name="android.car.permission.CAR_INFO" />
<uses-permission android:name="android.car.permission.CAR_ENERGY" />

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.vonforst.evmap">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

View File

@@ -1,20 +0,0 @@
package net.vonforst.evmap.adapter
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
class SingleViewAdapter(val view: View) : Adapter<SingleViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(view)
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
override fun getItemCount() = 1
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
}
}

View File

@@ -34,8 +34,7 @@ interface ChargepointApi<out T : ReferenceData> {
fun getFilters(referenceData: ReferenceData, sp: StringProvider): List<Filter<FilterValue>>
val name: String
val id: String
fun getName(): String
}
interface StringProvider {

View File

@@ -132,8 +132,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
data class ChargeLocationStatus(
val status: Map<Chargepoint, List<ChargepointStatus>>,
val source: String,
val evseIds: Map<Chargepoint, List<String>>? = null
val source: String
) {
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
val statusFiltered = status.filterKeys {

View File

@@ -152,14 +152,13 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
val connectorStatus = details.flatMap { it.chargePoints }.flatMap { cp ->
cp.connectors.map { connector ->
Triple(connector, cp.status, cp.evseId)
connector to cp.status
}
}
val enbwConnectors = mutableMapOf<Long, Pair<Double, String>>()
val enbwStatus = mutableMapOf<Long, ChargepointStatus>()
val enbwEvseId = mutableMapOf<Long, String>()
connectorStatus.forEachIndexed { index, (connector, statusStr, evseId) ->
connectorStatus.forEachIndexed { index, (connector, statusStr) ->
val id = index.toLong()
val power = connector.maxPowerInKw ?: 0.0
val type = when (connector.plugTypeName) {
@@ -180,22 +179,17 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
"UNSPECIFIED" -> ChargepointStatus.UNKNOWN
else -> ChargepointStatus.UNKNOWN
}
enbwConnectors[id] = power to type
enbwStatus[id] = status
evseId?.let { enbwEvseId[id] = it }
enbwConnectors.put(id, power to type)
enbwStatus.put(id, status)
}
val match = matchChargepoints(enbwConnectors, location.chargepointsMerged)
val chargepointStatus = match.mapValues { entry ->
entry.value.map { enbwStatus[it]!! }
}
val evseIds = if (enbwEvseId.size == enbwStatus.size) match.mapValues { entry ->
entry.value.map { enbwEvseId[it]!! }
} else null
return ChargeLocationStatus(
chargepointStatus,
"EnBW",
evseIds
"EnBW"
)
}

View File

@@ -130,14 +130,13 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
}
val connectorStatus = details.flatMap { it.evses }.flatMap { evse ->
evse.connectors.map { connector ->
Triple(connector, evse.status, evse.evseId)
connector to evse.status
}
}
val nmConnectors = mutableMapOf<Long, Pair<Double, String>>()
val nmStatus = mutableMapOf<Long, ChargepointStatus>()
val nmEvseId = mutableMapOf<Long, String>()
connectorStatus.forEach { (connector, statusStr, evseId) ->
connectorStatus.forEach { (connector, statusStr) ->
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
@@ -162,20 +161,15 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
}
nmConnectors.put(id, power to type)
nmStatus.put(id, status)
evseId?.let { nmEvseId[id] = it }
}
val match = matchChargepoints(nmConnectors, location.chargepointsMerged)
val chargepointStatus = match.mapValues { entry ->
entry.value.map { nmStatus[it]!! }
}
val evseIds = if (nmEvseId.size == nmStatus.size) match.mapValues { entry ->
entry.value.map { nmEvseId[it]!! }
} else null
return ChargeLocationStatus(
chargepointStatus,
"NewMotion",
evseIds
"NewMotion"
)
}

View File

@@ -112,47 +112,18 @@ interface ChargepriceApi {
else -> throw IllegalArgumentException()
}
/**
* Checks if a charger is supported by Chargeprice.
*
* This function just applies some heuristics on the charger's data without making API
* calls. If it returns true, that is not a guarantee that Chargeprice will have information
* on this charger. But if it is false, it is pretty unlikely that Chargeprice will have
* useful data, so we do not show the price comparison button in this case.
*/
@JvmStatic
fun isChargerSupported(charger: ChargeLocation): Boolean {
val dataSourceSupported = charger.dataSource in listOf("goingelectric", "openchargemap")
val countrySupported =
charger.chargepriceData?.country?.let { isCountrySupported(it, charger.dataSource) }
?: false
val networkSupported = charger.chargepriceData?.network?.let {
if (charger.dataSource == "openchargemap") {
it !in listOf(
"1", // unknown operator
"44", // private residence/individual
"45" // business owner at location
)
} else {
true
}
} ?: false
val powerAvailable = charger.chargepoints.all { it.hasKnownPower() }
return dataSourceSupported && countrySupported && networkSupported && powerAvailable
}
private fun isCountrySupported(country: String, dataSource: String): Boolean =
when (dataSource) {
"goingelectric" -> country in listOf(
// list of countries according to Chargeprice.app, 2021/08/24
"Deutschland",
"Österreich",
"Schweiz",
"Frankreich",
"Belgien",
"Niederlande",
"Luxemburg",
"Dänemark",
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
// list of countries updated 2021/08/24
"goingelectric" -> country in listOf(
"Deutschland",
"Österreich",
"Schweiz",
"Frankreich",
"Belgien",
"Niederlande",
"Luxemburg",
"Dänemark",
"Norwegen",
"Schweden",
"Slowenien",
@@ -162,28 +133,9 @@ interface ChargepriceApi {
"Italien",
"Spanien",
"Großbritannien",
"Irland",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
"Finnland",
"Lettland",
"Litauen",
"Estland",
"Liechtenstein",
"Rumänien",
"Slowakei",
"Slowenien",
"Polen",
"Serbien",
"Bulgarien",
"Kosovo",
"Montenegro",
"Albanien",
"Griechenland",
"Portugal",
"Island"
"Irland"
)
"openchargemap" -> country in listOf(
// list of countries according to Chargeprice.app, 2021/08/24
"DE",
"AT",
"CH",
@@ -201,25 +153,7 @@ interface ChargepriceApi {
"IT",
"ES",
"GB",
"IE",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
"FI",
"LV",
"LT",
"EE",
"LI",
"RO",
"SK",
"SI",
"PL",
"RS",
"BG",
"XK",
"ME",
"AL",
"GR",
"PT",
"IS"
"IE"
)
else -> false
}

View File

@@ -1,15 +1,12 @@
package net.vonforst.evmap.api.chargeprice
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.Patterns
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import jsonapi.*
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.equivalentPlugTypes
@@ -181,12 +178,9 @@ data class ChargePrice(
@Json(name = "branding")
val branding: ChargepriceBranding? = null,
@RelationshipsObject
val relationships: @WriteWith<RelationshipsParceler>() Relationships? = null,
) : Equatable, Cloneable, Parcelable {
@ToOne("tariff")
val tariffId: String?
get() = (relationships?.get("tariff") as? Relationship.ToOne)?.data?.id
) : Equatable, Cloneable, Parcelable {
fun formatMonthlyFees(ctx: Context): String {
return listOfNotNull(
if (totalMonthlyFee > 0) {
@@ -199,70 +193,6 @@ data class ChargePrice(
}
}
/**
* Parceler implementation for the Relationships object.
* Note that this ignores certain fields that we don't need (links, meta, etc.)
*/
internal object RelationshipsParceler : Parceler<Relationships?> {
override fun create(parcel: Parcel): Relationships? {
if (parcel.readInt() == 0) return null
val nMembers = parcel.readInt()
val members = (0 until nMembers).map { _ ->
val key = parcel.readString()!!
val value = if (parcel.readInt() == 0) {
val type = parcel.readString()
val id = parcel.readString()
val ri = if (type != null && id != null) {
ResourceIdentifier(type, id)
} else null
Relationship.ToOne(ri)
} else {
val size = parcel.readInt()
val ris = (0 until size).map { _ ->
val type = parcel.readString()!!
val id = parcel.readString()!!
ResourceIdentifier(type, id)
}
Relationship.ToMany(ris)
}
key to value
}.toMap()
return Relationships(members)
}
override fun Relationships?.write(parcel: Parcel, flags: Int) {
if (this == null) {
parcel.writeInt(0)
return
} else {
parcel.writeInt(1)
}
parcel.writeInt(members.size)
for (member in this.members) {
parcel.writeString(member.key)
when (val value = member.value) {
is Relationship.ToOne -> {
parcel.writeInt(0)
parcel.writeString(value.data?.type)
parcel.writeString(value.data?.id)
}
is Relationship.ToMany -> {
parcel.writeInt(1)
parcel.writeInt(value.data.size)
for (ri in value.data) {
parcel.writeString(ri.type)
parcel.writeString(ri.id)
}
}
}
}
}
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepointPrice(

View File

@@ -1,13 +0,0 @@
package net.vonforst.evmap.api.fronyx
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.time.ZonedDateTime
internal class ZonedDateTimeAdapter {
@FromJson
fun fromJson(value: String): ZonedDateTime? = ZonedDateTime.parse(value)
@ToJson
fun toJson(value: ZonedDateTime): String = value.toString()
}

View File

@@ -1,87 +0,0 @@
package net.vonforst.evmap.api.fronyx
import android.content.Context
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface FronyxApi {
@GET("predictions/evse-id/{evseId}")
suspend fun getPredictionsForEvseId(
@Path("evseId") evseId: String,
@Query("timeframe") timeframe: Int? = null
): FronyxEvseIdResponse
companion object {
private val cacheSize = 1L * 1024 * 1024 // 1MB
private val moshi = Moshi.Builder()
.add(ZonedDateTimeAdapter())
.build()
fun create(
apikey: String,
baseurl: String = "https://api.fronyx.io/api/",
context: Context? = null
): FronyxApi {
val client = OkHttpClient.Builder().apply {
addInterceptor { chain ->
// add API key to every request
val original = chain.request()
val new = original.newBuilder()
.header("X-API-Token", apikey)
.header("Content-Type", "application/json")
.build()
chain.proceed(new)
}
if (BuildConfig.DEBUG) {
addNetworkInterceptor(StethoInterceptor())
}
if (context != null) {
cache(Cache(context.getCacheDir(), cacheSize))
}
}.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseurl)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(client)
.build()
return retrofit.create(FronyxApi::class.java)
}
/**
* Checks if a chargepoint is supported by Fronyx.
*
* This function just applies some heuristics on the charger's data without making API
* calls. If it returns true, that is not a guarantee that Fronyx will have information
* on this chargepoint. But if it is false, it is pretty unlikely that Fronyx will have
* useful data, so we do not try to load the data in this case.
*/
fun isChargepointSupported(charger: ChargeLocation, chargepoint: Chargepoint): Boolean {
if (charger.address?.country !in listOf("Deutschland", "Germany")) {
// fronyx only predicts for chargers in Germany for now
return false
}
if (chargepoint.type !in listOf(
Chargepoint.CCS_UNKNOWN,
Chargepoint.CCS_TYPE_2,
Chargepoint.CHADEMO
)
) {
// fronyx only predicts DC chargers for now
return false
}
return true
}
}
}

View File

@@ -1,20 +0,0 @@
package net.vonforst.evmap.api.fronyx
import com.squareup.moshi.JsonClass
import java.time.ZonedDateTime
@JsonClass(generateAdapter = true)
data class FronyxEvseIdResponse(
val evseId: String,
val predictions: List<FronyxPrediction>
)
@JsonClass(generateAdapter = true)
data class FronyxPrediction(
val timestamp: ZonedDateTime,
val status: FronyxStatus
)
enum class FronyxStatus {
AVAILABLE, UNAVAILABLE
}

View File

@@ -129,8 +129,7 @@ class GoingElectricApiWrapper(
private val clusterThreshold = 11f
val api = GoingElectricApi.create(apikey, baseurl, context)
override val name = "GoingElectric.de"
override val id = "going_electric"
override fun getName() = "GoingElectric.de"
override suspend fun getChargepoints(
referenceData: ReferenceData,

View File

@@ -108,8 +108,7 @@ class OpenChargeMapApiWrapper(
private val clusterThreshold = 11
val api = OpenChargeMapApi.create(apikey, baseurl, context)
override val name = "OpenChargeMap.org"
override val id = "open_charge_map"
override fun getName() = "OpenChargeMap.org"
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
if (value == null || value.all) null else value.values.joinToString(",")

View File

@@ -0,0 +1,74 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.FragmentChargepriceFeedbackBinding
import net.vonforst.evmap.viewmodel.ChargepriceFeedbackViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class ChargepriceFeedbackFragment : Fragment() {
private lateinit var binding: FragmentChargepriceFeedbackBinding
private val vm: ChargepriceFeedbackViewModel by viewModels(factoryProducer = {
viewModelFactory {
ChargepriceFeedbackViewModel(
requireActivity().application,
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url)
)
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val fragmentArgs: ChargepriceFeedbackFragmentArgs by navArgs()
vm.feedbackType.value = fragmentArgs.feedbackType
vm.charger.value = fragmentArgs.charger
vm.vehicle.value = fragmentArgs.vehicle
vm.chargePrices.value = fragmentArgs.chargePrices?.toList()
vm.batteryRange.value = fragmentArgs.batteryRange?.toList()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_chargeprice_feedback, container, false
)
binding.lifecycleOwner = this
binding.vm = vm
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setupWithNavController(
findNavController(),
(requireActivity() as MapsActivity).appBarConfiguration
)
binding.tariffSpinner.setAdapter(
ArrayAdapter<String>(
requireContext(),
R.layout.item_simple_multiline,
R.id.text,
mutableListOf()
)
)
}
}

View File

@@ -13,7 +13,6 @@ import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.color.MaterialColors
@@ -25,14 +24,13 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.ChargepriceAdapter
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
import net.vonforst.evmap.adapter.SingleViewAdapter
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.databinding.FragmentChargepriceHeaderBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.ChargepriceFeedbackType
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
@@ -40,7 +38,6 @@ import java.text.NumberFormat
class ChargepriceFragment : Fragment() {
private lateinit var binding: FragmentChargepriceBinding
private lateinit var headerBinding: FragmentChargepriceHeaderBinding
private var connectionErrorSnackbar: Snackbar? = null
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
@@ -95,14 +92,8 @@ class ChargepriceFragment : Fragment() {
inflater,
R.layout.fragment_chargeprice, container, false
)
headerBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_chargeprice_header, container, false
)
binding.lifecycleOwner = this
binding.vm = vm
headerBinding.lifecycleOwner = this
headerBinding.vm = vm
binding.toolbar.inflateMenu(R.menu.chargeprice)
binding.toolbar.setTitle(R.string.chargeprice_title)
@@ -127,7 +118,7 @@ class ChargepriceFragment : Fragment() {
}
val vehicleAdapter = CheckableChargepriceCarAdapter()
headerBinding.vehicleSelection.adapter = vehicleAdapter
binding.vehicleSelection.adapter = vehicleAdapter
val vehicleObserver: Observer<ChargepriceCar> = Observer {
vehicleAdapter.setCheckedItem(it)
}
@@ -143,12 +134,8 @@ class ChargepriceFragment : Fragment() {
(requireActivity() as MapsActivity).openUrl(it.url)
}
}
val joinedAdapter = ConcatAdapter(
SingleViewAdapter(headerBinding.root),
chargepriceAdapter
)
binding.chargePricesList.apply {
adapter = joinedAdapter
adapter = chargepriceAdapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
@@ -166,9 +153,6 @@ class ChargepriceFragment : Fragment() {
vm.myTariffsAll.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffsAll = it
}
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
chargepriceAdapter.submitList(it.data)
}
val connectorsAdapter = CheckableConnectorAdapter()
@@ -187,7 +171,7 @@ class ChargepriceFragment : Fragment() {
plugs?.flatMap { plug -> equivalentPlugTypes(plug) }
}
headerBinding.connectorsList.apply {
binding.connectorsList.apply {
adapter = connectorsAdapter
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
@@ -199,13 +183,16 @@ class ChargepriceFragment : Fragment() {
binding.btnSettings.setOnClickListener {
findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment)
}
binding.btnFeedbackMissingPrice.setOnClickListener {
feedbackMissingPrice()
}
headerBinding.batteryRange.setLabelFormatter { value: Float ->
binding.batteryRange.setLabelFormatter { value: Float ->
val fmt = NumberFormat.getNumberInstance()
fmt.maximumFractionDigits = 0
fmt.format(value.toDouble()) + "%"
}
headerBinding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
binding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> vm.batteryRangeSliderDragging.value = true
MotionEvent.ACTION_UP -> vm.batteryRangeSliderDragging.value = false
@@ -219,6 +206,14 @@ class ChargepriceFragment : Fragment() {
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
true
}
R.id.menu_feedback_missing_price -> {
feedbackMissingPrice()
true
}
R.id.menu_feedback_wrong_price -> {
feedbackWrongPrice()
true
}
else -> false
}
}
@@ -252,4 +247,30 @@ class ChargepriceFragment : Fragment() {
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
}
private fun feedbackMissingPrice() {
findNavController().navigate(
R.id.action_chargeprice_to_chargepriceFeedbackFragment,
ChargepriceFeedbackFragmentArgs(
ChargepriceFeedbackType.MISSING_PRICE,
vm.charger.value,
vm.vehicle.value,
vm.chargePricesForChargepoint.value?.data?.toTypedArray(),
vm.batteryRange.value?.toFloatArray()
).toBundle()
)
}
private fun feedbackWrongPrice() {
findNavController().navigate(
R.id.action_chargeprice_to_chargepriceFeedbackFragment,
ChargepriceFeedbackFragmentArgs(
ChargepriceFeedbackType.WRONG_PRICE,
vm.charger.value,
vm.vehicle.value,
vm.chargePricesForChargepoint.value?.data?.toTypedArray(),
vm.batteryRange.value?.toFloatArray()
).toBundle()
)
}
}

View File

@@ -56,7 +56,8 @@ import com.google.android.material.transition.MaterialContainerTransform
import com.google.android.material.transition.MaterialFadeThrough
import com.google.android.material.transition.MaterialSharedAxis
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.*
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.stfalcon.imageviewer.StfalconImageViewer
import io.michaelrocks.bimap.HashBiMap
@@ -71,6 +72,7 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.bold
@@ -130,11 +132,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val state = bottomSheetBehavior.state
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
bottomSheetBehavior.state = STATE_COLLAPSED
} else if (state == STATE_COLLAPSED) {
vm.chargerSparse.value = null
} else if (state == STATE_HIDDEN) {
@@ -240,9 +238,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return binding.root
}
val bottomSheetCollapsible
get() = resources.getBoolean(R.bool.bottom_sheet_collapsible)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
@@ -258,7 +253,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.detailView.topPart.doOnNextLayout {
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
}
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
setupObservers()
setupClickListeners()
@@ -376,25 +370,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
null, extras
)
}
binding.detailView.imgPredictionSource.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.fronyx_url))
}
binding.detailView.btnPredictionHelp.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.prediction_help))
.setPositiveButton(R.string.ok) { _, _ -> }
.show()
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
}
setupSearchAutocomplete()
binding.detailAppBar.toolbar.setNavigationOnClickListener {
if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
bottomSheetBehavior.state = STATE_COLLAPSED
}
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
@@ -413,7 +394,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.charger.value?.data
if (charger?.editUrl != null) {
(activity as? MapsActivity)?.openUrl(charger.editUrl)
if (vm.apiId.value == "going_electric") {
if (vm.apiType == GoingElectricApiWrapper::class.java) {
// instructions specific to GoingElectric
Toast.makeText(
requireContext(),
@@ -573,8 +554,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
if (it != null) {
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
bottomSheetBehavior.state =
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT
bottomSheetBehavior.state = STATE_COLLAPSED
}
removeSearchFocus()
binding.fabDirections.show()

View File

@@ -26,9 +26,7 @@ abstract class LocationEngine(protected val context: Context) {
*/
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun requestLocationUpdates(priority: Priority, intervalMs: Long, listener: LocationListener) {
if (!requests.any { it.listener == listener }) {
requests.add(LocationRequest(priority, intervalMs, listener))
}
requests.add(LocationRequest(priority, intervalMs, listener))
enable()
}

View File

@@ -1,125 +1,34 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.CoroutineScope
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.await
import androidx.lifecycle.LiveData
import androidx.room.*
import net.vonforst.evmap.model.ChargeLocation
@Dao
abstract class ChargeLocationsDao {
interface ChargeLocationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(vararg locations: ChargeLocation)
suspend fun insert(vararg locations: ChargeLocation)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertBlocking(vararg locations: ChargeLocation)
@Delete
abstract suspend fun delete(vararg locations: ChargeLocation)
}
suspend fun delete(vararg locations: ChargeLocation)
/**
* The ChargeLocationsRepository wraps the ChargepointApi and the DB to provide caching
* functionality.
*/
class ChargeLocationsRepository(
api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
private val db: AppDatabase, private val prefs: PreferenceDataSource
) {
val api = MutableLiveData<ChargepointApi<ReferenceData>>().apply { value = api }
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocations(): LiveData<List<ChargeLocation>>
val referenceData = this.api.switchMap { api ->
when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
@Query("SELECT * FROM chargelocation")
suspend fun getAllChargeLocationsAsync(): List<ChargeLocation>
private val chargeLocationsDao = db.chargeLocationsDao()
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
fun getChargepoints(
bounds: LatLngBounds,
zoom: Float,
filters: FilterValues?
): LiveData<Resource<List<ChargepointListItem>>> {
return liveData {
val refData = referenceData.await()
val result = api.value!!.getChargepoints(refData, bounds, zoom, filters)
emit(result)
}
}
fun getChargepointsRadius(
location: LatLng,
radius: Int,
zoom: Float,
filters: FilterValues?
): LiveData<Resource<List<ChargepointListItem>>> {
return liveData {
val refData = referenceData.await()
val result = api.value!!.getChargepointsRadius(refData, location, radius, zoom, filters)
emit(result)
}
}
fun getChargepointDetail(
id: Long
): LiveData<Resource<ChargeLocation>> {
return liveData {
emit(Resource.loading(null))
val refData = referenceData.await()
val result = api.value!!.getChargepointDetail(refData, id)
emit(result)
}
}
fun getFilters(sp: StringProvider) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { refData: ReferenceData? ->
refData?.let { value = api.value!!.getFilters(refData, sp) }
}
}
suspend fun getFiltersAsync(sp: StringProvider): List<Filter<FilterValue>> {
val refData = referenceData.await()
return api.value!!.getFilters(refData, sp)
}
val chargeCardMap by lazy {
referenceData.map { refData: ReferenceData? ->
if (refData is GEReferenceData) {
refData.chargecards.associate {
it.id to it.convert()
}
} else {
null
}
}
}
@Query("SELECT * FROM chargelocation WHERE lat >= :lat1 AND lat <= :lat2 AND lng >= :lng1 AND lng <= :lng2")
suspend fun getChargeLocationsInBoundsAsync(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double
): List<ChargeLocation>
}

View File

@@ -64,7 +64,6 @@ abstract class FilterValueDao {
}
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
emit(null)
emit(getFilterValuesAsync(filterStatus, dataSource))
}

View File

@@ -87,7 +87,6 @@ class GEReferenceDataRepository(
val networks = dao.getAllNetworks()
val chargeCards = dao.getAllChargeCards()
return MediatorLiveData<GEReferenceData>().apply {
value = null
listOf(chargeCards, networks, plugs).map { source ->
addSource(source) { _ ->
val p = plugs.value ?: return@addSource

View File

@@ -79,7 +79,6 @@ class OCMReferenceDataRepository(
val countries = dao.getAllCountries()
val operators = dao.getAllOperators()
return MediatorLiveData<OCMReferenceData>().apply {
value = null
listOf(countries, connectionTypes, operators).map { source ->
addSource(source) { _ ->
val ct = connectionTypes.value

View File

@@ -246,13 +246,4 @@ class PreferenceDataSource(val context: Context) {
set(value) {
sp.edit().putString("place_search_result_android_auto_name", value).apply()
}
var showChargersAheadAndroidAuto: Boolean
get() = sp.getBoolean("show_chargers_ahead_android_auto", false)
set(value) {
sp.edit().putBoolean("show_chargers_ahead_android_auto", value).apply()
}
val predictionEnabled: Boolean
get() = sp.getBoolean("prediction_enabled", true)
}

View File

@@ -1,345 +0,0 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.graphics.*
import android.text.Layout
import android.text.SpannableString
import android.text.StaticLayout
import android.text.TextPaint
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import net.vonforst.evmap.R
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val dp = context.resources.displayMetrics.density
private val sp = context.resources.displayMetrics.scaledDensity
var zeroHeight = 4 * dp
var barWidth = (16 * dp).roundToInt()
var barMargin = (2 * dp).roundToInt()
var legendWidth = 12 * dp
var legendLineLength = 4 * dp
var legendLineWidth = 1 * dp
var dashLength = 4 * dp
var bubbleTextSize = (12 * sp).roundToInt()
var bubblePadding = (6 * dp).roundToInt()
var selectedBar: Int = 0
var bubbleStrokeWidth = 1 * dp
var barDrawable =
AppCompatResources.getDrawable(context, R.drawable.bar_graph)!!
var colorAvailable = ContextCompat.getColor(context, R.color.available)
var colorUnavailable = ContextCompat.getColor(context, R.color.unavailable)
var data: Map<ZonedDateTime, Int>? = null
set(value) {
field = value
invalidate()
}
var maxValue: Int? = null
set(value) {
field = value
invalidate()
}
var activeAlpha = 0.87f
var inactiveAlpha = 0.60f
private val legendPaint = Paint().apply {
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
color = ta.getColor(0, 0)
strokeWidth = legendLineWidth
textSize = legendWidth - legendLineLength
}
private val legendDashedPaint = Paint().apply {
set(legendPaint)
alpha = (inactiveAlpha * 255).roundToInt()
style = Paint.Style.STROKE
pathEffect = DashPathEffect(floatArrayOf(dashLength, dashLength), 0f)
strokeWidth = 1f
}
private val bubblePaint = Paint().apply {
set(legendPaint)
alpha = (inactiveAlpha * 255).roundToInt()
style = Paint.Style.STROKE
strokeWidth = bubbleStrokeWidth
}
private val bubbleTextPaint = TextPaint().apply {
set(legendPaint)
textSize = bubbleTextSize.toFloat()
}
private lateinit var graphBounds: Rect
private lateinit var bubbleBounds: Rect
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
val bottom = (paddingBottom + legendWidth).roundToInt()
val left = (paddingLeft + legendWidth).roundToInt()
val right = (paddingRight + legendWidth).roundToInt()
val top = (paddingTop + bubbleStrokeWidth / 2).roundToInt()
val bubbleTextHeight = bubbleTextPaint.fontMetrics.run { descent - ascent }
val bubbleHeight = (bubbleTextHeight + 3 * bubblePadding).roundToInt()
val bubbleLeft = (paddingLeft + bubbleStrokeWidth / 2).roundToInt()
val bubbleRight = (paddingRight + bubbleStrokeWidth / 2).roundToInt()
graphBounds = Rect(left, top + bubbleHeight, w - right, h - bottom)
bubbleBounds = Rect(bubbleLeft, top, w - bubbleRight, top + bubbleHeight)
}
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
override fun onDraw(canvas: Canvas) {
if (isInEditMode && data == null) {
// show sample data
val now = ZonedDateTime.now().run {
val minutesRound = ((minute / 15) + 1) * 15
plusMinutes((minutesRound - minute).toLong())
}
data = (0..20).associate {
now.plusMinutes(15L * it) to (Math.random() * 8).roundToInt()
}
maxValue = 8
}
val data = data?.toSortedMap() ?: return
if (data.isEmpty()) return
val maxValue = maxValue ?: data.maxOf { it.value }
drawGraph(canvas, data, maxValue)
drawBubble(canvas, data, maxValue)
}
private fun drawGraph(
canvas: Canvas,
data: SortedMap<ZonedDateTime, Int>,
maxValue: Int
) {
canvas.apply {
drawLine(
graphBounds.left.toFloat(),
graphBounds.top.toFloat(),
graphBounds.right.toFloat(),
graphBounds.top.toFloat(),
legendDashedPaint
)
legendPaint.textAlign = Paint.Align.CENTER
data.entries.forEachIndexed { i, (t, v) ->
val height =
zeroHeight + (graphBounds.height() - zeroHeight) * v.toFloat() / maxValue
val left = graphBounds.left + (barWidth + barMargin) * i
if (left + barWidth > graphBounds.right) return@forEachIndexed
barDrawable.setBounds(
left,
graphBounds.bottom - height.roundToInt(),
left + barWidth,
graphBounds.bottom
)
barDrawable.alpha =
((if (i == selectedBar) activeAlpha else inactiveAlpha) * 255).roundToInt()
barDrawable.setTint(getColor(v, maxValue))
barDrawable.draw(canvas)
val center = left.toFloat() + barWidth / 2
if (t.minute == 0) {
drawLine(
center, graphBounds.bottom.toFloat(),
center, graphBounds.bottom + legendLineLength, legendPaint
)
drawText(
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat),
center, graphBounds.bottom + legendWidth, legendPaint
)
}
if (i == selectedBar) {
drawLine(
center,
graphBounds.bottom - height,
center,
graphBounds.top.toFloat(),
legendDashedPaint
)
}
}
drawLine(
graphBounds.left.toFloat(),
graphBounds.bottom.toFloat(),
graphBounds.right.toFloat(),
graphBounds.bottom.toFloat(),
legendPaint
)
drawLine(
graphBounds.left.toFloat(),
graphBounds.bottom.toFloat(),
graphBounds.right.toFloat(),
graphBounds.bottom.toFloat(),
legendPaint
)
legendPaint.textAlign = Paint.Align.LEFT
drawText(
this@BarGraphView.maxValue.toString(),
graphBounds.right.toFloat() + legendLineLength,
graphBounds.top + (legendWidth - legendLineLength) / 3,
legendPaint
)
}
}
private fun getColor(v: Int, maxValue: Int) =
if (v < maxValue) colorAvailable else colorUnavailable
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
val data = data.toList()
if (data.size <= selectedBar) return
canvas.apply {
val center = graphBounds.left + selectedBar * (barWidth + barMargin) + barWidth * 0.5f
val (t, v) = data[selectedBar]
val tformat = context.getString(
R.string.prediction_time_colon,
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)
)
val availableformat = context.resources.getQuantityString(
R.plurals.prediction_number_available,
maxValue - v,
maxValue - v,
maxValue
)
val text = SpannableString("$tformat $availableformat").apply {
setSpan(
ForegroundColorSpan(getColor(v, maxValue)),
0,
tformat.length + 1,
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
)
setSpan(
StyleSpan(Typeface.BOLD),
0,
tformat.length + 1,
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
)
}
val bubbleTextWidth = StaticLayout.getDesiredWidth(text, bubbleTextPaint)
val bubbleWidth = bubbleTextWidth + 2 * bubblePadding
val bubbleLeft = max(
min(center - bubbleWidth / 2, bubbleBounds.right - bubbleWidth),
bubbleBounds.left.toFloat()
)
val bubblePath = generateBubblePath(
center,
bubbleBounds.bottom.toFloat(),
bubbleLeft,
bubbleBounds.top.toFloat(),
bubbleLeft + bubbleWidth,
(bubbleBounds.bottom - bubblePadding).toFloat(),
bubblePadding.toFloat()
)
drawPath(bubblePath, bubblePaint)
val layout = StaticLayout(
text,
bubbleTextPaint,
ceil(bubbleTextWidth).toInt(),
Layout.Alignment.ALIGN_NORMAL,
1f,
0f,
false
)
canvas.save()
canvas.translate(bubbleLeft + bubblePadding, bubbleBounds.top + bubblePadding.toFloat())
layout.draw(canvas)
canvas.restore()
//drawText(text, 0, text.length, bubbleLeft + bubblePadding, bubbleBounds.top + 2f * bubblePadding, bubbleTextPaint)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x.roundToInt()
val y = event.y.roundToInt()
if (graphBounds.contains(x, y) && event.action == MotionEvent.ACTION_DOWN) {
parent.requestDisallowInterceptTouchEvent(true)
updateSelectedBar(x)
return true
} else if (event.action == MotionEvent.ACTION_MOVE && x > graphBounds.left && y < graphBounds.right) {
updateSelectedBar(x)
return true
} else if (event.action == MotionEvent.ACTION_UP) {
parent.requestDisallowInterceptTouchEvent(false)
return true
}
return super.onTouchEvent(event)
}
private fun updateSelectedBar(x: Int) {
val bar = (x - graphBounds.left) / (barWidth + barMargin)
if (bar != selectedBar) {
selectedBar = bar
invalidate()
}
}
/**
* Generates a path that represents a "speech bubble" with tip position at tipX, tipY,
* bubble bounds left, top, right bottom and corner radius cornerRadius.
*/
private fun generateBubblePath(
tipX: Float,
tipY: Float,
left: Float,
top: Float,
right: Float,
bottom: Float,
cornerRadius: Float
): Path {
val tipWidth = tipY - bottom
return Path().apply {
moveTo(tipX, tipY)
lineTo(min(tipX + tipWidth, right - cornerRadius), bottom)
lineTo(right - cornerRadius, bottom)
arcTo(
right - cornerRadius * 2,
bottom - cornerRadius * 2,
right,
bottom,
90f,
-90f,
false
)
lineTo(right, top + cornerRadius)
arcTo(right - cornerRadius * 2, top, right, top + cornerRadius * 2, 0f, -90f, false)
lineTo(left + cornerRadius, top)
arcTo(left, top, left + cornerRadius * 2, top + cornerRadius * 2, 270f, -90f, false)
lineTo(left, bottom - cornerRadius)
arcTo(
left,
bottom - cornerRadius * 2,
left + cornerRadius * 2,
bottom,
180f,
-90f,
false
)
lineTo(max(tipX - tipWidth, left + cornerRadius), bottom)
close()
}
}
}

View File

@@ -9,6 +9,8 @@ import android.graphics.drawable.LayerDrawable
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
@@ -128,9 +130,18 @@ fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
}
@BindingAdapter("data")
fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
if (recyclerView.adapter is ListAdapter<*, *>) {
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
fun <T> setViewPager2Data(viewPager: ViewPager2, items: List<T>?) {
if (viewPager.adapter is ListAdapter<*, *>) {
(viewPager.adapter as ListAdapter<T, *>).submitList(items)
}
}
@BindingAdapter("data")
fun <T> setAutoCompleteTextViewData(atv: AutoCompleteTextView, items: List<T>?) {
if (atv.adapter is ArrayAdapter<*>) {
val arrayAdapter = atv.adapter as ArrayAdapter<T>
arrayAdapter.clear()
items?.let { arrayAdapter.addAll(it) }
}
}

View File

@@ -47,25 +47,6 @@ fun distanceBetween(
}
fun bearingBetween(startLat: Double, startLng: Double, endLat: Double, endLng: Double): Double {
val dLon = Math.toRadians(-endLng) - Math.toRadians(-startLng)
val originLat = Math.toRadians(startLat)
val destinationLat = Math.toRadians(endLat)
return Math.toDegrees(
atan2(
sin(dLon) * cos(destinationLat),
cos(originLat) * sin(destinationLat) - sin(originLat) * cos(destinationLat) * cos(dLon)
)
)
}
fun headingDiff(h1: Double, h2: Double): Double {
return (h1 - h2 + 540) % 360 - 180
}
fun getLocationFromIntent(intent: Intent): List<Double>? {
val pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
var coords = stringToCoords(pos)

View File

@@ -0,0 +1,129 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import java.io.IOException
enum class ChargepriceFeedbackType {
MISSING_PRICE, WRONG_PRICE, MISSING_VEHICLE
}
class ChargepriceFeedbackViewModel(
application: Application,
chargepriceApiKey: String,
chargepriceApiUrl: String
) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
private var prefs = PreferenceDataSource(application)
// data supplied through fragment args
val feedbackType = MutableLiveData<ChargepriceFeedbackType>()
val charger = MutableLiveData<ChargeLocation>()
val chargepoint = MutableLiveData<Chargepoint>()
val vehicle = MutableLiveData<ChargepriceCar>()
val chargePrices = MutableLiveData<List<ChargePrice>>()
val batteryRange = MutableLiveData<List<Float>>()
// data input by user
val tariff = MutableLiveData<String>()
val price = MutableLiveData<String>()
val notes = MutableLiveData<String>()
val email = MutableLiveData<String>()
val loading = MutableLiveData<Boolean>().apply { value = false }
val chargePricesStrings = chargePrices.map {
it.map {
val name = if (!it.tariffName.lowercase().startsWith(it.provider.lowercase())) {
"${it.provider} ${it.tariffName}"
} else it.tariffName
val price = application.getString(
R.string.charge_price_format,
it.chargepointPrices[0].price,
currency(it.currency)
)
"$name: $price"
}.toList()
}
private val feedback = MediatorLiveData<ChargepriceUserFeedback>().apply {
listOf(
feedbackType,
charger,
chargepoint,
vehicle,
chargePrices,
tariff,
price,
notes,
email
).forEach {
addSource(it) {
try {
value = when (feedbackType.value!!) {
ChargepriceFeedbackType.MISSING_PRICE -> {
ChargepriceMissingPriceFeedback(
tariff.value ?: "",
charger.value?.network?.take(200) ?: "",
price.value ?: "",
charger.value?.let { ChargepriceApi.getPoiUrl(it) } ?: "",
notes.value ?: "",
email.value ?: "",
getChargepriceContext(),
ChargepriceApi.getChargepriceLanguage()
)
}
ChargepriceFeedbackType.WRONG_PRICE -> {
ChargepriceWrongPriceFeedback(
"", // TODO: dropdown value
charger.value?.network?.take(200) ?: "",
"", // TODO: dropdown value
price.value ?: "",
charger.value?.let { ChargepriceApi.getPoiUrl(it) } ?: "",
notes.value ?: "",
email.value ?: "",
getChargepriceContext(),
ChargepriceApi.getChargepriceLanguage()
)
}
ChargepriceFeedbackType.MISSING_VEHICLE -> {
TODO()
}
}
} catch (e: IllegalArgumentException) {
value = null
}
}
}
}
val formValid = feedback.map { it != null }
fun sendFeedback() {
val feedback = feedback.value ?: return
viewModelScope.launch {
loading.value = true
try {
api.userFeedback(feedback)
} catch (e: IOException) {
}
loading.value = false
}
}
private fun getChargepriceContext(): String {
val result = StringBuilder()
vehicle.value?.let { result.append("Vehicle: ${it.brand} ${it.name}\n") }
batteryRange.value?.let { result.append("Battery SOC: ${it[0]} to ${it[1]}\n") }
return result.toString()
}
}

View File

@@ -243,12 +243,6 @@ class ChargepriceViewModel(
}
val cpStation = ChargepriceStation.fromEvmap(charger, compatibleConnectors)
if (cpStation.chargePoints.isEmpty()) {
// no compatible connectors
chargePrices.value = Resource.success(emptyList())
chargePriceMeta.value = Resource.success(ChargepriceMeta(emptyList()))
return
}
loadPricesJob?.cancel()
loadPricesJob = viewModelScope.launch {

View File

@@ -1,44 +1,64 @@
package net.vonforst.evmap.viewmodel
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.switchMap
import net.vonforst.evmap.model.Filter
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.FilterWithValue
import net.vonforst.evmap.storage.FilterValueDao
import kotlinx.coroutines.CoroutineScope
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.*
import kotlin.reflect.full.cast
fun ChargepointApi<ReferenceData>.getReferenceData(
scope: CoroutineScope,
ctx: Context
): LiveData<out ReferenceData> {
val db = AppDatabase.getInstance(ctx)
val prefs = PreferenceDataSource(ctx)
return when (this) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
this,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
this,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>?>
): MediatorLiveData<FilterValues?> =
MediatorLiveData<FilterValues?>().apply {
filterValues: LiveData<List<FilterValue>>
): MediatorLiveData<FilterValues> =
MediatorLiveData<FilterValues>().apply {
listOf(filters, filterValues).forEach {
addSource(it) {
val f = filters.value ?: run {
value = null
return@addSource
val f = filters.value ?: return@addSource
val values = filterValues.value ?: return@addSource
value = f.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
val values = filterValues.value ?: run {
value = null
return@addSource
}
value = filtersWithValue(f, values)
}
}
}
fun filtersWithValue(
filters: List<Filter<FilterValue>>,
values: List<FilterValue>
) = filters.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
fun FilterValueDao.getFilterValues(filterStatus: LiveData<Long>, dataSource: String) =
filterStatus.switchMap {
getFilterValues(it, dataSource)

View File

@@ -8,23 +8,27 @@ import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
class FilterViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
private val api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
private val repo = ChargeLocationsRepository(api, viewModelScope, db, prefs)
private val filters = repo.getFilters(application.stringProvider())
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
private val filterValues: LiveData<List<FilterValue>?> by lazy {
private val referenceData = api.getReferenceData(viewModelScope, application)
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { data ->
value = api.getFilters(data, application.stringProvider())
}
}
private val filterValues: LiveData<List<FilterValue>> by lazy {
db.filterValueDao().getFilterValues(FILTERS_CUSTOM, prefs.dataSource)
}
val filtersWithValue: LiveData<FilterValues?> by lazy {
val filtersWithValue: LiveData<FilterValues> by lazy {
filtersWithValue(filters, filterValues)
}

View File

@@ -7,34 +7,29 @@ import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.fronyx.FronyxApi
import net.vonforst.evmap.api.fronyx.FronyxEvseIdResponse
import net.vonforst.evmap.api.fronyx.FronyxStatus
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OCMConnection
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.distanceBetween
import retrofit2.HttpException
import java.io.IOException
import java.time.ZonedDateTime
@Parcelize
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
@@ -49,24 +44,17 @@ internal fun getClusterDistance(zoom: Float): Int? {
class MapViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
private val repo = ChargeLocationsRepository(
createApi(prefs.dataSource, application),
viewModelScope,
db,
prefs
)
val apiType: Class<ChargepointApi<ReferenceData>>
get() = api.value!!.javaClass
val apiName: String
get() = api.value!!.getName()
val apiId = repo.api.map { it.id }
init {
// necessary so that apiId is updated
apiId.observeForever { }
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
private var api = MutableLiveData<ChargepointApi<ReferenceData>>().apply {
value = createApi(prefs.dataSource, application)
}
val apiName = repo.api.map { it.name }
val bottomSheetState: MutableLiveData<Int> by lazy {
state.getLiveData("bottomSheetState")
}
@@ -83,28 +71,47 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
}
private val filterValues: LiveData<List<FilterValue>?> = repo.api.switchMap {
private val filterValues: LiveData<List<FilterValue>> =
db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val referenceData =
Transformations.switchMap(api) { it.getReferenceData(viewModelScope, application) }
private val filters = Transformations.map(referenceData) {
api.value!!.getFilters(
it,
application.stringProvider()
)
}
private val filters = repo.getFilters(application.stringProvider())
private val filtersWithValue: LiveData<FilterValues?> by lazy {
private val filtersWithValue: LiveData<FilterValues> by lazy {
filtersWithValue(filters, filterValues)
}
val filterProfiles: LiveData<List<FilterProfile>> = repo.api.switchMap {
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
db.filterProfileDao().getProfiles(prefs.dataSource)
}
val chargeCardMap = repo.chargeCardMap
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
MediatorLiveData<Map<Long, ChargeCard>>().apply {
value = null
addSource(referenceData) { data ->
value = if (data is GEReferenceData) {
data.chargecards.map {
it.id to it.convert()
}.toMap()
} else {
null
}
}
}
}
val filtersCount: LiveData<Int> by lazy {
MediatorLiveData<Int>().apply {
value = 0
addSource(filtersWithValue) { filtersWithValue ->
value = filtersWithValue?.count {
value = filtersWithValue.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
} ?: 0
}
}
}
}
@@ -114,7 +121,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
value = Resource.loading(emptyList())
// this is not automatically updated with mapPosition, as we only want to update
// when map is idle.
listOf(filtersWithValue, repo.api).forEach {
listOf(filtersWithValue, referenceData).forEach {
addSource(it) {
reloadChargepoints()
}
@@ -134,17 +141,28 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
state.getLiveData("chargerSparse")
}
val chargerDetails: LiveData<Resource<ChargeLocation>> = chargerSparse.switchMap { charger ->
charger?.id?.let {
repo.getChargepointDetail(it)
}
}.apply {
observeForever { chargerDetail ->
// persist data in case fragment gets recreated
state["chargerDetails"] = chargerDetail
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
value = state["chargerDetails"]
listOf(chargerSparse, referenceData).forEach {
addSource(it) { _ ->
val charger = chargerSparse.value
val refData = referenceData.value
if (charger != null && refData != null) {
if (charger.id != value?.data?.id) {
loadChargerDetails(charger, refData)
}
} else {
value = null
}
}
}
observeForever {
// persist data in case fragment gets recreated
state["chargerDetails"] = it
}
}
}
val charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
addSource(chargerDetails) {
@@ -178,15 +196,15 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val location: MutableLiveData<LatLng> by lazy {
MutableLiveData<LatLng>()
}
private val triggerAvailabilityRefresh = MutableLiveData<Boolean>(true)
val availability: LiveData<Resource<ChargeLocationStatus>> by lazy {
chargerSparse.switchMap { charger ->
charger?.let {
triggerAvailabilityRefresh.switchMap {
liveData {
emit(Resource.loading(null))
emit(getAvailability(charger))
val availability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
addSource(chargerSparse) { charger ->
if (charger != null) {
viewModelScope.launch {
loadAvailability(charger)
}
} else {
value = null
}
}
}
@@ -212,124 +230,6 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
addSource(filteredMinPower, callback)
}
}
val predictionApi = FronyxApi.create(application.getString(R.string.fronyx_key))
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
availability.switchMap { av ->
if (!prefs.predictionEnabled) return@switchMap null
av.data?.evseIds?.let { evseIds ->
liveData {
emit(Resource.loading(null))
val charger = charger.value?.data ?: return@liveData
val allEvseIds =
evseIds.filterKeys {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors.value?.let { filtered ->
equivalentPlugTypes(
it.type
).any { filtered.contains(it) }
} ?: true
}.flatMap { it.value }
try {
val result = allEvseIds.map {
predictionApi.getPredictionsForEvseId(it)
}
emit(Resource.success(result))
println(result)
} catch (e: IOException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: HttpException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: AvailabilityDetectorException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
}
}
} ?: liveData { emit(Resource.success(null)) }
}
}
val predictionGraph: LiveData<Map<ZonedDateTime, Int>?> by lazy {
prediction.map {
it.data?.let { responses ->
if (responses.isEmpty()) {
null
} else {
val evseIds = responses.map { it.evseId }
val groupByTimestamp = responses.flatMap { response ->
response.predictions.map {
Triple(
it.timestamp,
response.evseId,
it.status
)
}
}
.groupBy { it.first } // group by timestamp
.mapValues { it.value.map { it.second to it.third } } // only keep EVSEID and status
.filterValues { it.map { it.first } == evseIds } // remove values where status is not given for all EVSEs
.filterKeys { it > ZonedDateTime.now() } // only show predictions in the future
groupByTimestamp.mapValues {
it.value.count {
it.second == FronyxStatus.UNAVAILABLE
}
}.ifEmpty { null }
}
}
}
}
private val predictedChargepoints = charger.map {
it.data?.let { charger ->
charger.chargepoints.filter {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors.value?.let { filtered ->
equivalentPlugTypes(it.type).any {
filtered.contains(
it
)
}
} ?: true
}
}
}
val predictionMaxValue: LiveData<Int> by lazy {
predictedChargepoints.map {
it?.sumOf { it.count } ?: 0
}
}
val predictionDescription: LiveData<String?> by lazy {
predictedChargepoints.map { predictedChargepoints ->
if (predictedChargepoints == null) return@map null
val allChargepoints = charger.value?.data?.chargepoints ?: return@map null
val predictedChargepointTypes = predictedChargepoints.map { it.type }.distinct()
if (allChargepoints == predictedChargepoints) {
null
} else if (predictedChargepointTypes.size == 1) {
application.getString(
R.string.prediction_only,
nameForPlugType(application.stringProvider(), predictedChargepointTypes[0])
)
} else {
application.getString(
R.string.prediction_only,
application.getString(R.string.prediction_dc_plugs_only)
)
}
}
}
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
@@ -367,9 +267,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
fun reloadPrefs() {
filterStatus.value = prefs.filterStatus
if (prefs.dataSource != apiId.value) {
repo.api.value = createApi(prefs.dataSource, getApplication())
}
api.value = createApi(prefs.dataSource, getApplication())
}
fun toggleFilters() {
@@ -409,7 +307,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
fun reloadChargepoints() {
val pos = mapPosition.value ?: return
val filters = filtersWithValue.value ?: return
chargepointLoader(pos to filters)
val referenceData = referenceData.value ?: return
chargepointLoader(Triple(pos, filters, referenceData))
}
private val miniMarkerThreshold = 13f
@@ -436,16 +335,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}.distinctUntilChanged()
private var chargepointsInternal: LiveData<Resource<List<ChargepointListItem>>>? = null
private var chargepointLoader =
throttleLatest(
500L,
viewModelScope
) { data: Pair<MapPosition, FilterValues> ->
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
chargepoints.value = Resource.loading(chargepoints.value?.data)
val mapPosition = data.first
val filters = data.second
val api = api.value!!
val refData = data.third
if (filterStatus.value == FILTERS_FAVORITES) {
// load favorites from local DB
@@ -468,57 +368,96 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
return@throttleLatest
}
val result = repo.getChargepoints(mapPosition.bounds, mapPosition.zoom, filters)
chargepointsInternal?.let { chargepoints.removeSource(it) }
chargepointsInternal = result
chargepoints.addSource(result) {
chargepoints.value = it
if (api is GoingElectricApiWrapper) {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()
val apiId = apiId.value
when (apiId) {
"going_electric" -> {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
}
"open_charge_map" -> {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
repo.referenceData.value!! as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
}
else -> {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
}
}
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else if (api is OpenChargeMapApiWrapper) {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
refData as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("min_power")
} else {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
}
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
if (result.status == Status.ERROR && result.data == null) {
// keep old results if new data could not be loaded
result = Resource.error(result.message, chargepoints.value?.data)
}
chargepoints.value = result
}
private suspend fun loadAvailability(charger: ChargeLocation) {
availability.value = Resource.loading(null)
availability.value = getAvailability(charger)
}
fun reloadAvailability() {
triggerAvailabilityRefresh.value = true
val charger = chargerSparse.value ?: return
viewModelScope.launch {
loadAvailability(charger)
}
}
private var chargerLoadingTask: Job? = null
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
chargerDetails.value = Resource.loading(null)
chargerLoadingTask?.cancel()
chargerLoadingTask = viewModelScope.launch {
try {
val chargerDetail = api.value!!.getChargepointDetail(referenceData, charger.id)
chargerDetails.value = chargerDetail
if (favorites.value?.any { it.charger.id == chargerDetail.data?.id } == true) {
// update data of stored favorite
db.chargeLocationsDao().insert(charger)
}
} catch (e: IOException) {
chargerDetails.value = Resource.error(e.message, null)
e.printStackTrace()
}
}
}
fun loadChargerById(chargerId: Long) {
chargerDetails.value = Resource.loading(null)
chargerSparse.value = null
repo.getChargepointDetail(chargerId).observeForever { response ->
if (response.status == Status.SUCCESS) {
chargerSparse.value = response.data
referenceData.observeForever(object : Observer<ReferenceData> {
override fun onChanged(refData: ReferenceData) {
referenceData.removeObserver(this)
viewModelScope.launch {
val response = api.value!!.getChargepointDetail(refData, chargerId)
chargerDetails.value = response
if (response.status == Status.SUCCESS) {
chargerSparse.value = response.data
if (response.data != null && favorites.value?.any { it.charger.id == response.data.id } == true) {
// update data of stored favorite
db.chargeLocationsDao().insert(response.data)
}
} else {
chargerSparse.value = null
}
}
}
}
})
}
}

View File

@@ -4,7 +4,10 @@ import android.os.Parcelable
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.util.concurrent.atomic.AtomicBoolean
@@ -104,41 +107,4 @@ fun <T> throttleLatest(
waitingParam = param
}
}
}
public suspend fun <T> LiveData<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T?) {
if (value == null) return
removeObserver(this)
continuation.resume(value, null)
}
}
observeForever(observer)
continuation.invokeOnCancellation {
removeObserver(observer)
}
}
}
public suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<Resource<T>> {
override fun onChanged(value: Resource<T>) {
if (value.status != Status.LOADING) {
removeObserver(this)
continuation.resume(value, null)
}
}
}
observeForever(observer)
continuation.invokeOnCancellation {
removeObserver(observer)
}
}
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/black" />
<corners
android:topLeftRadius="2dp"
android:topRightRadius="2dp" />
</shape>

View File

@@ -1,10 +0,0 @@
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M11,21h-1l1,-7H7.5c-0.58,0 -0.57,-0.32 -0.38,-0.66 0.19,-0.34 0.05,-0.08 0.07,-0.12C8.48,10.94 10.42,7.54 13,3h1l-1,7h3.5c0.49,0 0.56,0.33 0.47,0.51l-0.07,0.15C12.96,17.55 11,21 11,21z" />
</vector>

View File

@@ -1,10 +0,0 @@
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M13,3L6,3v18h4v-6h3c3.31,0 6,-2.69 6,-6s-2.69,-6 -6,-6zM13.2,11L10,11L10,7h3.2c1.1,0 2,0.9 2,2s-0.9,2 -2,2z" />
</vector>

View File

@@ -1,36 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="566.9dp"
android:height="254.9dp"
android:viewportWidth="566.9"
android:viewportHeight="254.9">
<path
android:pathData="M60.8,86.3c-5.6,0 -10,0.5 -13.3,1.4c-3.3,0.9 -5.7,2.7 -7.1,5.2c-0.4,0.6 -0.7,1.3 -0.9,2c-1.1,3.3 -1.4,6.8 -1.4,10.2c0,2.1 0,4.2 0,6.2c0,0.1 0,0.2 0,0.3h22.3v20.9H38.1v71.2v7.6H14.5v-7.6v-71.2H0v-20.9h14.5V104c0,-13.4 3.9,-23.8 11.2,-31.3c7.3,-7.4 19,-10.8 35.1,-10V86.3M157.1,161.2c0,-15.5 11.2,-27.3 25.7,-27.3c14.5,0 25.5,11.8 25.5,27.3c0,15.6 -11,27.3 -25.5,27.3C168.3,188.5 157.1,176.9 157.1,161.2M182.8,110.1c-27.9,0 -49.7,22.2 -49.7,51.1c0,29.1 21.8,51.3 49.7,51.3c27.9,0 49.4,-22.2 49.4,-51.3C232.1,132.3 210.7,110.1 182.8,110.1M541.4,161.5c14.1,0 25.6,-11.5 25.6,-25.6c0,-14.1 -11.5,-25.6 -25.6,-25.6c-14.1,0 -25.6,11.5 -25.6,25.6C515.8,150 527.2,161.5 541.4,161.5M129.6,110.5c-2.2,-0.2 -4.3,-0.4 -6.6,-0.4c-4.4,0 -9.1,0.6 -13.9,1.9c-4.8,1.3 -8.5,3.4 -10.7,6.5v-7H74.7V211h23.8v-58.2c0,-5.4 1.4,-9.5 4.1,-12.3c4.4,-4.5 10.6,-6.5 16.7,-6.8c1.7,-0.1 3.5,0 5.3,0.1c1.6,0.2 3.2,0.5 4.7,0.7c0,-1 0,-2 0,-3c0,-2 0,-3.9 0,-5.9c0,-2.2 0,-4.5 0,-6.7c0,-1.9 0.2,-3.8 0.2,-5.7c0,-0.9 0.2,-1.7 0.2,-2.6C129.6,110.6 129.6,110.5 129.6,110.5zM475.8,160.6l29.7,-49h-28.7l-16.3,31.7l-16.5,-31.7H415l30.1,49l-30.7,50.6h28.7l17.3,-33.1l17.7,33.1h28.7L475.8,160.6zM356.9,254.8c14.9,0.5 26.7,-3.1 34.7,-10.9c7.6,-7.4 11.6,-18.1 12,-33l0,0v-2.4v-97.1h-24.5c0,0 0,14.3 0,28.8v14.9v0.1v14.5c0,5.3 -1.2,9.7 -5.8,13.8c-4.3,3.9 -10.9,5.1 -15.4,5.1c-2.7,0 -10.7,-0.9 -15.4,-7.5c-2.4,-3.4 -3.9,-9.1 -4.2,-14c-0.3,-5 -0.3,-7.4 -0.6,-14.7c-1.1,-30.2 -26,-47.6 -55,-40.3c-5,1.3 -9,3.4 -11.2,6.5v-7.2h-23.8V211h23.8v-52v-6c0,-5.4 1.5,-9.5 4.3,-12.3c2.8,-2.7 5.8,-4.2 9.5,-5.1c3.7,-0.9 6.6,-1 8.5,-1c2.9,0 6.5,0.8 8,1.2c2.8,0.8 6.3,3.3 8.3,6.2c3.7,5.3 3.3,12.8 3.5,18.9c0.2,7.6 0.4,15.3 2.7,22.7c3.2,10.3 9.7,20 19.7,24.6c9,4.1 20,5.2 29.7,3.4c3,-0.6 6.3,-1.5 9.1,-3c1.6,-0.9 3.2,-1.9 4.5,-3.2c0.4,10.7 0.6,17.6 -6,22.5c-1.8,1.4 -4.3,2.5 -6.5,2.9c-3.1,0.6 -6.5,1 -9.7,0.9L356.9,254.8z"
android:fillColor="#000044" />
<path
android:pathData="M97.6,89.8V39.2c0,-1.6 -0.1,-3.2 -0.2,-4.8c-0.1,-1.7 -0.3,-3.3 -0.5,-4.9h6.6l0.9,10h-1c0.9,-3.3 2.7,-5.9 5.5,-7.9c2.7,-1.9 6,-2.9 9.8,-2.9c3.8,0 7.1,0.9 9.9,2.6c2.8,1.7 4.9,4.2 6.5,7.5c1.6,3.2 2.4,7.2 2.4,11.8c0,4.5 -0.8,8.4 -2.3,11.7c-1.5,3.2 -3.7,5.8 -6.5,7.5c-2.8,1.8 -6.1,2.6 -9.9,2.6c-3.8,0 -7,-1 -9.7,-2.9c-2.7,-1.9 -4.6,-4.5 -5.5,-7.8h0.9v28.1H97.6zM117.3,66.8c4,0 7.2,-1.4 9.6,-4.2c2.4,-2.8 3.6,-6.8 3.6,-12.1c0,-5.4 -1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.6,-4.2c-4,0 -7.2,1.4 -9.5,4.2c-2.4,2.8 -3.6,6.8 -3.6,12.2c0,5.3 1.2,9.4 3.6,12.1C110.2,65.4 113.4,66.8 117.3,66.8z"
android:fillColor="#000044" />
<path
android:pathData="M165.4,72.4c-4.1,0 -7.6,-0.9 -10.6,-2.6c-3,-1.8 -5.3,-4.3 -6.9,-7.6c-1.6,-3.3 -2.4,-7.2 -2.4,-11.6c0,-4.5 0.8,-8.4 2.4,-11.7c1.6,-3.2 3.9,-5.8 6.9,-7.5c3,-1.8 6.5,-2.6 10.5,-2.6c4.1,0 7.6,0.9 10.6,2.6c3,1.8 5.3,4.3 7,7.5c1.7,3.2 2.5,7.1 2.5,11.7c0,4.5 -0.8,8.4 -2.5,11.6c-1.7,3.3 -4,5.8 -7,7.6C172.9,71.5 169.4,72.4 165.4,72.4zM165.4,66.8c4,0 7.1,-1.4 9.5,-4.2c2.4,-2.8 3.5,-6.8 3.5,-12.1c0,-5.4 -1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.5,-4.2c-4,0 -7.1,1.4 -9.5,4.2c-2.4,2.8 -3.5,6.8 -3.5,12.2c0,5.3 1.2,9.4 3.5,12.1C158.2,65.4 161.4,66.8 165.4,66.8z"
android:fillColor="#000044" />
<path
android:pathData="M207.2,71.6l-15.6,-42.2h7.2l13,37.1h-2.2l13.4,-37.1h5.9l13.2,37.1h-2.1l13.1,-37.1h6.9l-15.7,42.2h-6.6l-13.6,-37.7h3.4l-13.7,37.7H207.2z"
android:fillColor="#000044" />
<path
android:pathData="M287.7,72.4c-6.6,0 -11.8,-1.9 -15.6,-5.8c-3.8,-3.8 -5.7,-9.2 -5.7,-16c0,-4.4 0.8,-8.3 2.5,-11.5c1.7,-3.3 4,-5.8 7.1,-7.6c3,-1.8 6.5,-2.7 10.4,-2.7c3.9,0 7.1,0.8 9.7,2.4c2.6,1.6 4.6,3.9 6,6.9c1.4,3 2.1,6.5 2.1,10.6v2.5h-32.7v-4.3h28.2l-1.4,1.1c0,-4.5 -1,-8 -3,-10.5c-2,-2.5 -5,-3.8 -9,-3.8c-4.2,0 -7.5,1.5 -9.8,4.4C274.2,41.1 273,45 273,50v0.8c0,5.3 1.3,9.3 3.9,12c2.6,2.7 6.3,4.1 11,4.1c2.5,0 4.9,-0.4 7.1,-1.1c2.2,-0.8 4.3,-2 6.3,-3.7l2.4,4.8c-1.8,1.8 -4.2,3.2 -7,4.2C293.8,71.9 290.8,72.4 287.7,72.4z"
android:fillColor="#000044" />
<path
android:pathData="M314.5,71.6v-32c0,-1.7 0,-3.4 -0.1,-5.1c-0.1,-1.7 -0.2,-3.4 -0.4,-5h6.6l0.8,10.2l-1.2,0.1c0.6,-2.5 1.5,-4.6 2.9,-6.2c1.4,-1.6 3.1,-2.8 5,-3.7c1.9,-0.8 3.9,-1.2 6,-1.2c0.8,0 1.6,0 2.2,0.1c0.6,0.1 1.2,0.2 1.8,0.4l-0.1,6c-0.8,-0.3 -1.6,-0.5 -2.3,-0.5s-1.5,-0.1 -2.4,-0.1c-2.5,0 -4.6,0.6 -6.4,1.8c-1.8,1.2 -3.2,2.7 -4.1,4.5s-1.4,3.8 -1.4,5.9v24.9H314.5z"
android:fillColor="#000044" />
<path
android:pathData="M363.9,72.4c-6.6,0 -11.8,-1.9 -15.6,-5.8c-3.8,-3.8 -5.7,-9.2 -5.7,-16c0,-4.4 0.8,-8.3 2.5,-11.5c1.7,-3.3 4,-5.8 7.1,-7.6c3,-1.8 6.5,-2.7 10.4,-2.7c3.9,0 7.1,0.8 9.7,2.4c2.6,1.6 4.6,3.9 6,6.9c1.4,3 2.1,6.5 2.1,10.6v2.5h-32.7v-4.3H376l-1.4,1.1c0,-4.5 -1,-8 -3,-10.5c-2,-2.5 -5,-3.8 -9,-3.8c-4.2,0 -7.5,1.5 -9.8,4.4c-2.4,2.9 -3.5,6.9 -3.5,11.9v0.8c0,5.3 1.3,9.3 3.9,12c2.6,2.7 6.3,4.1 11,4.1c2.5,0 4.9,-0.4 7.1,-1.1c2.2,-0.8 4.3,-2 6.3,-3.7l2.4,4.8c-1.8,1.8 -4.2,3.2 -7,4.2C370,71.9 367,72.4 363.9,72.4z"
android:fillColor="#000044" />
<path
android:pathData="M406.8,72.4c-3.7,0 -6.9,-0.9 -9.7,-2.6c-2.8,-1.8 -5,-4.3 -6.5,-7.5c-1.5,-3.2 -2.3,-7.1 -2.3,-11.7c0,-4.6 0.8,-8.5 2.3,-11.8c1.5,-3.2 3.7,-5.7 6.5,-7.5c2.8,-1.7 6,-2.6 9.7,-2.6c3.8,0 7.1,1 9.9,2.9c2.8,1.9 4.6,4.5 5.6,7.7h-1V9.8h6.8v61.8h-6.7V61.5h0.9c-0.9,3.4 -2.7,6 -5.5,7.9C413.9,71.4 410.6,72.4 406.8,72.4zM408.2,66.8c4,0 7.2,-1.4 9.6,-4.2c2.4,-2.8 3.6,-6.8 3.6,-12.1c0,-5.4 -1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.6,-4.2c-4,0 -7.2,1.4 -9.5,4.2c-2.4,2.8 -3.6,6.8 -3.6,12.2c0,5.3 1.2,9.4 3.6,12.1C401.1,65.4 404.3,66.8 408.2,66.8z"
android:fillColor="#000044" />
<path
android:pathData="M484.3,72.4c-3.8,0 -7.1,-1 -9.8,-2.9c-2.7,-1.9 -4.6,-4.6 -5.5,-7.9h0.9v10.1h-6.7V9.8h6.8v29.5h-1c1,-3.2 2.8,-5.8 5.5,-7.7c2.7,-1.9 6,-2.9 9.8,-2.9c3.8,0 7.1,0.9 9.9,2.6c2.8,1.8 4.9,4.3 6.5,7.5c1.5,3.2 2.3,7.1 2.3,11.7s-0.8,8.4 -2.4,11.7c-1.6,3.2 -3.7,5.8 -6.5,7.5C491.4,71.5 488.1,72.4 484.3,72.4zM482.9,66.8c4,0 7.2,-1.4 9.6,-4.1c2.4,-2.7 3.6,-6.8 3.6,-12.2s-1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.6,-4.2c-4,0 -7.2,1.4 -9.5,4.2c-2.4,2.8 -3.6,6.8 -3.6,12.2c0,5.3 1.2,9.4 3.6,12.1C475.8,65.4 478.9,66.8 482.9,66.8z"
android:fillColor="#000044" />
<path
android:pathData="M511.9,90.7l-1.6,-5.6c2.6,-0.6 4.8,-1.3 6.6,-2.1c1.8,-0.8 3.2,-1.9 4.4,-3.2c1.2,-1.3 2.2,-3 3,-5l2.2,-5l-0.2,2.9l-18.4,-43.1h7.4l15.2,37h-2.2l15,-37h7.1L531,75.1c-1.1,2.7 -2.4,4.9 -3.7,6.8c-1.3,1.8 -2.8,3.3 -4.3,4.5c-1.5,1.1 -3.2,2.1 -5.1,2.7S514.1,90.3 511.9,90.7z"
android:fillColor="#000044" />
</vector>

View File

@@ -5,10 +5,6 @@
<data>
<import type="java.util.Map" />
<import type="java.time.ZonedDateTime" />
<import type="net.vonforst.evmap.model.ChargeLocation" />
<import type="net.vonforst.evmap.model.Chargepoint" />
@@ -43,18 +39,6 @@
name="availability"
type="Resource&lt;ChargeLocationStatus&gt;" />
<variable
name="predictionGraph"
type="Map&lt;ZonedDateTime, Integer&gt;" />
<variable
name="predictionMaxValue"
type="Integer" />
<variable
name="predictionDescription"
type="String" />
<variable
name="filteredAvailability"
type="Resource&lt;ChargeLocationStatus&gt;" />
@@ -77,8 +61,7 @@
</data>
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="@dimen/detail_corner_radius"
@@ -153,8 +136,8 @@
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:invisibleUnlessAnimated="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/txtName"
tools:backgroundTint="@color/available"
@@ -192,11 +175,10 @@
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_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
@@ -219,10 +201,10 @@
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"
android:autoLink="web"
android:linksClickable="true"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
@@ -249,10 +231,10 @@
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"
android:autoLink="web"
android:linksClickable="true"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
@@ -278,12 +260,13 @@
android:id="@+id/details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:nestedScrollingEnabled="false"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider3"
app:layout_constraintTop_toBottomOf="@+id/btnChargeprice"
tools:itemCount="3"
tools:listitem="@layout/item_detail" />
@@ -304,17 +287,17 @@
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" />
app:layout_constraintTop_toBottomOf="@+id/textView4" />
<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 : @string/realtime_data_unavailable}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/connectors"
tools:text="Echtzeitdaten nicht verfügbar" />
@@ -323,8 +306,8 @@
android:id="@+id/topPart"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="-10dp"
android:text="TextView"
android:layout_marginBottom="-10dp"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
@@ -338,107 +321,12 @@
android:layout_marginTop="8dp"
android:text="@string/go_to_chargeprice"
android:transitionName="@string/shared_element_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargepriceData != null &amp;&amp; charger.data.chargepriceData.country != null &amp;&amp; ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
app:icon="@drawable/ic_chargeprice"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider1" />
<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/btnChargeprice" />
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/utilization_prediction"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{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:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:text="@{predictionDescription}"
app:goneUnless="@{predictionGraph != null}"
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="@{predictionGraph != null}"
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="@{predictionGraph}"
app:goneUnless="@{predictionGraph != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView8"
app:maxValue="@{predictionMaxValue}"
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:scaleType="fitCenter"
android:background="?selectableItemBackgroundBorderless"
app:tint="@color/logo_tint_night"
app:goneUnless="@{predictionGraph != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/prediction"
app:srcCompat="@drawable/ic_powered_by_fronyx" />
<View
android:id="@+id/divider1"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
app:goneUnless="@{predictionGraph != null}"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@+id/imgPredictionSource" />
<ImageView
android:id="@+id/imgVerified"
android:layout_width="18dp"
@@ -447,13 +335,13 @@
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:contentDescription="@string/verified"
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
app:goneUnless="@{ charger.data.verified }"
app:layout_constraintBottom_toBottomOf="@+id/txtName"
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
@@ -464,12 +352,12 @@
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:contentDescription="@string/fault_report"
app:tooltipTextCompat="@{@string/fault_report}"
app:goneUnless="@{ charger.data.faultReport != null }"
app:layout_constraintBottom_toBottomOf="@+id/txtName"
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
@@ -477,31 +365,31 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:breakStrategy="balanced"
android:text="@{charger.data.license}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
android:text="@{charger.data.license}"
android:breakStrategy="balanced"
app:goneUnless="@{charger.data.license != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/sourceButton"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
<Button
android:id="@+id/btnRefreshLiveData"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.App.Button.OutlinedButton.IconOnly.Small"
android:contentDescription="@string/refresh_live_data"
android:enabled="@{availability.status != Status.LOADING}"
app:icon="@drawable/ic_refresh"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView7" />
app:layout_constraintBottom_toBottomOf="@+id/textView13"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/connectors" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.cardview.widget.CardView>
</layout>

View File

@@ -14,7 +14,7 @@
type="ChargepriceViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:id="@+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -54,87 +54,208 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/charge_prices_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
tools:itemCount="1"
tools:listitem="@layout/fragment_chargeprice_preview" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<ProgressBar
android:id="@+id/progressBar5"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="280dp"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView8"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="280dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_tariffs_found"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/chargeprice_select_connector"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vehicle_selection" />
<TextView
android:id="@+id/textView9"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="280dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_compatible_connectors"
app:goneUnless="@{vm.noCompatibleConnectors}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/connectors_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:data="@{vm.charger.chargepointsMerged}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
tools:itemCount="3"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_connector_button"
tools:orientation="horizontal" />
<TextView
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="280dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_select_car_first"
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS &amp;&amp; vm.vehicles.data.size() == 0}"
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
tools:text="Charge from 20% to 80%" />
<Button
android:id="@+id/btnSettings"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings"
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS &amp;&amp; vm.vehicles.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging &amp;&amp; vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
<TextView
android:id="@+id/tvVehicleHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/chargeprice_vehicle"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{vm.vehicles.data != null &amp;&amp; vm.vehicles.data.size() > 1}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/vehicle_selection"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvVehicleHeader"
app:data="@{vm.vehicles.data}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:goneUnless="@{vm.vehicles.data != null &amp;&amp; vm.vehicles.data.size() > 1}"
android:orientation="horizontal"
tools:listitem="@layout/item_chargeprice_vehicle_chip" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/battery_range"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:valueFrom="0.0"
android:valueTo="100.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView4"
app:values="@={vm.batteryRange}" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/charge_prices_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:nestedScrollingEnabled="false"
app:data="@{vm.chargePricesForChargepoint.data}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/battery_range"
tools:itemCount="3"
tools:listitem="@layout/item_chargeprice"
tools:visibility="invisible" />
<TextView
android:id="@+id/textView8"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_tariffs_found"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toTopOf="@+id/btnFeedbackMissingPrice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/textView9"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_compatible_connectors"
app:goneUnless="@{vm.noCompatibleConnectors}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_select_car_first"
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS &amp;&amp; vm.vehicles.data.size() == 0}"
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
app:layout_constraintVertical_chainStyle="packed" />
<ProgressBar
android:id="@+id/progressBar5"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<Button
android:id="@+id/btnSettings"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings"
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS &amp;&amp; vm.vehicles.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
<Button
android:id="@+id/btnFeedbackMissingPrice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/chargeprice_feedback_missing_price"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.viewmodel.ChargepriceFeedbackViewModel" />
<import type="net.vonforst.evmap.viewmodel.ChargepriceFeedbackType" />
<variable
name="vm"
type="ChargepriceFeedbackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:goneUnless="@{vm.loading}" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
app:goneUnless="@{!vm.loading}">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/txtCPO"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@{@string/chargeprice_feedback_cpo(vm.charger.network)}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:goneUnless="@{vm.charger.network != null &amp;&amp; vm.feedbackType == ChargepriceFeedbackType.MISSING_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/chargeprice_feedback_cpo" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_tariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:goneUnless="@{vm.feedbackType == ChargepriceFeedbackType.MISSING_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/txtCPO">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_tariff"
android:maxLength="100"
android:text="@={vm.tariff}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_tariff_spinner"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{vm.feedbackType == ChargepriceFeedbackType.WRONG_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_tariff">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/tariff_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_tariff"
android:inputType="none"
app:data="@{vm.chargePricesStrings}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_price"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_tariff_spinner">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_price"
android:maxLength="100"
android:text="@={vm.price}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="1000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_price">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="160dp"
android:gravity="top"
android:hint="@string/chargeprice_feedback_comment"
android:inputType="textMultiLine"
android:maxLength="1000"
android:text="@={vm.notes}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_comment">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_email"
android:inputType="textEmailAddress"
android:maxLength="100"
android:text="@={vm.email}" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:onClick="@{(view) -> vm.sendFeedback()}"
android:enabled="@{vm.formValid}"
android:text="@string/chargeprice_feedback_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_email" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

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

View File

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

View File

@@ -19,8 +19,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/map"
@@ -143,7 +142,7 @@
<FrameLayout
android:id="@+id/gallery_container"
android:layout_width="@dimen/map_toolbar_width"
android:layout_width="match_parent"
android:layout_height="@dimen/gallery_height_with_margin"
android:background="?android:colorBackground"
app:layout_behavior="@string/BackDropBottomSheetBehavior">
@@ -175,11 +174,11 @@
app:backgroundTint="?android:colorBackground"
app:borderWidth="0dp"
app:isFabActive="@{ vm.myLocationEnabled }"
app:layout_behavior="@string/hide_on_scroll_fab_behavior" />
app:layout_behavior="net.vonforst.evmap.ui.HideOnScrollFabBehavior" />
<androidx.core.widget.NestedScrollView
android:id="@+id/bottom_sheet"
android:layout_width="@dimen/map_toolbar_width"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical"
@@ -188,7 +187,6 @@
app:behavior_peekHeight="@dimen/peek_height"
app:bottomsheetbehavior_defaultState="stateHidden"
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
android:clipToPadding="false"
tools:bottomsheetbehavior_defaultState="stateCollapsed">
<include
@@ -197,9 +195,6 @@
app:charger="@{vm.charger}"
app:availability="@{vm.availability}"
app:filteredAvailability="@{vm.filteredAvailability}"
app:predictionGraph="@{vm.predictionGraph}"
app:predictionMaxValue="@{vm.predictionMaxValue}"
app:predictionDescription="@{vm.predictionDescription}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}"
@@ -216,7 +211,6 @@
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_directions"
android:translationX="@dimen/directions_fab_translationx"
app:layout_anchor="@id/bottom_sheet"
app:layout_anchorGravity="top|right|end"
app:layout_behavior="@string/ScrollAwareFABBehavior"
@@ -224,7 +218,7 @@
<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
android:id="@+id/detail_app_bar"
android:layout_width="@dimen/map_toolbar_width"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" />
@@ -243,7 +237,7 @@
app:borderWidth="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_layers"
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
app:layout_behavior="net.vonforst.evmap.ui.HideOnExpandFabBehavior"
android:theme="@style/NoElevationOverlay" />
<androidx.cardview.widget.CardView

View File

@@ -162,7 +162,7 @@
android:layout_height="40dp"
android:layout_margin="8dp"
android:scaleType="fitCenter"
app:invisibleUnless="@{item.branding.logoUrl != null}"
app:goneUnless="@{item.branding.logoUrl != null}"
app:imageUrl="@{item.branding.logoUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="13dp"
android:paddingTop="13dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="3"
android:textAppearance="?attr/textAppearanceBodyLarge"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam" />
</FrameLayout>

View File

@@ -6,6 +6,16 @@
android:id="@+id/menu_help"
android:icon="@drawable/ic_help"
android:title="@string/help"
app:showAsAction="always" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_feedback_missing_price"
android:title="@string/chargeprice_feedback_missing_price"
app:showAsAction="never" />
<item
android:id="@+id/menu_feedback_wrong_price"
android:title="@string/chargeprice_feedback_wrong_price"
app:showAsAction="never" />
</menu>

View File

@@ -124,6 +124,13 @@
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<action
android:id="@+id/action_chargeprice_to_chargepriceFeedbackFragment"
app:destination="@id/chargeprice_feedback"
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<action
android:id="@+id/action_chargeprice_to_donateFragment"
app:destination="@id/donate" />
@@ -131,6 +138,31 @@
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation" />
</fragment>
<fragment
android:id="@+id/chargeprice_feedback"
android:name="net.vonforst.evmap.fragment.ChargepriceFeedbackFragment"
android:label="@string/chargeprice_feedback"
tools:layout="@layout/fragment_chargeprice_feedback">
<argument
android:name="feedbackType"
app:argType="net.vonforst.evmap.viewmodel.ChargepriceFeedbackType" />
<argument
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation"
app:nullable="true" />
<argument
android:name="vehicle"
app:argType="net.vonforst.evmap.api.chargeprice.ChargepriceCar"
app:nullable="true" />
<argument
android:name="chargePrices"
app:argType="net.vonforst.evmap.api.chargeprice.ChargePrice[]"
app:nullable="true" />
<argument
android:name="batteryRange"
app:argType="float[]"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/donate"
android:name="net.vonforst.evmap.fragment.DonateFragment"

View File

@@ -101,7 +101,6 @@
<string name="pref_language">App-Sprache</string>
<string name="pref_darkmode">Dunkles Design</string>
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
<string name="location_error">Standort nicht erkannt. Bitte Systemeinstellungen prüfen</string>
<string name="retry">Wiederholen</string>
<string name="filter_open_247">24 Stunden geöffnet</string>
<string name="filter_barrierfree">Ohne Vertrag / Registrierung nutzbar</string>
@@ -245,7 +244,7 @@
<string name="help">Hilfe</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Schieflast erlauben</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Einphasiges Laden mit mehr als 4.5 kW erlauben</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Einphasiges laden mit mehr als 4.5 kW erlauben</string>
<string name="pref_map_rotate_gestures_enabled">Kartenrotation</string>
<string name="pref_map_rotate_gestures_on">Karte mit zwei Fingern rotieren</string>
<string name="pref_map_rotate_gestures_off">Karte immer nach Norden ausrichten</string>
@@ -271,15 +270,13 @@
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</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="utilization_prediction">Auslastungsprognose</string>
<string name="prediction_help">Die Prognose basiert auf Faktoren wie Wochentag, Uhrzeit und Nutzung in der Vergangenheit. So kannst du stark ausgelastete Ladesäulen vermeiden. Keine Garantie.</string>
<string name="prediction_time_colon">%s Uhr:</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d verfügbar</item>
<item quantity="other">%1$d/%2$d verfügbar</item>
</plurals>
<string name="pref_prediction_enabled">Auslastungsprognosen anzeigen</string>
<string name="pref_prediction_enabled_summary">für unterstützte Ladestationen\n(momentan nur Schnellader in Deutschland)</string>
<string name="prediction_only">(nur %s)</string>
<string name="prediction_dc_plugs_only">DC-Anschlüsse</string>
<string name="chargeprice_feedback">Feedback</string>
<string name="chargeprice_feedback_missing_price">Fehlenden Preis melden</string>
<string name="chargeprice_feedback_wrong_price">Falschen Preis melden</string>
<string name="chargeprice_feedback_email">E-Mail-Adresse für Rückfragen</string>
<string name="chargeprice_feedback_comment">Weitere Infos</string>
<string name="chargeprice_feedback_price">Preis (pro kWh, Minute, etc.)</string>
<string name="chargeprice_feedback_cpo">Betreiber der Station (CPO): %s</string>
<string name="chargeprice_feedback_tariff">Anbieter bzw. Tarif</string>
<string name="chargeprice_feedback_send">Absenden</string>
</resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- tools:ignore="MissingQuantity" is temporary until Weblate 4.14 is released --><resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingQuantity">
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connecteurs</string>
<string name="no_maps_app_found">Installez d\'abord une application de navigation</string>
<string name="no_browser_app_found">Installez d\'abord un navigateur web</string>
<string name="no_maps_app_found">Aucune application de navigation trouvée</string>
<string name="no_browser_app_found">Aucun navigateur web trouvé</string>
<string name="address">Adresse</string>
<string name="operator">Opérateur</string>
<string name="network">Réseau</string>
@@ -22,19 +22,19 @@
<string name="menu_favs">Favoris</string>
<string name="menu_filter">Filtre</string>
<string name="not_implemented">pas encore mis en œuvre</string>
<string name="about">À propos</string>
<string name="about">À propos d\'EVMap</string>
<string name="github_link_title">Code source</string>
<string name="settings_ui">Interface</string>
<string name="privacy">Confidentialité</string>
<string name="fav_add">Enregistrer comme favori</string>
<string name="pref_navigate_use_maps">Naviguer maintenant</string>
<string name="settings_ui">Interface utilisateur</string>
<string name="privacy">Politique de confidentialité</string>
<string name="fav_add">Ajouter aux favoris</string>
<string name="pref_navigate_use_maps">Démarrer la navigation immédiatement</string>
<string name="coordinates">Coordonnées</string>
<string name="pref_navigate_use_maps_on">Le bouton de navigation démarre le guidage d\'itinéraire avec Google Maps</string>
<string name="pref_navigate_use_maps_on">Le bouton de navigation lance immédiatement la navigation Google Maps</string>
<string name="share">Partager</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_supercharger">Superchargeur Tesla</string>
<string name="show_less">moins…</string>
<string name="favorites_empty_state">Les chargeurs sauvegardés apparaissent ici</string>
<string name="favorites_empty_state">Si vous ajoutez des chargeurs à vos favoris, ils apparaîtront ici.</string>
<string name="donate">Faire un don</string>
<string name="map_type_satellite">Satellite</string>
<string name="map_type_terrain">Terrain</string>
@@ -70,10 +70,10 @@
<string name="category_zoo">Zoo</string>
<string name="menu_apply">Appliquer les filtres</string>
<string name="save_as_profile">Enregistrer en tant que profil</string>
<string name="welcome_1">Trouvez des chargeurs de véhicules électriques autour de vous</string>
<string name="welcome_2">La couleur d\'un chargeur sur la carte vous indique sa puissance de charge maximale</string>
<string name="welcome_2_detail">Cela peut également être vu dans \"À propos\" → \"Foire aux questions\"</string>
<string name="donation_dialog_title">Merci d\'utiliser EVMap</string>
<string name="welcome_1">Trouvez des chargeurs de véhicules électriques autour de vous.</string>
<string name="welcome_2">La couleur d\'un chargeur sur la carte vous indique sa puissance de charge maximale.</string>
<string name="welcome_2_detail">(Vous pouvez vérifier à nouveau les couleurs sous \"À propos d\'EVMap → FAQ\" dans le menu)</string>
<string name="donation_dialog_title">Merci d\'utiliser EVMap !</string>
<string name="chargeprice_donation_dialog_title">Vous êtes un vrai chasseur de bonnes affaires !</string>
<string name="deleted_filterprofile">\"%s\" supprimé</string>
<string name="undo">Annuler</string>
@@ -81,22 +81,21 @@
<string name="verified">vérifié</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d mode de paiement compatible</item>
<item quantity="many">%d modes de paiement compatibles</item>
<item quantity="other">%d modes de paiement compatibles</item>
</plurals>
<string name="verified_desc">Le fonctionnement du chargeur a été confirmé au moins une fois par un membre de la communauté %s</string>
<string name="verified_desc">Chargeur vérifié par un membre de la communauté %s - ne fonctionne pas forcément en ce moment.</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">frais de session</string>
<string name="chargeprice_per_kwh">par kWh</string>
<string name="chargeprice_per_minute">par min</string>
<string name="chargeprice_blocking_fee">Frais de blocage &gt;%s</string>
<string name="chargeprice_no_tariffs_found">Aucun tarif de recharge pour ce chargeur sur Chargeprice.app</string>
<string name="chargeprice_no_tariffs_found">Chargeprice.app n\'a trouvé aucun tarif de recharge compatible avec ce chargeur.</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Afficher les tarifs exclusifs aux clients</string>
<string name="chargeprice_battery_range">Charge de %1$.0f%% à %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Charge de</string>
<string name="chargeprice_stats">(%1$.0f kWh, approx. %2$s, ⌀ %3$.0f kW)</string>
<string name="chargeprice_vehicle">Véhicule</string>
<string name="close">Fermer</string>
<string name="close">fermer</string>
<string name="chargeprice_title">Prix</string>
<string name="pref_chargeprice_currency">Devise</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
@@ -104,7 +103,6 @@
<string name="pref_data_source">Source des données</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d tarif sélectionné</item>
<item quantity="many">%d tarifs sélectionnés</item>
<item quantity="other">%d tarifs sélectionnés</item>
</plurals>
<string name="data_source_openchargemap">Open Charge Map</string>
@@ -121,8 +119,8 @@
<string name="pref_search_delete_recent">Supprimer les résultats de recherche récents</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Permettre une charge déséquilibrée</string>
<string name="pref_map_rotate_gestures_enabled">Rotation de la carte</string>
<string name="pref_map_rotate_gestures_off">Rotation désactivée (nord toujours en haut)</string>
<string name="pref_map_rotate_gestures_enabled">Activer la rotation de la carte</string>
<string name="pref_map_rotate_gestures_off">La carte reste orientée vers le nord</string>
<string name="refresh_live_data">rafraîchir le statut en temps réel</string>
<string name="pref_language_device_default">Utiliser la langue de l\'appareil</string>
<string name="pref_darkmode_device_default">Utiliser le réglage de l\'appareil</string>
@@ -141,22 +139,22 @@
<string name="general_info">Informations générales</string>
<string name="realtime_data_loading">Vérification du statut en temps réel…</string>
<string name="plug_ccs">CCS</string>
<string name="donation_successful">Merci ❤️</string>
<string name="donation_failed">Quelque chose s\'est mal passé 😕</string>
<string name="donation_successful">Merci ! ❤️</string>
<string name="donation_failed">Quelque chose s\'est mal passé. 😕</string>
<string name="category_supermarket">Supermarché</string>
<string name="version">Version</string>
<string name="oss_licenses">Licences</string>
<string name="oss_licenses">Licences Open Source</string>
<string name="realtime_data_source">Source du statut en temps réel (bêta) : %s</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_type_3">Type 3A</string>
<string name="plug_type_3">Type 3a</string>
<string name="plug_cee_rot">CEE Rouge</string>
<string name="all">tous</string>
<string name="fault_report_date">Rapport d\'anomalie (dernière mise à jour : %s)</string>
<string name="menu_report_new_charger">Nouveau chargeur</string>
<string name="menu_report_new_charger">Signaler un nouveau chargeur</string>
<string name="filter_connectors">Connecteurs</string>
<string name="copyright_summary">©2020-2022 Johan von Forstner</string>
<string name="other">Autre</string>
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance lapplication de cartes à lemplacement du chargeur</string>
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance lapplication de cartes avec lemplacement du chargeur</string>
<string name="settings_map">Carte</string>
<string name="fault_report">Rapport d\'anomalie</string>
<string name="filter_free">Uniquement des chargeurs gratuits</string>
@@ -179,13 +177,13 @@
<string name="number_selected">%d sélectionné</string>
<string name="cancel">Annuler</string>
<string name="filter_operators">Opérateurs</string>
<string name="chargeprice_donation_dialog_detail">Vous faites bon usage de la fonction de comparaison des prix. Aidez-nous à couvrir les coûts de ces données en soutenant EVMap par un don.</string>
<string name="chargeprice_donation_dialog_detail">Il semble que vous appréciez beaucoup la fonction de comparaison des prix. Pour accéder aux données de tarification, le développeur d\'EVMap doit payer une redevance mensuelle au fournisseur de données Chargeprice.app. Par conséquent, veuillez envisager de soutenir EVMap par un don.</string>
<string name="and_n_others">et %d autres</string>
<string name="contact">Contact</string>
<string name="pref_map_provider">Fournisseur de cartes</string>
<string name="twitter">Twitter</string>
<string name="category_petrol_station">Station-service</string>
<string name="edit_on_goingelectric_info">Veuillez vous connecter à GoingElectric.de si cette page est vide</string>
<string name="edit_on_goingelectric_info">Si seule une page vide s\'affiche ici, veuillez d\'abord vous connecter à GoingElectric.de.</string>
<string name="settings_chargeprice">Comparaison des prix</string>
<string name="category_service_on_motorway">Aire de service (sur autoroute)</string>
<string name="category_railway_station">Gare ferroviaire</string>
@@ -198,7 +196,7 @@
<string name="reorder">réorganiser</string>
<string name="delete">Supprimer</string>
<string name="save_profile_enter_name">Saisissez le nom du profil de filtrage :</string>
<string name="donation_dialog_detail">EVMap est un logiciel libre et gratuit. Les contributions de codage sur GitHub sont très appréciées. Pour aider à couvrir les frais de fonctionnement de l\'accès aux sources de données, veuillez envisager de faire un don du montant de votre choix au développeur.</string>
<string name="donation_dialog_detail">EVMap est un logiciel libre et open source que je développe pendant mon temps libre. Les contributions de codage sur GitHub sont très appréciées. Cependant, en raison de la popularité croissante de l\'application, je dois également couvrir certains coûts de fonctionnement, par exemple pour l\'accès aux sources de données. Par conséquent, veuillez envisager de soutenir l\'application par un don ou via les sponsors GitHub.</string>
<string name="charging_barrierfree">Utilisable sans enregistrement</string>
<string name="chargeprice_battery_range_to">à</string>
<string name="category_service_off_motorway">Aire de service (hors autoroute)</string>
@@ -210,24 +208,23 @@
<string name="category_holiday_home">Maison de vacances</string>
<string name="category_caravan_site">Emplacement pour caravanes</string>
<string name="filter_custom">Filtre modifié</string>
<string name="filterprofiles_empty_state">Vous n\'avez aucun profil de filtrage enregistré</string>
<string name="filterprofiles_empty_state">Vous n\'avez pas encore enregistré de profils de filtrage.</string>
<string name="welcome_to_evmap">Bienvenue sur EVMap</string>
<string name="chargeprice_provider_customer_tariff">Uniquement pour les clients du fournisseur</string>
<string name="powered_by_chargeprice">alimenté par Chargeprice</string>
<string name="pref_my_vehicle">Mes véhicules</string>
<string name="pref_my_tariffs">Mes tarifs de recharge</string>
<string name="license">Licence</string>
<string name="autocomplete_connection_error">Impossible de charger les suggestions</string>
<string name="autocomplete_connection_error">Les suggestions n\'ont pas pu être chargées</string>
<string name="chargeprice_select_connector">Choisir le connecteur</string>
<string name="chargeprice_select_car_first">Veuillez d\'abord sélectionner le modèle de votre voiture dans les paramètres</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Certains fournisseurs d\'énergie offrent des tarifs moins chers exclusivement à leurs clients</string>
<string name="pref_chargeprice_no_base_fee">Exclure les tarifs avec frais mensuels</string>
<string name="chargeprice_no_compatible_connectors">Pas de connecteurs compatibles dans cette station de recharge</string>
<string name="chargeprice_select_car_first">Veuillez d\'abord sélectionner le modèle de votre voiture dans les paramètres.</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Certains fournisseurs offrent des tarifs moins chers exclusivement à leurs clients (par exemple, électricité domestique, gaz)</string>
<string name="pref_chargeprice_no_base_fee">Afficher uniquement les tarifs sans frais mensuels</string>
<string name="chargeprice_no_compatible_connectors">Aucun des connecteurs de cette station de charge n\'est compatible avec votre véhicule.</string>
<string name="chargeprice_connection_error">Impossible de charger les prix</string>
<string name="pref_search_provider">Fournisseur de recherche de lieux</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one" tools:ignore="ImpliedQuantity">(sera mis en évidence dans la comparaison des prix)</item>
<item quantity="many">(seront mis en évidence dans la comparaison des prix)</item>
<item quantity="other">(seront mis en évidence dans la comparaison des prix)</item>
</plurals>
<string name="deleted_recent_search_results">Les résultats de recherche récents ont été supprimés</string>
@@ -238,21 +235,21 @@
<string name="got_it">J\'ai compris</string>
<string name="powered_by_mapbox">propulsé par Mapbox</string>
<string name="lets_go">Allons-y</string>
<string name="crash_report_text">EVMap a planté. Veuillez envoyer un rapport de plantage au développeur.</string>
<string name="crash_report_text">Désolé, il semble que EVMap ait planté. Veuillez envoyer un rapport de plantage au développeur.</string>
<string name="unknown_operator">Opérateur inconnu</string>
<string name="data_source_goingelectric_desc">Idéal dans les pays germanophones. Descriptions en allemand. Maintenu par la communauté.</string>
<string name="data_source_goingelectric_desc">Très bonne couverture en Allemagne, en Autriche et en Suisse et dans de nombreux pays voisins. Descriptions en allemand. Maintenu par la communauté.</string>
<string name="data_source_openchargemap_desc">Couverture mondiale avec une qualité variable. Descriptions en anglais ou dans la langue locale. Données ouvertes maintenues par la communauté et provenant de sources gouvernementales dans certains pays (par exemple, Amérique du Nord, Royaume-Uni, France, Norvège).</string>
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
<string name="settings_data_sources">Sources de données</string>
<string name="data_sources_description">Veuillez choisir une source de données pour les stations de recharge. Vous pourrez la modifier ultérieurement dans les paramètres de l\'application.</string>
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses à récupérer. Veuillez envisager de faire un don via \"À propos\" -&gt; \"Faire un don\".</string>
<string name="data_sources_description">EVMap supporte plusieurs sources de données pour les stations de recharge. Veuillez sélectionner celle que vous souhaitez utiliser. Vous pourrez toujours la modifier ultérieurement dans les paramètres de l\'application.</string>
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses. Si vous utilisez souvent cette fonctionnalité, veuillez envisager de faire un don via \"À propos dEVMap -&gt; Faire un don\".</string>
<string name="pref_chargeprice_currency_hrk">Kuna croate (HRK)</string>
<string name="help">Aide</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge en courant alternatif monophasé de plus de 4,5 kW</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge avec &gt;4,5 kW aux stations AC pour les voitures avec chargeur monophasé</string>
<string name="pref_chargeprice_currency_huf">Forint hongrois (HUF)</string>
<string name="pref_chargeprice_currency_pln">Złoty polonais (PLN)</string>
<string name="pref_map_rotate_gestures_on">Utilisez deux doigts pour faire pivoter la carte</string>
<string name="pref_map_rotate_gestures_on">La carte peut être pivotée avec un geste à deux doigts</string>
<string name="pref_chargeprice_currency_chf">Franc suisse (CHF)</string>
<string name="pref_chargeprice_currency_usd">Dollar américain (USD)</string>
<string name="pref_chargeprice_currency_sek">Couronne suédoise (SEK)</string>

View File

@@ -143,12 +143,12 @@
<string name="category_parking_underground">Parkeringsgarasje under bakken</string>
<string name="reorder">endre rekkefølge</string>
<string name="save_profile_enter_name">Skriv inn navnet på filterprofilen:</string>
<string name="filterprofiles_empty_state">Du har ikke noen lagrede filterprofiler</string>
<string name="filterprofiles_empty_state">Du har ikke noen lagrede filterprofiler.</string>
<string name="chargeprice_donation_dialog_title">Du er en sann gjerrigknark.</string>
<string name="deleted_filterprofile">Slettet «%s»</string>
<string name="charging_barrierfree">Kan brukes uten registrering</string>
<string name="welcome_1">Finn kjøretøyladere der du er</string>
<string name="welcome_2">Hver laders farge samsvarer med dens høyeste ladeeffekt</string>
<string name="welcome_1">Finn kjøretøyladere der du er.</string>
<string name="welcome_2">Hver laders farge samsvarer med dens høyeste ladeeffekt.</string>
<string name="welcome_2_detail">Dette er også å finne i «Om» → «O-S-S» i menyen</string>
<string name="verified_desc">Lader bekreftet av et medlem av %s-gemenskapen. Dette betyr ikke at den virker nå.</string>
<string name="charge_price_format">%2$s%1$.2f</string>
@@ -271,17 +271,4 @@
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
<string name="about_contributors">Bidragsytere</string>
<string name="about_contributors_text">Takk til alle som har kodet og oversatt EVMap:</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d tilgjengelig</item>
<item quantity="other">%1$d/%2$d tilgjengelig</item>
</plurals>
<string name="prediction_help">Basert på hvilken dag det er, tid på dagen, og tidligere bruk, slik at du kan finne ledige ladere. Ingen garanti dog.</string>
<string name="prediction_time_colon">%s:</string>
<string name="prediction_dc_plugs_only">Likestrømmsstøpsel</string>
<string name="location_error">Klarte ikke å fastsette posisjon. Sjekk systeminnstillingene.</string>
<string name="utilization_prediction">Bruksprognose</string>
<string name="prediction_only">(kun %s)</string>
<string name="pref_prediction_enabled">Vis bruksprognoser</string>
<string name="pref_prediction_enabled_summary">for støttede ladere
\n(foreløpig kun for likestrøm i Tyskland)</string>
</resources>

View File

@@ -2,5 +2,4 @@
<resources>
<color name="background">#121212</color>
<color name="my_tariff_background">#1FFFFFFF</color>
<color name="logo_tint_night">#FFFFFF</color>
</resources>

View File

@@ -2,5 +2,4 @@
<resources>
<dimen name="map_toolbar_width">500dp</dimen>
<dimen name="layers_fab_top_padding">20dp</dimen>
<dimen name="directions_fab_translationx">-44dp</dimen>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="bottom_sheet_collapsible">false</bool>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hide_on_scroll_fab_behavior">@null</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="bottom_sheet_collapsible">true</bool>
</resources>

View File

@@ -28,5 +28,4 @@
<color name="my_tariff_background">#1F000000</color>
<color name="background">#FFFFFF</color>
<color name="pager_unselected">#1F000000</color>
<color name="logo_tint_night">@null</color>
</resources>

View File

@@ -11,5 +11,4 @@
<item name="match_parent" type="dimen">-1</item>
<dimen name="map_toolbar_width">@dimen/match_parent</dimen>
<dimen name="layers_fab_top_padding">100dp</dimen>
<dimen name="directions_fab_translationx">0dp</dimen>
</resources>

View File

@@ -8,11 +8,9 @@
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
<string name="github_sponsors_link">https://github.com/sponsors/johan12345/</string>
<string name="chargeprice_api_url">https://api.chargeprice.app/v1/</string>
<string name="fronyx_url">https://fronyx.io/</string>
<string name="pref_language_en">English</string>
<string name="pref_language_de">Deutsch</string>
<string name="pref_language_fr">Français</string>
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
<string name="about_contributors_list">Danilo Bargen\nAltonss\nAllan Nordhøy\nLicaon_Kter\npt2121\nnautilusx</string>
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
</resources>

View File

@@ -100,7 +100,6 @@
<string name="pref_language">App language</string>
<string name="pref_darkmode">Dark mode</string>
<string name="connection_error">Could not load charging stations</string>
<string name="location_error">Failed to detect location. Please check system settings</string>
<string name="retry">Retry</string>
<string name="filter_open_247">Available 24/7</string>
<string name="filter_barrierfree">Usable without registration</string>
@@ -270,15 +269,13 @@
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contributors</string>
<string name="about_contributors_text">Thanks to all contributors for their coding and translation contributions to EVMap:</string>
<string name="utilization_prediction">Utilization prediction</string>
<string name="prediction_help">The prediction is based on factors like day of the week, time of day and past usage, so that you can avoid overcrowded chargers. No guarantee.</string>
<string name="prediction_time_colon">%s:</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d available</item>
<item quantity="other">%1$d/%2$d available</item>
</plurals>
<string name="pref_prediction_enabled">Show utilization predictions</string>
<string name="pref_prediction_enabled_summary">for supported chargers\n(currently only DC in Germany)</string>
<string name="prediction_only">(%s only)</string>
<string name="prediction_dc_plugs_only">DC plugs</string>
<string name="chargeprice_feedback">Feedback</string>
<string name="chargeprice_feedback_missing_price">Report missing price</string>
<string name="chargeprice_feedback_wrong_price">Report incorrect price</string>
<string name="chargeprice_feedback_email">Email address for further questions</string>
<string name="chargeprice_feedback_comment">Other details</string>
<string name="chargeprice_feedback_price">Price (per kWh, minute, etc.)</string>
<string name="chargeprice_feedback_cpo">Station operator (CPO): %s</string>
<string name="chargeprice_feedback_tariff">Provider / plan</string>
<string name="chargeprice_feedback_send">Submit</string>
</resources>

View File

@@ -9,12 +9,6 @@
android:entryValues="@array/pref_data_source_values"
android:defaultValue="goingelectric"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="prediction_enabled"
android:title="@string/pref_prediction_enabled"
android:defaultValue="true"
android:summary="@string/pref_prediction_enabled_summary" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">

View File

@@ -1,60 +0,0 @@
package net.vonforst.evmap.api.fronyx
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.okResponse
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.HttpURLConnection
import java.time.ZoneOffset
import java.time.ZonedDateTime
class FronyxApiTest {
val webServer = MockWebServer()
val fronyx: FronyxApi
init {
webServer.start()
val apikey = ""
fronyx = FronyxApi.create(
apikey,
webServer.url("/").toString()
)
webServer.dispatcher = object : Dispatcher() {
val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
override fun dispatch(request: RecordedRequest): MockResponse {
val segments = request.requestUrl!!.pathSegments
val urlHead = segments.subList(0, 2).joinToString("/")
when (urlHead) {
"predictions/evse-id" -> {
val id = segments[2]
return okResponse("/fronyx/${id.replace("*", "_")}.json")
}
else -> return notFoundResponse
}
}
}
}
@Test
fun apiTest() {
val evseId = "DE*ION*E202102"
runBlocking {
val result = fronyx.getPredictionsForEvseId(evseId)
assertEquals(result.evseId, evseId)
assertEquals(25, result.predictions.size)
assertEquals(
ZonedDateTime.of(2022, 9, 18, 13, 45, 0, 0, ZoneOffset.UTC),
result.predictions[0].timestamp
)
assertEquals(FronyxStatus.AVAILABLE, result.predictions[0].status)
}
}
}

View File

@@ -1,105 +0,0 @@
{
"evseId": "DE*ION*E202102",
"predictions": [
{
"timestamp": "2022-09-18T13:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T14:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T14:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T14:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T14:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T17:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T17:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T17:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T17:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:00:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:00:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:45:00.000Z",
"status": "AVAILABLE"
}
]
}

View File

@@ -3,14 +3,14 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.5.2'
ext.nav_version = '2.5.1'
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"

View File

@@ -26,9 +26,6 @@ be put into the app in the form of a resource file called `apikeys.xml` under
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
<string name="fronyx_key" translatable="false">
insert your Fronyx key here
</string>
</resources>
```
@@ -172,18 +169,3 @@ In case you want to pay for access to the full Chargeprice API, check out their
[sales@chargeprice.net](mailto:sales@chargeprice.net).
</details>
Availability data providers
---------------------------
### fronyx
[fronyx](https://fronyx.io/) provides us predictions of charging station availability.
<details>
<summary>How to obtain an API key</summary>
The API is not publically available, contact [fronyx](https://fronyx.io/contact-us/) to get an API
key and documentation.
If you don't want to test this functionality, simply leave the API key blank.
</details>

View File

@@ -1,8 +0,0 @@
Verbesserungen:
- Einige Texte vereinfacht
- Unterstützung für Sprachauswahl pro App von Android 13
Fehler behoben:
- Filtermenü ließ sich nicht öffnen
- Abstürze / Inkonsistenzen nach Wechsel der Datenquelle
- Abstürze unter Android Auto

View File

@@ -1,8 +0,0 @@
Verbesserungen:
- Weitere unterstützte Länder für Preisvergleich mit Chargeprice.app
- Android Auto: Suchbutton nun auf dem Hauptbildschirm
- Android Auto: Emoji durch Icons ersetzt
Fehler behoben:
- Abstürze / Inkonsistenzen nach Wechsel der Datenquelle
- Probleme beim Laden der Echtzeitdaten

View File

@@ -1,2 +0,0 @@
Fehler behoben:
- verschiedene filterabhängige Anzeigen waren seit 1.3.11 nicht mehr korrekt

View File

@@ -1 +0,0 @@
Änderungen in Android Auto aus 1.3.12 rückgängig gemacht, da diese für Abstürze sorgten.

View File

@@ -1,11 +0,0 @@
Neue Features:
- Auslastungsprognose (nur für DC-Ladestationen in Deutschland, powered by fronyx)
Verbesserungen:
- Layout für die Karten- und Detailansicht für große Tablets angepasst
- Android Auto: Mehr Details im Preisvergleich, Suchbutton auf dem Hauptbildschirm, Android Auto: Emoji durch Icons ersetzt
Fehler behoben:
- Inkonsistente Anzeige beim Laden der Details
- Preisvergleich: Langsames Wechseln der Anschlüsse
- Android Auto: Korrektur Auswahl des Ladepunkts für Preisvergleich

View File

@@ -1,8 +0,0 @@
Improvements:
- Simplified some texts
- Support for Android 13's per-app language selector
Bugfixes:
- Filter menu could not be opened
- Crashes / inconsistencies after switching data source
- Crashes on Android Auto

View File

@@ -1,8 +0,0 @@
Improvements:
- More European countries supported for price comparison with Chargeprice.app
- Android Auto: Search button is now located on main screen
- Android Auto: Replaced emojis with proper icons
Bugfixes:
- Crashes / inconsistencies after switching data source
- Problems when loading realtime availability data

View File

@@ -1,2 +0,0 @@
Bugfixes:
- some filter-dependent views were not correct anymore since 1.3.11

View File

@@ -1 +0,0 @@
Reverted Android Auto changes in 1.3.13, which caused crashes

View File

@@ -1,11 +0,0 @@
New features:
- Utilization prediction (only for DC chargers in Germany, powered by fronyx)
Improvements:
- Map and detail view adapted for large tablets
- Android Auto: More details on price comparison screen, Search button is now located on main screen, Replaced emojis with proper icons
Bugfixes:
- Inconsistent display while loading charger details
- Price comparison: Slowdown while switching connectors
- Android Auto: Fixed selecting highest-power chargepoint for price comparison

View File

@@ -1,6 +1,6 @@
#Sat Aug 06 15:33:46 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME