Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31fcee97e1 | ||
|
|
de8fd364f4 | ||
|
|
e3f271be5d | ||
|
|
99a2540398 | ||
|
|
85173b438b | ||
|
|
c288883572 | ||
|
|
c8f949da01 | ||
|
|
fe33dca1bc | ||
|
|
4fb5090e9b | ||
|
|
d9b8bf382a | ||
|
|
d69456dfd0 | ||
|
|
4da2a273c7 | ||
|
|
8e622c881d | ||
|
|
89b2175d89 | ||
|
|
3c30481821 | ||
|
|
385353689d | ||
|
|
7f9c838b9d | ||
|
|
205814e6f6 | ||
|
|
f30ae4a720 | ||
|
|
a6117d3484 | ||
|
|
f650def803 | ||
|
|
09ec0d1635 | ||
|
|
cfc98209a1 | ||
|
|
17dbab4659 | ||
|
|
f4ed7f7397 | ||
|
|
c26253fe44 | ||
|
|
1c7cc32427 | ||
|
|
4ff944c4e4 | ||
|
|
2ec437a14b | ||
|
|
ae05d44649 | ||
|
|
a0f90a8c94 | ||
|
|
e1b7463490 | ||
|
|
27cbb5e208 | ||
|
|
5526e5d97c | ||
|
|
fc92ee9cdc | ||
|
|
f20fab965c | ||
|
|
9146e7dab8 | ||
|
|
54936b6e1a | ||
|
|
f3245dc29b | ||
|
|
90968029ad | ||
|
|
97cab1d007 | ||
|
|
0d41fb2685 | ||
|
|
2c0a5085ab | ||
|
|
9e80270a78 | ||
|
|
1b374cda1c | ||
|
|
b82f6f68fb | ||
|
|
8e19399aaa | ||
|
|
e315da926e | ||
|
|
9450230856 | ||
|
|
81d62860e2 | ||
|
|
e825654b9c | ||
|
|
0a4878a129 | ||
|
|
af50a95abd | ||
|
|
fe58551de9 | ||
|
|
abc85c136b | ||
|
|
0ad4691d30 | ||
|
|
d85a64ec77 | ||
|
|
52fefb564a | ||
|
|
d18b2e26b8 | ||
|
|
d5f55366a9 | ||
|
|
2f93e92b57 | ||
|
|
24e5d072d6 | ||
|
|
a9e9055671 | ||
|
|
53ab8dc4e8 | ||
|
|
c0d7d59817 | ||
|
|
42cfdfee1d | ||
|
|
41cb6cf6b0 | ||
|
|
64f50cc5e6 | ||
|
|
c24c03bb32 | ||
|
|
bec25dd4d2 | ||
|
|
4e4c5a0e9a | ||
|
|
17bd7f024e | ||
|
|
fc5e77b01a | ||
|
|
6a114fc2ea | ||
|
|
6e32c6644c | ||
|
|
8fb34ae66f | ||
|
|
ae3489621e | ||
|
|
ff0a110f51 | ||
|
|
24ef4888a8 | ||
|
|
13916b0c8d | ||
|
|
efdd0d6bc5 | ||
|
|
155aca0041 | ||
|
|
6393eadc81 | ||
|
|
4319ece4f3 | ||
|
|
62f2002e5c | ||
|
|
4a82250a3d | ||
|
|
a8f23e9fb6 | ||
|
|
7da64fd566 | ||
|
|
09b5d536cb | ||
|
|
5e01200d96 | ||
|
|
c8d2e73218 | ||
|
|
d7b377ea56 | ||
|
|
edd35fba1b | ||
|
|
1f23080141 | ||
|
|
a3d9ecf49e | ||
|
|
6681d3cc17 | ||
|
|
a184b817bc | ||
|
|
b658e0183c | ||
|
|
6a0234ac2f | ||
|
|
d5ac35100b | ||
|
|
d3b4cb6a90 | ||
|
|
5d70d8c09a | ||
|
|
9642a58206 | ||
|
|
0e3280a119 | ||
|
|
c60043f925 | ||
|
|
b445be99bb | ||
|
|
02395dda7f | ||
|
|
c33c69db0b | ||
|
|
77fdfc7ccb | ||
|
|
bbb5c93132 | ||
|
|
2e8cdb01fd | ||
|
|
6b6c7da081 | ||
|
|
720d52285d | ||
|
|
e7efda2e90 | ||
|
|
ed80d7b968 | ||
|
|
8b1b971fad | ||
|
|
cf20ab8d82 | ||
|
|
581d0c07ec | ||
|
|
0b17821611 | ||
|
|
2493328715 | ||
|
|
f8abeed96c | ||
|
|
d9ca21c31e | ||
|
|
f6998382b1 | ||
|
|
5fc343d973 | ||
|
|
6b0a8bb506 | ||
|
|
93f379f4e2 | ||
|
|
00e555594a | ||
|
|
4ec5c8fb2e | ||
|
|
40b7ad8ef9 | ||
|
|
e1fed1ba26 | ||
|
|
d429ef88b3 | ||
|
|
9f0c5caf31 | ||
|
|
34b51a0742 | ||
|
|
a533fd315e | ||
|
|
d39d51d32c | ||
|
|
db11170967 | ||
|
|
4135740d07 | ||
|
|
b67bd12784 | ||
|
|
b0e000e936 | ||
|
|
1d8a7347c9 | ||
|
|
90f6cb65a8 | ||
|
|
5c57a5318b | ||
|
|
9456a6e8ef | ||
|
|
4846699f66 | ||
|
|
682f05b98b | ||
|
|
1f36ef6af8 | ||
|
|
032be00bcd | ||
|
|
3ac7b4aaee | ||
|
|
3386024acb | ||
|
|
ad2fb3063c | ||
|
|
caee3b1d67 | ||
|
|
60b151c690 | ||
|
|
e8873fa98c | ||
|
|
63740a8fe5 | ||
|
|
c80452a1fd | ||
|
|
7420101153 | ||
|
|
080d3d1080 | ||
|
|
d5ea8cfffa | ||
|
|
0676dcf31b |
23
README.md
@@ -20,7 +20,7 @@ Features
|
||||
- Advanced filtering options, including saved filter profiles
|
||||
- Favorites list, also with availability information
|
||||
- Integrated price comparison using [Chargeprice.app](https://chargeprice.app) (only in Europe)
|
||||
- Android Auto integration
|
||||
- Android Auto & Android Automotive OS integration
|
||||
- No ads, fully open source
|
||||
- Compatible with Android 5.0 and above
|
||||
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
|
||||
@@ -41,9 +41,24 @@ EVMap uses and put them into the app in the form of a resource file called `apik
|
||||
`app/src/main/res/values`. You can find more information on which API keys are necessary for which
|
||||
features and how they can be obtained in our [documentation page](doc/api_keys.md).
|
||||
|
||||
There are two different build flavors, `google` and `foss`, where only the `google` variant uses
|
||||
Google Maps data and provides the Android Auto integration. The `foss` variant only uses Mapbox data
|
||||
and should run on devices without Google Play Services.
|
||||
There are three different build flavors, `googleNormal`, `fossNormal` and `googleAutomotive`.
|
||||
- The `foss` variant only uses Mapbox data and should run on most Android devices, even without
|
||||
Google Play Services.
|
||||
- The `google` variants also include access to Google Maps data.
|
||||
- `googleNormal` is intended to run on smartphones and tablets, and also includes the Android
|
||||
Auto app for use on the car display.
|
||||
- `googleAutomotive` variant is intended to be installed directly on car infotainment systems
|
||||
using the Google-flavored Android Automotive OS. It does not provide the usual smartphone UI.
|
||||
|
||||
We also have a special [documentation page](doc/android_auto.md) on how to test the Android Auto
|
||||
app.
|
||||
|
||||
Translations
|
||||
------------
|
||||
|
||||
You can use our [Weblate page](https://hosted.weblate.org/projects/evmap/) to help translate EVMap
|
||||
into new languages.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/evmap/">
|
||||
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="500" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
23
_img/appicon_notification.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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 120 120" style="enable-background:new 0 0 120 120;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0"
|
||||
d="M27.1,88.3l-2.2-19.2l-3.3,0.3l2.2,19.2L27.1,88.3z M39,86.9l-2.2-19.2l-3.3,0.3l2.2,19.2L39,86.9z" />
|
||||
<path class="st0" d="M45.2,113c-1,1.3-1.8,2.1-2,2.2c-3,2.4-5.4,3.1-7.4,2.2c-3.5-1.7-3.2-8.2-3.1-8.9l2.4,0.1
|
||||
c-0.1,1.8,0.2,5.8,1.8,6.6c0.9,0.5,2.5-0.1,4.6-1.8l0,0c0,0,6.7-6.7,5.3-12c-1.6-6.4,5.8-15.5,8.2-18.6l0.3-0.3l2,1.5l-0.3,0.5
|
||||
c-7.5,9.2-8.3,14-7.7,16.4C50.5,105.4,47.4,110.4,45.2,113z" />
|
||||
<path class="st0" d="M19.7,88.1l0.9,7.9l7.3,4.9l9.8-1l6-6.4l-0.9-7.9L19.7,88.1z" />
|
||||
<g>
|
||||
<path class="st0"
|
||||
d="M37.6,99.7l-9.8,1l2.1,8.7l7.7-0.9V99.7L37.6,99.7z M44.6,79l0.8,7.2l-28.2,3.2l-0.8-7.2L44.6,79z" />
|
||||
</g>
|
||||
</g>
|
||||
<path class="st0" d="M66.7,0C46.5,0,30.1,16.4,30.1,36.6c0,27.6,30.8,42,34.5,81.4c0.1,1.2,1,2,2.2,2c1.2,0,2.1-0.8,2.2-2
|
||||
c3.7-39.4,34.5-53.8,34.5-81.4C103.3,16.2,86.9,0,66.7,0z M78.4,34.7L64.3,59V40.8h-6V18.7c0,0,20.2,0,20.1-0.1l-8.1,16.2H78.4z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,31 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 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 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
|
||||
viewBox="0 0 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{display:none;}
|
||||
.st2{display:inline;fill:#802C27;}
|
||||
.st3{fill:#808080;}
|
||||
.st4{display:none;fill:#802C27;}
|
||||
.st1{fill:#808080;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M109.8,0h13.6c33.9,1.9,67.1,18.5,87.7,45.8c13.5,17.2,21,38.6,22.7,60.3v8.1c-0.8,42.1-27.7,76.6-51,109.4
|
||||
c-26.2,37-50.4,77.3-57.1,122.9c-1.8,7.7,0.4,18.5-8.9,22c-2.2-1.7-4.7-3.1-6.2-5.4c-2.7-25.5-9.1-50.7-20-73.9
|
||||
c-12.3-27.1-29.5-51.6-47-75.6C33,199,23,184.2,14.7,168.3c-13-23.8-17.9-51.9-12.5-78.6c4.4-21.1,15.4-40.6,30.6-55.7
|
||||
C53.3,14,81.1,1.8,109.8,0z" />
|
||||
</g>
|
||||
</g>
|
||||
<g class="st1">
|
||||
<path class="st2" d="M107.2,74.1c18.9-4.8,40.4,5.5,47.7,23.7c6.1,14.5,1.9,32.5-9.9,42.9c-12.6,11.5-32.4,14-47.5,6
|
||||
c-13.9-6.8-23-22.6-21.3-38.1C77.6,92,91.1,77.7,107.2,74.1z" />
|
||||
</g>
|
||||
<path class="st0" d="M117,367.4c-0.4-0.3-0.8-0.6-1.2-0.9c-1.6-1.2-3.1-2.3-4.2-3.7c-2.9-26.9-9.6-51.7-20.1-74
|
||||
c-12.4-27.3-30.1-52.4-47.1-75.8c-8.7-12-19.8-27.9-28.8-45.2C2.3,143.6-2.1,115.9,3.2,89.9c4.3-20.4,15-40,30.3-55.2
|
||||
C53.6,15.1,81.5,2.8,109.9,1l13.5,0c34.4,1.9,66.9,18.9,86.9,45.4c12.8,16.3,20.8,37.5,22.5,59.8l0,8
|
||||
c-0.7,38.8-23.7,70.9-45.9,101.9c-1.7,2.3-3.3,4.6-5,6.9c-24.4,34.5-50.3,76.1-57.3,123.3c-0.5,2-0.7,4.3-0.9,6.5
|
||||
C123.3,359,122.8,364.9,117,367.4z" />
|
||||
<path class="st1" d="M123.3,2c34.1,1.9,66.3,18.8,86.2,45c12.6,16.1,20.5,37.1,22.3,59.1l0,8c-0.7,38.5-23.6,70.5-45.7,101.3
|
||||
c-1.7,2.3-3.3,4.6-5,6.9c-24.5,34.6-50.5,76.3-57.4,123.7c-0.5,2.1-0.7,4.4-0.9,6.7c-0.5,5.9-1,11-5.8,13.4
|
||||
c-0.2-0.2-0.5-0.4-0.7-0.5c-1.5-1.1-2.9-2-3.8-3.3c-2.9-26.9-9.7-51.8-20.1-74C80,261,62.3,235.8,45.2,212.4
|
||||
c-8.7-11.9-19.8-27.8-28.8-45.1C3.3,143.3-1,115.9,4.2,90.1c4.2-20.2,14.9-39.6,30-54.7C54.2,16,81.7,3.8,109.9,2H123.3 M123.4,0
|
||||
h-13.6c-28.7,1.8-56.5,14-77,34C17.6,49.1,6.6,68.6,2.2,89.7c-5.4,26.7-0.5,54.8,12.5,78.6C23,184.2,33,199,43.6,213.6
|
||||
c17.5,24,34.7,48.5,47,75.6c10.9,23.2,17.3,48.4,20,73.9c1.5,2.3,4,3.7,6.2,5.4c9.3-3.5,7.1-14.3,8.9-22
|
||||
c6.7-45.6,30.9-85.9,57.1-122.9c23.3-32.8,50.2-67.3,51-109.4v-8.1c-1.7-21.7-9.2-43.1-22.7-60.3C190.5,18.5,157.3,1.9,123.4,0
|
||||
L123.4,0z" />
|
||||
</g>
|
||||
<path class="st3" d="M90.9,57.3v68.2h18.6v55.8l43.4-74.4h-24.8l24.8-49.6H90.9z" />
|
||||
<path class="st4" d="M159,85.3L159,85.3l-20.8-20.9l-5.9,5.9l11.8,11.8c-5.3,2-9,7.1-9,13.1c0,7.7,6.3,14,14,14c2,0,3.9-0.4,5.6-1.2
|
||||
v40.4c0,3.1-2.5,5.6-5.6,5.6s-5.6-2.5-5.6-5.6v-25.2c0-6.2-5-11.2-11.2-11.2h-5.6V72.8c0-6.2-5-11.2-11.2-11.2H81.8
|
||||
c-6.2,0-11.2,5-11.2,11.2v89.7h56.1v-42.1h8.4v28c0,7.7,6.3,14,14,14s14-6.3,14-14V95.2C163.1,91.3,161.6,87.8,159,85.3
|
||||
M149.1,100.8c-3.1,0-5.6-2.5-5.6-5.6c0-3.1,2.5-5.6,5.6-5.6s5.6,2.5,5.6,5.6C154.7,98.3,152.2,100.8,149.1,100.8 M93.1,145.6v-25.2
|
||||
H81.8l22.4-42.1v28h11.2L93.1,145.6z" />
|
||||
<path class="st1"
|
||||
d="M90.9,57.3v68.2h18.6v55.8l43.4-74.4h-24.8l24.8-49.6C152.9,57.3,90.9,57.3,90.9,57.3z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,27 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 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 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#B5B5B5;}
|
||||
.st2{fill:#808080;}
|
||||
.st1{fill:#808080;}
|
||||
.st2{fill:#B5B5B5;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M109.8,0h13.6c33.9,1.9,67.1,18.5,87.7,45.8c13.5,17.2,21,38.6,22.7,60.3v8.1c-0.8,42.1-27.7,76.6-51,109.4
|
||||
c-26.2,37-50.4,77.3-57.1,122.9c-1.8,7.7,0.4,18.5-8.9,22c-2.2-1.7-4.7-3.1-6.2-5.4c-2.7-25.5-9.1-50.7-20-73.9
|
||||
c-12.3-27.1-29.5-51.6-47-75.6C33,199,23,184.2,14.7,168.3c-13-23.8-17.9-51.9-12.5-78.6C6.6,68.6,17.6,49.1,32.8,34
|
||||
C53.3,14,81.1,1.8,109.8,0z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st1"
|
||||
points="143.2,109.4 123.5,143.2 123.5,181.3 166.9,106.9 144.7,106.9 " />
|
||||
<path class="st1"
|
||||
d="M122.2,101.9h16.7h5.7l22.3-44.6c0,0-10.2,0-22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path class="st2" d="M138.9,57.3c-9.7,0-19.8,0-26.4,0c-2.5,0-5.1,0-7.6,0c-8.2,0-16.1,0-21.4,0c-4.1,0-6.6,0-6.6,0v68.2h18.6v55.8
|
||||
l43.4-74.4h-24.8L138.9,57.3z" />
|
||||
<path class="st0" d="M117,367.4c-0.4-0.3-0.8-0.6-1.2-0.9c-1.6-1.2-3.1-2.3-4.2-3.7c-2.9-26.9-9.6-51.7-20.1-74
|
||||
c-12.4-27.3-30.1-52.4-47.1-75.8c-8.7-12-19.8-27.9-28.8-45.2C2.3,143.6-2.1,115.9,3.2,89.9c4.3-20.4,15-40,30.3-55.2
|
||||
C53.6,15.1,81.5,2.8,109.9,1l13.5,0c34.4,1.9,66.9,18.9,86.9,45.4c12.8,16.3,20.8,37.5,22.5,59.8l0,8
|
||||
c-0.7,38.8-23.7,70.9-45.9,101.9c-1.7,2.3-3.3,4.6-5,6.9c-24.4,34.5-50.3,76.1-57.3,123.3c-0.5,2-0.7,4.3-0.9,6.5
|
||||
C123.3,359,122.8,364.9,117,367.4z" />
|
||||
<path class="st1" d="M123.3,2c34.1,1.9,66.3,18.8,86.2,45c12.6,16.1,20.5,37.1,22.3,59.1l0,8c-0.7,38.5-23.6,70.5-45.7,101.3
|
||||
c-1.7,2.3-3.3,4.6-5,6.9c-24.5,34.6-50.5,76.3-57.4,123.7c-0.5,2.1-0.7,4.4-0.9,6.7c-0.5,5.9-1,11-5.8,13.4
|
||||
c-0.2-0.2-0.5-0.4-0.7-0.5c-1.5-1.1-2.9-2-3.8-3.3c-2.9-26.9-9.7-51.8-20.1-74C80,261,62.3,235.8,45.2,212.4
|
||||
c-8.7-11.9-19.8-27.8-28.8-45.1C3.3,143.3-1,115.9,4.2,90.1c4.2-20.2,14.9-39.6,30-54.7C54.2,16,81.7,3.8,109.9,2H123.3 M123.4,0
|
||||
h-13.6c-28.7,1.8-56.5,14-77,34C17.6,49.1,6.6,68.6,2.2,89.7c-5.4,26.7-0.5,54.8,12.5,78.6C23,184.2,33,199,43.6,213.6
|
||||
c17.5,24,34.7,48.5,47,75.6c10.9,23.2,17.3,48.4,20,73.9c1.5,2.3,4,3.7,6.2,5.4c9.3-3.5,7.1-14.3,8.9-22
|
||||
c6.7-45.6,30.9-85.9,57.1-122.9c23.3-32.8,50.2-67.3,51-109.4v-8.1c-1.7-21.7-9.2-43.1-22.7-60.3C190.5,18.5,157.3,1.9,123.4,0
|
||||
L123.4,0z" />
|
||||
</g>
|
||||
<polygon class="st2" points="143.2,109.4 123.5,143.2 123.5,181.3 166.9,106.9 144.7,106.9 " />
|
||||
<path class="st2" d="M122.2,101.9h16.7h5.7l22.3-44.6c0,0-10.2,0-22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path class="st1" d="M138.9,57.3c-9.7,0-19.8,0-26.4,0c-2.5,0-5.1,0-7.6,0c-8.2,0-16.1,0-21.4,0c-4.1,0-6.6,0-6.6,0v68.2h18.6v55.8
|
||||
l43.4-74.4h-24.8L138.9,57.3z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -8,9 +8,12 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
||||
|
||||
def supportedLocales = "en,de,fr,nb-rNO"
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
@@ -18,10 +21,12 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 82
|
||||
versionName "1.3.3"
|
||||
versionCode 116
|
||||
versionName "1.3.11"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs supportedLocales.split(",")
|
||||
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -92,6 +97,15 @@ android {
|
||||
}
|
||||
lint {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
warning 'MissingTranslation'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
|
||||
resourcePlaceholders {
|
||||
files = ['xml/shortcuts.xml']
|
||||
}
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
@@ -137,11 +151,11 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0-beta01'
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.5.1"
|
||||
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'
|
||||
@@ -149,44 +163,50 @@ 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:f69f532660'
|
||||
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 'moe.banana:moshi-jsonapi:3.5.0'
|
||||
implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.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"
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
implementation 'com.airbnb.android:lottie:4.1.0'
|
||||
implementation 'io.michaelrocks.bimap:bimap:1.1.0'
|
||||
implementation 'com.mapzen.android:lost:3.0.2'
|
||||
implementation 'com.google.guava:guava:29.0-android'
|
||||
implementation 'com.github.pengrad:mapscaleview:1.6.0'
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
googleImplementation 'androidx.car.app:app:1.2.0-rc01'
|
||||
googleNormalImplementation 'androidx.car.app:app-projected:1.2.0-rc01'
|
||||
googleAutomotiveImplementation 'androidx.car.app:app-automotive:1.2.0-rc01'
|
||||
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"
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '3c67d7a1dc'
|
||||
def anyMapsVersion = 'a9b3dd7d99'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.0.2'
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
|
||||
implementation("com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
|
||||
exclude group: 'com.google.android.gms', module: 'play-services-location'
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-core'
|
||||
}
|
||||
// patched version of mapbox-android-core that removes build-time dependency on GMS
|
||||
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
|
||||
|
||||
// Google Places
|
||||
implementation 'com.google.android.libraries.places:places:2.6.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
|
||||
googleImplementation 'com.google.android.libraries.places:places:2.6.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
|
||||
@@ -196,12 +216,12 @@ dependencies {
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// viewmodel library
|
||||
def lifecycle_version = "2.4.1"
|
||||
def lifecycle_version = "2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.4.2"
|
||||
def room_version = "2.4.3"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
@@ -218,12 +238,20 @@ dependencies {
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
|
||||
|
||||
// testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
|
||||
// testing for car app
|
||||
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.8.1'
|
||||
testGoogleImplementation 'androidx.test:core:1.4.0'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
|
||||
5
app/src/debug/res/values/donottranslate.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="chargeprice_api_url">https://staging-api.chargeprice.app/v1/</string>
|
||||
<string name="chargeprice_key">20c0d68918c9dc96c564784b711a6570</string>
|
||||
</resources>
|
||||
@@ -1,14 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" tranlatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
|
||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap (Mapbox).</string>
|
||||
6
app/src/foss/res/values-fr/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.</string>
|
||||
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap (Mapbox).</string>
|
||||
<string name="donate_paypal">Faire un don avec PayPal</string>
|
||||
</resources>
|
||||
6
app/src/foss/res/values-nb-rNO/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donate_paypal">Doner med PayPal</string>
|
||||
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap (Mapbox).</string>
|
||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
|
||||
</resources>
|
||||
15
app/src/foss/res/values/arrays.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>@string/pref_provider_osm_mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" translatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>@string/pref_provider_osm_mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" translatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
6
app/src/foss/res/values/donottranslate.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
<string name="pref_map_provider_default" translatable="false">mapbox</string>
|
||||
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
|
||||
</resources>
|
||||
6
app/src/foss/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
|
||||
<string name="donate_paypal">Donate with PayPal</string>
|
||||
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" tranlatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" tranlatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
<string name="pref_map_provider_default" translatable="false">mapbox</string>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
|
||||
<string name="donate_paypal">Donate with PayPal</string>
|
||||
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
|
||||
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
@@ -7,16 +7,10 @@ import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.maps.MapsInitializer
|
||||
import com.google.android.libraries.places.api.Places
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
|
||||
fun init(context: Context) {
|
||||
Places.initialize(context, context.getString(R.string.google_maps_key))
|
||||
|
||||
val localeContext = LocaleContextWrapper.wrap(
|
||||
context.applicationContext, PreferenceDataSource(context).language
|
||||
)
|
||||
MapsInitializer.initialize(localeContext, MapsInitializer.Renderer.LATEST, null)
|
||||
MapsInitializer.initialize(context, MapsInitializer.Renderer.LATEST, null)
|
||||
}
|
||||
|
||||
fun checkPlayServices(activity: Activity): Boolean {
|
||||
|
||||
@@ -5,16 +5,15 @@ import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.ScreenManager
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
@@ -25,10 +24,14 @@ import androidx.car.app.validation.HostValidator
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.location.Priority
|
||||
import net.vonforst.evmap.utils.checkFineLocationPermission
|
||||
|
||||
|
||||
interface LocationAwareScreen {
|
||||
@@ -68,7 +71,8 @@ class CarAppService : androidx.car.app.CarAppService() {
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setSmallIcon(R.drawable.ic_appicon_notification)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setTicker(getString(R.string.auto_location_service))
|
||||
.setWhen(System.currentTimeMillis())
|
||||
|
||||
@@ -99,8 +103,8 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
location?.let { value?.updateLocation(it) }
|
||||
}
|
||||
private var location: Location? = null
|
||||
private val locationManager: LocationManager by lazy {
|
||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private val locationEngine: LocationEngine by lazy {
|
||||
FusionEngine(carContext)
|
||||
}
|
||||
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
@@ -112,10 +116,25 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
return MapScreen(carContext, this)
|
||||
val mapScreen = MapScreen(carContext, this)
|
||||
|
||||
if (!locationPermissionGranted()) {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return mapScreen
|
||||
}
|
||||
|
||||
fun locationPermissionGranted() = carContext.checkAnyLocationPermission()
|
||||
private fun locationPermissionGranted() = carContext.checkFineLocationPermission()
|
||||
|
||||
private fun updateLocation(location: Location?) {
|
||||
Log.d(TAG, "Received location: $location")
|
||||
@@ -142,6 +161,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
requestPhoneLocationUpdates()
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION])
|
||||
private fun requestCarHardwareLocationUpdates() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
@@ -153,15 +173,18 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
}
|
||||
}
|
||||
|
||||
private val phoneLocationListener = LocationListenerCompat {
|
||||
this.updateLocation(it)
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||
private fun requestPhoneLocationUpdates() {
|
||||
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
val location = locationEngine.getLastKnownLocation()
|
||||
updateLocation(location)
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
locationEngine.requestLocationUpdates(
|
||||
Priority.HIGH_ACCURACY,
|
||||
1000,
|
||||
1f,
|
||||
this::updateLocation
|
||||
phoneLocationListener
|
||||
)
|
||||
}
|
||||
|
||||
@@ -181,7 +204,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||
private fun removePhoneLocationUpdates() {
|
||||
locationManager.removeUpdates(this::updateLocation)
|
||||
locationEngine.removeUpdates(phoneLocationListener)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
|
||||
140
app/src/google/java/net/vonforst/evmap/auto/CarSensors.kt
Normal file
@@ -0,0 +1,140 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.common.CarValue
|
||||
import androidx.car.app.hardware.common.OnCarDataAvailableListener
|
||||
import androidx.car.app.hardware.info.*
|
||||
import androidx.car.app.hardware.info.CarSensors.UpdateRate
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* CarSensors is not yet implemented for Android Automotive OS
|
||||
* (see docs at https://developer.android.com/reference/androidx/car/app/hardware/info/CarSensors)
|
||||
* so we provide our own implementation based on SensorManager APIs.
|
||||
*/
|
||||
val CarContext.patchedCarSensors: CarSensors
|
||||
get() = if (BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
(this.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carSensors
|
||||
} else {
|
||||
CarSensorsWrapper(this)
|
||||
}
|
||||
|
||||
class CarSensorsWrapper(carContext: CarContext) :
|
||||
CarSensors {
|
||||
private val sensorManager = carContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private val compassListeners: MutableMap<OnCarDataAvailableListener<Compass>, SensorEventListener> =
|
||||
mutableMapOf()
|
||||
|
||||
override fun addAccelerometerListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<Accelerometer>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeAccelerometerListener(listener: OnCarDataAvailableListener<Accelerometer>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addGyroscopeListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<Gyroscope>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeGyroscopeListener(listener: OnCarDataAvailableListener<Gyroscope>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addCompassListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<Compass>
|
||||
) {
|
||||
val magSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
||||
val accSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
|
||||
if (magSensor == null) {
|
||||
executor.execute {
|
||||
listener.onCarDataAvailable(Compass(CarValue(null, 0, CarValue.STATUS_UNAVAILABLE)))
|
||||
}
|
||||
return
|
||||
}
|
||||
val sensorListener = object : SensorEventListener {
|
||||
var magValues: FloatArray? = null
|
||||
|
||||
// AAOS cars may not provide an acceleration sensor, so we assume acceleration based on
|
||||
// Earth's gravity. May not be correct when driving on other planets.
|
||||
var accValues = floatArrayOf(0f, 0f, SensorManager.GRAVITY_EARTH)
|
||||
val rotMatrix = FloatArray(9)
|
||||
val orientation = FloatArray(3)
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
when (event.sensor) {
|
||||
magSensor -> magValues = event.values
|
||||
accSensor -> accValues = event.values
|
||||
}
|
||||
if (magValues == null) return
|
||||
|
||||
SensorManager.getRotationMatrix(rotMatrix, null, accValues, magValues)
|
||||
SensorManager.getOrientation(rotMatrix, orientation)
|
||||
val compassDegrees = orientation.map { Math.toDegrees(it.toDouble()).toFloat() }
|
||||
executor.execute {
|
||||
listener.onCarDataAvailable(
|
||||
Compass(
|
||||
CarValue(
|
||||
compassDegrees,
|
||||
event.timestamp,
|
||||
CarValue.STATUS_SUCCESS
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||
}
|
||||
|
||||
}
|
||||
compassListeners[listener] = sensorListener
|
||||
sensorManager.registerListener(sensorListener, magSensor, mapRate(rate))
|
||||
accSensor?.let { sensorManager.registerListener(sensorListener, it, mapRate(rate)) }
|
||||
}
|
||||
|
||||
private fun mapRate(@UpdateRate rate: Int): Int {
|
||||
return when (rate) {
|
||||
CarSensors.UPDATE_RATE_NORMAL -> SensorManager.SENSOR_DELAY_NORMAL
|
||||
CarSensors.UPDATE_RATE_UI -> SensorManager.SENSOR_DELAY_UI
|
||||
CarSensors.UPDATE_RATE_FASTEST -> SensorManager.SENSOR_DELAY_FASTEST
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeCompassListener(listener: OnCarDataAvailableListener<Compass>) {
|
||||
compassListeners[listener]?.let {
|
||||
sensorManager.unregisterListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCarHardwareLocationListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<CarHardwareLocation>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeCarHardwareLocationListener(listener: OnCarDataAvailableListener<CarHardwareLocation>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@ import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import jsonapi.Meta
|
||||
import jsonapi.Relationship
|
||||
import jsonapi.Relationships
|
||||
import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.banana.jsonapi2.HasMany
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
import moe.banana.jsonapi2.JsonBuffer
|
||||
import moe.banana.jsonapi2.ResourceIdentifier
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
@@ -34,7 +34,10 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
|
||||
ChargepriceApi.create(
|
||||
carContext.getString(R.string.chargeprice_key),
|
||||
carContext.getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
private var prices: List<ChargePrice>? = null
|
||||
private var meta: ChargepriceChargepointMeta? = null
|
||||
@@ -94,7 +97,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
)
|
||||
.build().intent
|
||||
intent.data =
|
||||
Uri.parse("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter()}")
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
carContext.startActivity(intent)
|
||||
@@ -169,39 +172,44 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}
|
||||
|
||||
private fun loadPrices(model: Model?) {
|
||||
val dataAdapter = getDataAdapter() ?: return
|
||||
val dataAdapter = ChargepriceApi.getDataAdapter(charger) ?: return
|
||||
val manufacturer = model?.manufacturer?.value
|
||||
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val car = determineVehicle(manufacturer, modelName)
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
|
||||
val result = api.getChargePrices(ChargepriceRequest().apply {
|
||||
this.dataAdapter = dataAdapter
|
||||
station = cpStation
|
||||
vehicle = HasOne(car)
|
||||
tariffs = if (!prefs.chargepriceMyTariffsAll) {
|
||||
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
|
||||
HasMany<ChargepriceTariff>(*myTariffs.map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
it
|
||||
val result = api.getChargePrices(
|
||||
ChargepriceRequest(
|
||||
dataAdapter = dataAdapter,
|
||||
station = cpStation,
|
||||
vehicle = car,
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange.map { it.toDouble() },
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
|
||||
),
|
||||
relationships = if (!prefs.chargepriceMyTariffsAll) {
|
||||
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
|
||||
Relationships(
|
||||
"tariffs" to Relationship.ToMany(
|
||||
myTariffs.map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
id = it
|
||||
)
|
||||
},
|
||||
meta = Meta.from(
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
|
||||
ChargepriceApi.moshi
|
||||
)
|
||||
)
|
||||
)
|
||||
}.toTypedArray()).apply {
|
||||
meta = JsonBuffer.create(
|
||||
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange.map { it.toDouble() },
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
|
||||
)
|
||||
}, ChargepriceApi.getChargepriceLanguage())
|
||||
} else null
|
||||
), ChargepriceApi.getChargepriceLanguage()
|
||||
)
|
||||
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
|
||||
@@ -215,14 +223,16 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
meta =
|
||||
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull {
|
||||
it.power
|
||||
}
|
||||
|
||||
prices = result.map { cp ->
|
||||
val metaMapped =
|
||||
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||
meta = metaMapped.chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull {
|
||||
it.power
|
||||
}
|
||||
|
||||
prices = result.data!!.map { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == chargepoint.plug && it.power == chargepoint.power
|
||||
@@ -230,15 +240,15 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.clone().apply {
|
||||
cp.copy(
|
||||
chargepointPrices = filteredPrices
|
||||
}
|
||||
)
|
||||
}
|
||||
}.filterNotNull()
|
||||
.sortedBy { it.chargepointPrices.first().price }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariff?.get()?.id in myTariffs
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
}
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
@@ -316,10 +326,4 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}
|
||||
return vehicles[0]
|
||||
}
|
||||
|
||||
private fun getDataAdapter(): String? = when (charger.dataSource) {
|
||||
"goingelectric" -> ChargepriceApi.DATA_SOURCE_GOINGELECTRIC
|
||||
"openchargemap" -> ChargepriceApi.DATA_SOURCE_OPENCHARGEMAP
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package net.vonforst.evmap.auto
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
@@ -32,17 +34,19 @@ import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
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.getReferenceData
|
||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
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
|
||||
@@ -50,14 +54,11 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
|
||||
private val imageSize = 128 // images should be 128dp according to docs
|
||||
private val imageHeightLarge = 480 // images should be 480 x 854 dp according to docs
|
||||
private val imageWidthLarge = 854
|
||||
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
|
||||
|
||||
private val iconGen =
|
||||
ChargerIconGenerator(carContext, null, height = imageSize)
|
||||
@@ -72,9 +73,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
private var favoriteUpdateJob: Job? = null
|
||||
|
||||
init {
|
||||
referenceData.observe(this) {
|
||||
loadCharger()
|
||||
}
|
||||
loadCharger()
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -357,27 +356,21 @@ 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 = api.getChargepointDetail(referenceData, chargerSparse.id)
|
||||
val response = repo.getChargepointDetail(chargerSparse.id).awaitFinished()
|
||||
if (response.status == Status.SUCCESS) {
|
||||
val charger = response.data!!
|
||||
|
||||
val photo = charger.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val density = carContext.resources.displayMetrics.density
|
||||
val url = if (largeImageSupported) {
|
||||
photo.getUrl(
|
||||
width = (imageWidthLarge * density).roundToInt(),
|
||||
height = (imageHeightLarge * density).roundToInt()
|
||||
)
|
||||
} else {
|
||||
photo.getUrl(size = (imageSize * density).roundToInt())
|
||||
}
|
||||
val size =
|
||||
(density * if (largeImageSupported) imageSizeLarge else imageSize).roundToInt()
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
var img =
|
||||
val img =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
|
||||
// draw icon on top of image
|
||||
@@ -387,19 +380,29 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
|
||||
img = img.copy(Bitmap.Config.ARGB_8888, true)
|
||||
val outImg = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val iconSmall = icon.scale(
|
||||
(img.height * 0.4 / icon.height * icon.width).roundToInt(),
|
||||
(img.height * 0.4).roundToInt()
|
||||
(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
|
||||
)
|
||||
val canvas = Canvas(img)
|
||||
canvas.drawBitmap(
|
||||
iconSmall,
|
||||
0f,
|
||||
(img.height - iconSmall.height * 1.1).toFloat(),
|
||||
(size - iconSmall.height * 1.1).toFloat(),
|
||||
null
|
||||
)
|
||||
this@ChargerDetailScreen.photo = img
|
||||
this@ChargerDetailScreen.photo = outImg
|
||||
}
|
||||
this@ChargerDetailScreen.charger = charger
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import net.vonforst.evmap.viewmodel.FilterViewModel
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(ctx)
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
@@ -47,6 +47,30 @@ class FilterScreen(ctx: CarContext) : 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(
|
||||
@@ -65,42 +89,6 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
}
|
||||
})
|
||||
}.build())
|
||||
if (filterStatus !in listOf(
|
||||
FILTERS_CUSTOM,
|
||||
FILTERS_FAVORITES,
|
||||
FILTERS_DISABLED
|
||||
)
|
||||
) {
|
||||
addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_delete
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener {
|
||||
val currentProfile =
|
||||
filterProfiles.value?.find { it.id == filterStatus }
|
||||
?: return@setOnClickListener
|
||||
lifecycleScope.launch {
|
||||
db.filterProfileDao().delete(currentProfile)
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(
|
||||
R.string.deleted_filterprofile,
|
||||
currentProfile.name
|
||||
),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
}.build()
|
||||
@@ -190,33 +178,46 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
setHeaderAction(Action.BACK)
|
||||
setActionStrip(ActionStrip.Builder().apply {
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_check
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
vm.saveFilterValues()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
val currentProfile = vm.filterProfile.value
|
||||
if (currentProfile != null) {
|
||||
addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_delete
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
vm.deleteCurrentProfile()
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(
|
||||
R.string.deleted_filterprofile,
|
||||
currentProfile.name
|
||||
),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
invalidate()
|
||||
screenManager.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_save
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
}.build())
|
||||
}
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_save
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
val textPromptScreen = TextPromptScreen(
|
||||
carContext,
|
||||
R.string.save_as_profile,
|
||||
@@ -248,18 +249,21 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
is BooleanFilter -> {
|
||||
setToggle(Toggle.Builder {
|
||||
(value as BooleanFilterValue).value = it
|
||||
lifecycleScope.launch { vm.saveFilterValues() }
|
||||
}.setChecked((value as BooleanFilterValue).value).build())
|
||||
}
|
||||
is MultipleChoiceFilter -> {
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
screenManager.pushForResult(
|
||||
MultipleChoiceFilterScreen(
|
||||
carContext,
|
||||
filter,
|
||||
value as MultipleChoiceFilterValue
|
||||
)
|
||||
)
|
||||
) {
|
||||
lifecycleScope.launch { vm.saveFilterValues() }
|
||||
}
|
||||
}
|
||||
addText(
|
||||
if ((value as MultipleChoiceFilterValue).all) {
|
||||
@@ -276,13 +280,15 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
setBrowsable(true)
|
||||
addText((value as SliderFilterValue).value.toString() + " " + filter.unit)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
screenManager.pushForResult(
|
||||
SliderFilterScreen(
|
||||
carContext,
|
||||
filter,
|
||||
value
|
||||
)
|
||||
)
|
||||
) {
|
||||
lifecycleScope.launch { vm.saveFilterValues() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,7 +385,7 @@ class SliderFilterScreen(
|
||||
private fun generateSlider(): CharSequence {
|
||||
val bar = "━"
|
||||
val dot = "⬤"
|
||||
val length = 35
|
||||
val length = 30
|
||||
|
||||
val position =
|
||||
((filter.inverseMapping(value.value) - filter.min) / (filter.max - filter.min).toDouble() * length).roundToInt()
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.Manifest
|
||||
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
|
||||
@@ -16,7 +13,9 @@ 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.*
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
@@ -27,19 +26,23 @@ 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.distanceBetween
|
||||
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.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
@@ -48,7 +51,7 @@ import kotlin.math.roundToInt
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
|
||||
ItemList.OnItemVisibilityChangedListener {
|
||||
ItemList.OnItemVisibilityChangedListener, DefaultLifecycleObserver {
|
||||
companion object {
|
||||
val MARKER = "map"
|
||||
}
|
||||
@@ -64,28 +67,26 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
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) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST)
|
||||
min(
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST),
|
||||
25
|
||||
)
|
||||
} else 6
|
||||
|
||||
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 var filterStatus = prefs.filterStatus
|
||||
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
|
||||
|
||||
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
}
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
listOf(
|
||||
@@ -99,29 +100,39 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
)
|
||||
}
|
||||
|
||||
private var searchLocation: LatLng? = null
|
||||
|
||||
init {
|
||||
filtersWithValue.observe(this) {
|
||||
loadChargers()
|
||||
}
|
||||
lifecycle.addObserver(this)
|
||||
marker = MARKER
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
checkLocationPermission()
|
||||
session.requestLocationUpdates()
|
||||
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
prefs.placeSearchResultAndroidAutoName?.let {
|
||||
carContext.getString(R.string.auto_chargers_near_location, it)
|
||||
} ?: carContext.getString(
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).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()
|
||||
@@ -132,7 +143,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
@@ -144,18 +155,19 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
} ?: setLoading(true)
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
val filtersCount = if (filterStatus.value == FILTERS_FAVORITES) 1 else {
|
||||
filtersWithValue.value?.count {
|
||||
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
|
||||
filtersWithValue?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
}
|
||||
|
||||
setActionStrip(
|
||||
ActionStrip.Builder()
|
||||
.addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_settings
|
||||
)
|
||||
@@ -179,10 +191,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.pushForResult(FilterScreen(carContext)) {
|
||||
chargers = null
|
||||
filterStatus.value = prefs.filterStatus
|
||||
}
|
||||
screenManager.push(FilterScreen(carContext, session))
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
@@ -191,25 +200,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun checkLocationPermission() {
|
||||
if (!session.locationPermissionGranted()) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
screenManager.pushForResult(
|
||||
PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_location_permission_needed,
|
||||
listOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
)
|
||||
) {
|
||||
session.requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
|
||||
val markerTint = if ((charger.maxPower ?: 0.0) > 100) {
|
||||
R.color.charger_100kw_dark // slightly darker color for better contrast
|
||||
@@ -287,13 +277,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.pushForResult(ChargerDetailScreen(carContext, charger)) {
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
// favorites list may have been updated
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
}
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
session.mapScreen = null
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
@@ -304,10 +289,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
) {
|
||||
return
|
||||
}
|
||||
val previousLocation = this.location
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
return
|
||||
if (previousLocation == null) {
|
||||
loadChargers()
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
@@ -322,13 +307,21 @@ 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 {
|
||||
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.value == FILTERS_FAVORITES) {
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
chargers =
|
||||
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
|
||||
distanceBetween(
|
||||
@@ -337,48 +330,69 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
val response = repo.getChargepointsRadius(
|
||||
searchLocation,
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
filtersWithValue
|
||||
).awaitFinished()
|
||||
if (response.status == Status.ERROR) {
|
||||
withContext(Dispatchers.Main) { showLoadingError() }
|
||||
return@launch
|
||||
}
|
||||
var chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < maxRows) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
val response = repo.getChargepointsRadius(
|
||||
searchLocation,
|
||||
searchRadius * 10,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
filtersWithValue
|
||||
).awaitFinished()
|
||||
if (response.status == Status.ERROR) {
|
||||
withContext(Dispatchers.Main) { showLoadingError() }
|
||||
return@launch
|
||||
}
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
}
|
||||
this@MapScreen.chargers = chargers
|
||||
}
|
||||
|
||||
updateCoroutine = null
|
||||
lastDistanceUpdateTime = Instant.now()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
withContext(Dispatchers.Main) { showLoadingError() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
this.energyLevel = energyLevel
|
||||
invalidate()
|
||||
private fun showLoadingError() {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
this.energyLevel = energyLevel
|
||||
if (isUpdate) invalidate()
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
private fun setupListeners() {
|
||||
if (!permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
@@ -388,25 +402,45 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
})
|
||||
return
|
||||
|
||||
println("Setting up energy level listener")
|
||||
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Setting up energy level listener")
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
||||
// Deleting the data already in onStop makes sure that we show a loading screen directly
|
||||
// (i.e. onGetTemplate is not called while the old data is still there)
|
||||
chargers = null
|
||||
availabilities.clear()
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
private fun removeListeners() {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentRefreshRequested() {
|
||||
loadChargers()
|
||||
availabilities.clear()
|
||||
|
||||
val start = visibleStart
|
||||
val end = visibleEnd
|
||||
if (start != null && end != null) {
|
||||
onItemVisibilityChanged(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemVisibilityChanged(startIndex: Int, endIndex: Int) {
|
||||
// when the list is scrolled, load corresponding availabilities
|
||||
if (startIndex == visibleStart && endIndex == visibleEnd) return
|
||||
if (startIndex == visibleStart && endIndex == visibleEnd && !availabilities.isEmpty()) return
|
||||
if (startIndex == -1 || endIndex == -1) return
|
||||
if (availabilityUpdateCoroutine != null) return
|
||||
|
||||
visibleEnd = endIndex
|
||||
@@ -423,7 +457,14 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// update availabilities
|
||||
availabilityUpdateCoroutine = lifecycleScope.launch {
|
||||
delay(300L)
|
||||
val tasks = chargers?.subList(startIndex, endIndex)?.mapNotNull {
|
||||
|
||||
val chargers = chargers ?: return@launch
|
||||
if (chargers.isEmpty()) return@launch
|
||||
|
||||
val tasks = chargers.subList(
|
||||
min(startIndex, chargers.size - 1),
|
||||
min(endIndex, chargers.size - 1)
|
||||
).mapNotNull {
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
lifecycleScope.async {
|
||||
@@ -433,7 +474,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
} else null
|
||||
}
|
||||
if (!tasks.isNullOrEmpty()) {
|
||||
if (tasks.isNotEmpty()) {
|
||||
tasks.awaitAll()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import net.vonforst.evmap.R
|
||||
class PermissionScreen(
|
||||
ctx: CarContext,
|
||||
@StringRes val message: Int,
|
||||
val permissions: List<String>
|
||||
val permissions: List<String>,
|
||||
val finishApp: Boolean = true
|
||||
) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return MessageTemplate.Builder(carContext.getString(message))
|
||||
@@ -31,7 +32,13 @@ class PermissionScreen(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.cancel))
|
||||
.setOnClickListener {
|
||||
carContext.finishCarApp()
|
||||
if (finishApp) {
|
||||
carContext.finishCarApp()
|
||||
} else {
|
||||
// pop twice to get away from the screen that requires the permission
|
||||
screenManager.pop()
|
||||
screenManager.pop()
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
|
||||
234
app/src/google/java/net/vonforst/evmap/auto/PlaceSearchScreen.kt
Normal file
@@ -0,0 +1,234 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
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.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.iconForPlaceType
|
||||
import net.vonforst.evmap.adapter.isSpecialPlace
|
||||
import net.vonforst.evmap.autocomplete.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.storage.RecentAutocompletePlace
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
|
||||
@ExperimentalCarApi
|
||||
class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx),
|
||||
SearchTemplate.SearchCallback, LocationAwareScreen,
|
||||
DefaultLifecycleObserver {
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
}
|
||||
private var resultList: List<AutocompletePlace>? = null
|
||||
private var recentResults = mutableListOf<RecentAutocompletePlace>()
|
||||
private var currentProvider: AutocompleteProvider? = null
|
||||
private val providers = getAutocompleteProviders(ctx)
|
||||
private val recents = AppDatabase.getInstance(ctx).recentAutocompletePlaceDao()
|
||||
private val maxItems = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private var location: Location? = null
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var updateJob: Job? = null
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
listOf(
|
||||
"android.car.permission.CAR_ENERGY",
|
||||
"android.car.permission.CAR_ENERGY_PORTS",
|
||||
"android.car.permission.READ_CAR_DISPLAY_UNITS",
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
"com.google.android.gms.permission.CAR_FUEL"
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
update("")
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return SearchTemplate.Builder(this).apply {
|
||||
setHeaderAction(Action.BACK)
|
||||
setSearchHint(carContext.getString(R.string.search))
|
||||
resultList?.let {
|
||||
setItemList(buildItemList(it))
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildItemList(results: List<AutocompletePlace>): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
results.forEach { place ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(place.primaryText)
|
||||
addText(place.secondaryText)
|
||||
|
||||
val icon = iconForPlaceType(place.types)
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, icon))
|
||||
.setTint(if (isSpecialPlace(place.types)) CarColor.PRIMARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
|
||||
// distance
|
||||
place.distanceMeters?.let {
|
||||
val text = SpannableStringBuilder()
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(
|
||||
roundValueToDistance(
|
||||
it,
|
||||
energyLevel?.distanceDisplayUnit?.value
|
||||
)
|
||||
),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
addText(text)
|
||||
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id)
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onSearchTextChanged(searchText: String) {
|
||||
update(searchText)
|
||||
}
|
||||
|
||||
override fun onSearchSubmitted(searchText: String) {
|
||||
update(searchText)
|
||||
}
|
||||
|
||||
private fun update(searchText: String) {
|
||||
updateJob?.cancel()
|
||||
updateJob = lifecycleScope.launch {
|
||||
if (prefs.searchProvider == "mapbox" && !isShortQuery(searchText)) {
|
||||
delay(500L)
|
||||
}
|
||||
try {
|
||||
loadNewList(searchText)
|
||||
} catch (e: IOException) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.autocomplete_connection_error,
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadNewList(query: String) {
|
||||
for (provider in providers) {
|
||||
try {
|
||||
recentResults.clear()
|
||||
currentProvider = provider
|
||||
|
||||
// first search in recent places
|
||||
val recentPlaces = if (query.isEmpty()) {
|
||||
recents.getAllAsync(provider.id, limit = maxItems)
|
||||
} else {
|
||||
recents.searchAsync(query, provider.id, limit = maxItems)
|
||||
}
|
||||
recentResults.addAll(recentPlaces)
|
||||
resultList =
|
||||
recentPlaces.map { it.asAutocompletePlace(LatLng.fromLocation(location)) }
|
||||
invalidate()
|
||||
|
||||
// if we already have enough results or the query is short, stop here
|
||||
if (isShortQuery(query) || recentResults.size >= maxItems) break
|
||||
|
||||
// then search online
|
||||
val recentIds = recentPlaces.map { it.id }
|
||||
resultList = withContext(Dispatchers.IO) {
|
||||
(resultList!! + provider.autocomplete(query, LatLng.fromLocation(location))
|
||||
.filter { !recentIds.contains(it.id) }).take(maxItems)
|
||||
}
|
||||
invalidate()
|
||||
break
|
||||
} catch (e: ApiUnavailableException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isShortQuery(query: CharSequence) = query.length < 3
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
session.requestLocationUpdates()
|
||||
session.mapScreen = this
|
||||
|
||||
if (supportsCarApiLevel3(carContext) && permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
it
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}) {
|
||||
|
||||
println("Setting up energy level listener")
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
this.energyLevel = energyLevel
|
||||
if (isUpdate) invalidate()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
session.mapScreen = null
|
||||
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getDetails(id: String): PlaceWithBounds {
|
||||
val provider = currentProvider!!
|
||||
val result = resultList!!.find { it.id == id }!!
|
||||
|
||||
val recentPlace = recentResults.find { it.id == id }
|
||||
if (recentPlace != null) return recentPlace.asPlaceWithBounds()
|
||||
|
||||
val details = provider.getDetails(id)
|
||||
|
||||
recents.insert(RecentAutocompletePlace(result, details, provider.id, Instant.now()))
|
||||
|
||||
return details
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,31 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
|
||||
val dataSourceValues = carContext.resources.getStringArray(R.array.pref_data_source_values)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.auto_settings))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_data_source))
|
||||
val dataSourceId = prefs.dataSource
|
||||
val dataSourceDesc = dataSourceNames[dataSourceValues.indexOf(dataSourceId)]
|
||||
addText(dataSourceDesc)
|
||||
setTitle(carContext.getString(R.string.settings_data_sources))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
@@ -40,7 +38,7 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(ChooseDataSourceScreen(carContext))
|
||||
screenManager.push(DataSettingsScreen(carContext))
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
@@ -81,31 +79,110 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChooseDataSourceScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
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 =
|
||||
carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_data_source))
|
||||
setTitle(carContext.getString(R.string.settings_data_sources))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
for (i in dataSourceNames.indices) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_data_source))
|
||||
setBrowsable(true)
|
||||
val dataSourceId = prefs.dataSource
|
||||
val dataSourceDesc = dataSourceNames[dataSourceValues.indexOf(dataSourceId)]
|
||||
addText(dataSourceDesc)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
R.string.pref_data_source,
|
||||
dataSourceNames,
|
||||
dataSourceValues,
|
||||
prefs.dataSource,
|
||||
dataSourceDescriptions
|
||||
) {
|
||||
prefs.dataSource = it
|
||||
})
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_search_provider))
|
||||
setBrowsable(true)
|
||||
val searchProviderId = prefs.searchProvider
|
||||
val searchProviderDesc =
|
||||
searchProviderNames[searchProviderValues.indexOf(searchProviderId)]
|
||||
addText(searchProviderDesc)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
R.string.pref_search_provider,
|
||||
searchProviderNames,
|
||||
searchProviderValues,
|
||||
prefs.searchProvider
|
||||
) {
|
||||
prefs.searchProvider = it
|
||||
})
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_search_delete_recent))
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
db.recentAutocompletePlaceDao().deleteAll()
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.deleted_recent_search_results,
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class ChooseDataSourceScreen(
|
||||
ctx: CarContext,
|
||||
@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) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(title))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
for (i in names.indices) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(dataSourceNames[i])
|
||||
addText(dataSourceDescriptions[i])
|
||||
setTitle(names[i])
|
||||
descriptions?.let { addText(it[i]) }
|
||||
}.build())
|
||||
}
|
||||
setOnSelectedListener {
|
||||
prefs.dataSource = dataSourceValues[it]
|
||||
callback(values[it])
|
||||
screenManager.pop()
|
||||
}
|
||||
setSelectedIndex(dataSourceValues.indexOf(prefs.dataSource))
|
||||
setSelectedIndex(values.indexOf(currentValue))
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
@@ -145,7 +222,10 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
R.plurals.chargeprice_some_tariffs_selected,
|
||||
n,
|
||||
n
|
||||
) + "\n" + carContext.getString(R.string.pref_my_tariffs_summary)
|
||||
) + "\n" + carContext.resources.getQuantityString(
|
||||
R.plurals.pref_my_tariffs_summary,
|
||||
n
|
||||
)
|
||||
}
|
||||
)
|
||||
}.build())
|
||||
@@ -210,7 +290,10 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceCar>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
private var api = ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
|
||||
private var api = ChargepriceApi.create(
|
||||
carContext.getString(R.string.chargeprice_key),
|
||||
carContext.getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
override val isMultiSelect = true
|
||||
override val shouldShowSelectAll = false
|
||||
|
||||
@@ -235,7 +318,10 @@ class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<Chargepric
|
||||
|
||||
class SelectTariffsScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceTariff>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
private var api = ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
|
||||
private var api = ChargepriceApi.create(
|
||||
carContext.getString(R.string.chargeprice_key),
|
||||
carContext.getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
override val isMultiSelect = true
|
||||
override val shouldShowSelectAll = true
|
||||
|
||||
@@ -369,6 +455,7 @@ class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
val nSpacers = when {
|
||||
maxItems % 3 == 0 -> 1
|
||||
maxItems == 100 -> 0 // AA has increased the limit to 100 and changed the way items are laid out
|
||||
maxItems % 4 == 0 -> 2
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@@ -3,31 +3,59 @@ package net.vonforst.evmap.auto
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.InputCallback
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.model.signin.InputSignInMethod
|
||||
import androidx.car.app.model.signin.SignInTemplate
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
class TextPromptScreen(
|
||||
ctx: CarContext,
|
||||
@StringRes val title: Int,
|
||||
@StringRes val prompt: Int,
|
||||
val initialValue: String? = null
|
||||
val initialValue: String? = null,
|
||||
val cancelable: Boolean = true
|
||||
) : Screen(ctx),
|
||||
InputCallback {
|
||||
private var inputText = ""
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val signInMethod = InputSignInMethod.Builder(this).apply {
|
||||
initialValue?.let { setDefaultValue(it) }
|
||||
initialValue?.let {
|
||||
setDefaultValue(it)
|
||||
inputText = initialValue
|
||||
}
|
||||
setShowKeyboardByDefault(true)
|
||||
}.build()
|
||||
return SignInTemplate.Builder(signInMethod).apply {
|
||||
setHeaderAction(Action.BACK)
|
||||
setInstructions(carContext.getString(prompt))
|
||||
setTitle(carContext.getString(title))
|
||||
if (cancelable) {
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.cancel))
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
screenManager.pop()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.ok))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
onInputSubmitted(inputText)
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onInputTextChanged(text: String) {
|
||||
inputText = text
|
||||
}
|
||||
|
||||
override fun onInputSubmitted(text: String) {
|
||||
setResult(text)
|
||||
screenManager.pop()
|
||||
|
||||
@@ -10,9 +10,8 @@ import androidx.car.app.hardware.info.*
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.CompassNeedle
|
||||
@@ -20,8 +19,10 @@ import net.vonforst.evmap.ui.Gauge
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), LifecycleObserver {
|
||||
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver {
|
||||
private val carInfo =
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
private val carSensors = carContext.patchedCarSensors
|
||||
private var model: Model? = null
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var speed: Speed? = null
|
||||
@@ -57,7 +58,8 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), LifecycleObserver {
|
||||
PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_vehicle_data_permission_needed,
|
||||
permissions
|
||||
permissions,
|
||||
finishApp = false
|
||||
)
|
||||
) {
|
||||
setupListeners()
|
||||
@@ -225,33 +227,39 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), LifecycleObserver {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
if (!permissionsGranted()) return
|
||||
|
||||
println("Setting up energy level listener")
|
||||
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
hardwareMan.carInfo.addSpeedListener(exec, ::onSpeedUpdated)
|
||||
hardwareMan.carSensors.addCompassListener(
|
||||
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
carInfo.addSpeedListener(exec, ::onSpeedUpdated)
|
||||
carSensors.addCompassListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
exec,
|
||||
::onCompassUpdated
|
||||
)
|
||||
|
||||
hardwareMan.carInfo.fetchModel(exec) {
|
||||
carInfo.fetchModel(exec) {
|
||||
this.model = it
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
hardwareMan.carInfo.removeSpeedListener(::onSpeedUpdated)
|
||||
hardwareMan.carSensors.removeCompassListener(::onCompassUpdated)
|
||||
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
carInfo.removeSpeedListener(::onSpeedUpdated)
|
||||
carSensors.removeCompassListener(::onCompassUpdated)
|
||||
}
|
||||
|
||||
private fun permissionsGranted(): Boolean =
|
||||
|
||||
13
app/src/google/res/drawable/ic_search_off.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5C16,5.91 13.09,3 9.5,3C6.08,3 3.28,5.64 3.03,9h2.02C5.3,6.75 7.18,5 9.5,5C11.99,5 14,7.01 14,9.5S11.99,14 9.5,14c-0.17,0 -0.33,-0.03 -0.5,-0.05v2.02C9.17,15.99 9.33,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57L14,14.71v0.79l5,4.99L20.49,19L15.5,14z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6.47,10.82l-2.47,2.47l-2.47,-2.47l-0.71,0.71l2.47,2.47l-2.47,2.47l0.71,0.71l2.47,-2.47l2.47,2.47l0.71,-0.71l-2.47,-2.47l2.47,-2.47z" />
|
||||
</vector>
|
||||
@@ -1,13 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
||||
<string name="auto_location_service">EVMap läuft unter Android Auto und nutzt dafür deinen Standort.</string>
|
||||
<string name="auto_no_chargers_found">Keine Ladestationen in der Nähe gefunden</string>
|
||||
@@ -19,6 +11,7 @@
|
||||
<string name="grant_on_phone">Auf Telefon zulassen</string>
|
||||
<string name="auto_chargers_closeby">In der Nähe</string>
|
||||
<string name="auto_favorites">Favoriten</string>
|
||||
<string name="auto_chargers_near_location">Nahe %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Störungsmeldung (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Weitere Aktualisierung nicht möglich. Bitte zurück gehen und neu starten.</string>
|
||||
<string name="auto_prices">Preise</string>
|
||||
37
app/src/google/res/values-fr/strings.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.
|
||||
\n
|
||||
\nGoogle prend 15% sur chaque don.</string>
|
||||
<string name="auto_location_service">EVMap fonctionne sur Android Auto et utilise votre position.</string>
|
||||
<string name="open_in_app">Ouvrir dans l\'application</string>
|
||||
<string name="opened_on_phone">Ouvert sur le téléphone</string>
|
||||
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
|
||||
<string name="grant_on_phone">Grant au téléphone</string>
|
||||
<string name="auto_prices">Prix</string>
|
||||
<string name="auto_vehicle_data">Données sur le véhicule</string>
|
||||
<string name="auto_range">Autonomie</string>
|
||||
<string name="auto_speed">Vitesse</string>
|
||||
<string name="welcome_android_auto">Prise en charge d’Android Auto</string>
|
||||
<string name="sounds_cool">ça a l\'air cool</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Aucun des véhicules sélectionnés dans l\'application ne correspond à ce véhicule (%1$s %2$s).</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Plusieurs véhicules sélectionnés dans l\'application correspondent à ce véhicule (%1$s %2$s).</string>
|
||||
<string name="selecting_all">tous les éléments sélectionnés</string>
|
||||
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap (Mapbox) pour les données cartographiques.</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap n\'a pas pu déterminer le modèle de votre véhicule.</string>
|
||||
<string name="auto_no_chargers_found">Aucun chargeur à proximité n\'a été trouvé</string>
|
||||
<string name="auto_no_favorites_found">Pas de favoris trouvés</string>
|
||||
<string name="auto_charging_level">Niveau de charge</string>
|
||||
<string name="auto_chargers_closeby">Chargeurs à proximité</string>
|
||||
<string name="auto_chargers_near_location">Près de %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Rapport d\'anomalie (%s)</string>
|
||||
<string name="auto_no_data">Indisponible</string>
|
||||
<string name="auto_settings">Paramètres</string>
|
||||
<string name="selecting_none">désélectionner tous les éléments</string>
|
||||
<string name="auto_vehicle_data_permission_needed">Pour cette fonction, EVMap doit avoir accès aux données de votre véhicule.</string>
|
||||
<string name="auto_heading">Direction</string>
|
||||
<string name="auto_favorites">Favoris</string>
|
||||
<string name="auto_no_refresh_possible">D\'autres mises à jour ne sont pas possibles. Veuillez revenir en arrière et redémarrer.</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Plage de charge pour la comparaison des prix</string>
|
||||
<string name="welcome_android_auto_detail">Vous pouvez également utiliser EVMap à partir d\'Android Auto sur les voitures prises en charge. Il suffit de sélectionner l\'application EVMap dans le menu Android Auto.</string>
|
||||
</resources>
|
||||
37
app/src/google/res/values-nb-rNO/strings.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende penger til utvikleren.
|
||||
\n
|
||||
\nGoogle tar 15% av alle donasjoner.</string>
|
||||
<string name="auto_favorites">Favoritter</string>
|
||||
<string name="auto_charging_level">Ladingsnivå</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap kunne ikke fastsette kjøretøymodellen.</string>
|
||||
<string name="selecting_none">fravalgte alle elementer</string>
|
||||
<string name="grant_on_phone">Innvilg på mobilenheten</string>
|
||||
<string name="auto_chargers_closeby">Ladere i nærheten</string>
|
||||
<string name="auto_prices">Pris</string>
|
||||
<string name="auto_no_chargers_found">Ingen ladere i nærheten</string>
|
||||
<string name="auto_no_favorites_found">Fant ikke noen favoritter</string>
|
||||
<string name="open_in_app">Åpne i programmet</string>
|
||||
<string name="auto_location_service">EVMap kjører på Android Auto og bruker posisjonen din.</string>
|
||||
<string name="auto_heading">Fartsretning</string>
|
||||
<string name="opened_on_phone">Åpnet på mobilenheten</string>
|
||||
<string name="auto_location_permission_needed">Innvilg posisjonstilgang for å bruke EVMap på Android Auto.</string>
|
||||
<string name="auto_chargers_near_location">Nær %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Feilrapport (%s)</string>
|
||||
<string name="auto_vehicle_data">Kjøretøydata</string>
|
||||
<string name="auto_no_data">Utilgjengelig</string>
|
||||
<string name="auto_speed">Hastighet</string>
|
||||
<string name="auto_settings">Innstillinger</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Ingen av kjøretøyene valgt i programmet samsvarer med dette kjøretøyet (%1$s %2$s).</string>
|
||||
<string name="welcome_android_auto">Android Auto-støtte</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Flere kjøretøy valgt i programmet samsvarer med dette kjøretøyet (%1$s %2$s).</string>
|
||||
<string name="auto_vehicle_data_permission_needed">EvMap trenger tilgang til kjøretøydata for å bruke denne funksjonen.</string>
|
||||
<string name="auto_no_refresh_possible">Videre oppdateringer er ikke mulig. Gå tilbake og start på ny.</string>
|
||||
<string name="auto_range">Rekkevidde</string>
|
||||
<string name="welcome_android_auto_detail">Du kan også bruke EVMap inne i Android Auto på bilder som støtter dette ved å velge det i Android Auto-menyen.</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Prissammenligning for laderekkevidde fordelt på pris</string>
|
||||
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap (Mapbox) for kartdata.</string>
|
||||
<string name="selecting_all">valgte alle elementene</string>
|
||||
<string name="sounds_cool">den er grei</string>
|
||||
</resources>
|
||||
19
app/src/google/res/values/arrays.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>@string/pref_provider_google_maps</item>
|
||||
<item>@string/pref_provider_osm_mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" translatable="false">
|
||||
<item>google</item>
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>@string/pref_provider_google_maps</item>
|
||||
<item>@string/pref_provider_osm_mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" translatable="false">
|
||||
<item>google</item>
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -3,5 +3,5 @@
|
||||
<color name="gauge_active">#00e676</color>
|
||||
<color name="gauge_middle">#087f23</color>
|
||||
<color name="gauge_inactive">#9e9e9e</color>
|
||||
<color name="charger_100kw_dark">#fdd835</color>
|
||||
<color name="charger_100kw_dark">#FBC02D</color>
|
||||
</resources>
|
||||
5
app/src/google/res/values/donottranslate.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pref_map_provider_default" translatable="false">google</string>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
</resources>
|
||||
@@ -1,23 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" tranlatable="false">
|
||||
<item>google</item>
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" tranlatable="false">
|
||||
<item>google</item>
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="pref_map_provider_default" translatable="false">google</string>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string>
|
||||
<string name="auto_location_service">EVMap is running on Android Auto and using your location.</string>
|
||||
<string name="auto_no_chargers_found">No nearby chargers found</string>
|
||||
@@ -29,6 +11,7 @@
|
||||
<string name="grant_on_phone">Grant on phone</string>
|
||||
<string name="auto_chargers_closeby">Nearby chargers</string>
|
||||
<string name="auto_favorites">Favorites</string>
|
||||
<string name="auto_chargers_near_location">Near %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Fault report (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Further updates not possible. Please go back and restart.</string>
|
||||
<string name="auto_prices">Pricing</string>
|
||||
@@ -44,7 +27,7 @@
|
||||
<string name="sounds_cool">sounds cool</string>
|
||||
<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">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%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="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>
|
||||
5
app/src/googleAutomotive/res/values-fr/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Autoriser</string>
|
||||
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
|
||||
</resources>
|
||||
5
app/src/googleAutomotive/res/values-nb-rNO/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="auto_location_permission_needed">Du må du innvilge posisjonstilgang for å kjøre EVMap i bilen din.</string>
|
||||
<string name="grant_on_phone">Tillat</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="auto_location_permission_needed">To run EVMap on your car, you need to grant access to your location.</string>
|
||||
<string name="grant_on_phone">Allow</string>
|
||||
<string name="auto_location_permission_needed">To run EVMap on your car, you need to grant access to your location.</string>
|
||||
</resources>
|
||||
@@ -24,7 +24,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
android:localeConfig="@xml/locales_config">
|
||||
|
||||
<meta-data
|
||||
android:name="com.mapbox.ACCESS_TOKEN"
|
||||
@@ -262,16 +263,14 @@
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<!-- Override services of the com.mapzen.android.lost library with exported:false
|
||||
until https://github.com/lostzen/lost/pull/270 is merged -->
|
||||
<service
|
||||
android:name="com.mapzen.android.lost.internal.GeofencingIntentService"
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.mapzen.lost.action.ACTION_GEOFENCING_SERVICE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap
|
||||
import android.app.Application
|
||||
import com.facebook.stetho.Stetho
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.limiter
|
||||
@@ -13,7 +14,16 @@ import org.acra.ktx.initAcra
|
||||
class EvMapApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
updateNightMode(PreferenceDataSource(this))
|
||||
val prefs = PreferenceDataSource(this)
|
||||
updateNightMode(prefs)
|
||||
|
||||
// Convert to new AppCompat storage for app language
|
||||
val lang = prefs.language
|
||||
if (lang != null && lang !in listOf("", "default")) {
|
||||
updateAppLocale(lang)
|
||||
prefs.language = null
|
||||
}
|
||||
|
||||
Stetho.initializeWithDefaults(this);
|
||||
init(applicationContext)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package net.vonforst.evmap
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -32,11 +31,9 @@ import net.vonforst.evmap.fragment.MapFragmentArgs
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.navigation.NavHostFragment
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
import net.vonforst.evmap.utils.getLocationFromIntent
|
||||
|
||||
|
||||
const val REQUEST_LOCATION_PERMISSION = 1
|
||||
const val EXTRA_CHARGER_ID = "chargerId"
|
||||
const val EXTRA_LAT = "lat"
|
||||
const val EXTRA_LON = "lon"
|
||||
@@ -54,14 +51,6 @@ class MapsActivity : AppCompatActivity(),
|
||||
var fragmentCallback: FragmentCallback? = null
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
return super.attachBaseContext(
|
||||
LocaleContextWrapper.wrap(
|
||||
newBase, PreferenceDataSource(newBase).language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val splashScreen = installSplashScreen()
|
||||
@@ -84,9 +73,9 @@ class MapsActivity : AppCompatActivity(),
|
||||
val navView = findViewById<NavigationView>(R.id.nav_view)
|
||||
navView.setupWithNavController(navController)
|
||||
|
||||
val header = navView.getHeaderView(0)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(header) { v, insets ->
|
||||
v.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(navView) { v, insets ->
|
||||
val header = navView.getHeaderView(0)
|
||||
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
insets
|
||||
}
|
||||
|
||||
@@ -163,6 +152,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.favs)
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
@@ -85,25 +80,6 @@ fun max(a: Int?, b: Int?): Int? {
|
||||
|
||||
fun <T> List<T>.containsAny(vararg values: T) = values.any { this.contains(it) }
|
||||
|
||||
public suspend fun <T> LiveData<T>.await(): T {
|
||||
return withContext(Dispatchers.Main.immediate) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T) {
|
||||
removeObserver(this)
|
||||
continuation.resume(value, null)
|
||||
}
|
||||
}
|
||||
|
||||
observeForever(observer)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.isDarkMode() =
|
||||
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
getCheckedItem()?.let { onCheckedItemChangedListener?.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
)
|
||||
}
|
||||
}
|
||||
is BooleanFilterValue -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ interface ChargepointApi<out T : ReferenceData> {
|
||||
|
||||
fun getFilters(referenceData: ReferenceData, sp: StringProvider): List<Filter<FilterValue>>
|
||||
|
||||
fun getName(): String
|
||||
val name: String
|
||||
val id: String
|
||||
}
|
||||
|
||||
interface StringProvider {
|
||||
|
||||
@@ -139,7 +139,7 @@ data class ChargeLocationStatus(
|
||||
(connectors == null || connectors.map {
|
||||
equivalentPlugTypes(it)
|
||||
}.any { equivalent -> it.type in equivalent })
|
||||
&& (minPower == null || (it.power != null && it.power > minPower))
|
||||
&& (minPower == null || (it.power != null && it.power >= minPower))
|
||||
}
|
||||
return this.copy(status = statusFiltered)
|
||||
}
|
||||
@@ -191,4 +191,4 @@ suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationSta
|
||||
}
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package net.vonforst.evmap.api.chargeprice
|
||||
import android.content.Context
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import moe.banana.jsonapi2.ArrayDocument
|
||||
import moe.banana.jsonapi2.JsonApiConverterFactory
|
||||
import moe.banana.jsonapi2.ResourceAdapterFactory
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import jsonapi.Document
|
||||
import jsonapi.JsonApiFactory
|
||||
import jsonapi.retrofit.DocumentConverterFactory
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
@@ -20,34 +22,45 @@ import java.util.*
|
||||
interface ChargepriceApi {
|
||||
@POST("charge_prices")
|
||||
suspend fun getChargePrices(
|
||||
@Body request: ChargepriceRequest,
|
||||
@Body @jsonapi.retrofit.Document request: ChargepriceRequest,
|
||||
@Header("Accept-Language") language: String
|
||||
): ArrayDocument<ChargePrice>
|
||||
): Document<List<ChargePrice>>
|
||||
|
||||
@GET("vehicles")
|
||||
suspend fun getVehicles(): ArrayDocument<ChargepriceCar>
|
||||
@jsonapi.retrofit.Document
|
||||
suspend fun getVehicles(): List<ChargepriceCar>
|
||||
|
||||
@GET("tariffs")
|
||||
suspend fun getTariffs(): ArrayDocument<ChargepriceTariff>
|
||||
@jsonapi.retrofit.Document
|
||||
suspend fun getTariffs(): List<ChargepriceTariff>
|
||||
|
||||
@POST("user_feedback")
|
||||
suspend fun userFeedback(@Body @jsonapi.retrofit.Document feedback: ChargepriceUserFeedback)
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 1L * 1024 * 1024 // 1MB
|
||||
val supportedLanguages = setOf("de", "en", "fr", "nl")
|
||||
|
||||
val DATA_SOURCE_GOINGELECTRIC = "going_electric"
|
||||
val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
|
||||
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
|
||||
private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
|
||||
|
||||
private val jsonApiAdapterFactory = ResourceAdapterFactory.builder()
|
||||
.add(ChargepriceRequest::class.java)
|
||||
.add(ChargepriceTariff::class.java)
|
||||
.add(ChargepriceBrand::class.java)
|
||||
.add(ChargePrice::class.java)
|
||||
.add(ChargepriceCar::class.java)
|
||||
private val jsonApiAdapterFactory = JsonApiFactory.Builder()
|
||||
.addType(ChargepriceRequest::class.java)
|
||||
.addType(ChargepriceTariff::class.java)
|
||||
.addType(ChargepriceBrand::class.java)
|
||||
.addType(ChargePrice::class.java)
|
||||
.addType(ChargepriceCar::class.java)
|
||||
.build()
|
||||
val moshi = Moshi.Builder()
|
||||
.add(jsonApiAdapterFactory)
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ChargepriceUserFeedback::class.java, "type")
|
||||
.withSubtype(ChargepriceMissingPriceFeedback::class.java, "missing_price")
|
||||
.withSubtype(ChargepriceWrongPriceFeedback::class.java, "wrong_price")
|
||||
.withSubtype(ChargepriceMissingVehicleFeedback::class.java, "missing_vehicle")
|
||||
)
|
||||
.build()
|
||||
|
||||
fun create(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.chargeprice.app/v1/",
|
||||
@@ -73,7 +86,8 @@ interface ChargepriceApi {
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseurl)
|
||||
.addConverterFactory(JsonApiConverterFactory.create(moshi))
|
||||
.addConverterFactory(DocumentConverterFactory.create())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(ChargepriceApi::class.java)
|
||||
@@ -89,6 +103,15 @@ interface ChargepriceApi {
|
||||
}
|
||||
}
|
||||
|
||||
fun getPoiUrl(charger: ChargeLocation) =
|
||||
"https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter(charger)}"
|
||||
|
||||
fun getDataAdapter(charger: ChargeLocation) = when (charger.dataSource) {
|
||||
"goingelectric" -> DATA_SOURCE_GOINGELECTRIC
|
||||
"openchargemap" -> DATA_SOURCE_OPENCHARGEMAP
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
|
||||
// list of countries updated 2021/08/24
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package net.vonforst.evmap.api.chargeprice
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.util.Patterns
|
||||
import com.squareup.moshi.Json
|
||||
import moe.banana.jsonapi2.HasMany
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
|
||||
import moe.banana.jsonapi2.JsonApi
|
||||
import moe.banana.jsonapi2.Resource
|
||||
import com.squareup.moshi.JsonClass
|
||||
import jsonapi.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
@@ -17,16 +17,21 @@ import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
|
||||
@JsonApi(type = "charge_price_request")
|
||||
class ChargepriceRequest : Resource() {
|
||||
@field:Json(name = "data_adapter")
|
||||
lateinit var dataAdapter: String
|
||||
lateinit var station: ChargepriceStation
|
||||
lateinit var options: ChargepriceOptions
|
||||
var tariffs: HasMany<ChargepriceTariff>? = null
|
||||
var vehicle: HasOne<ChargepriceCar>? = null
|
||||
}
|
||||
@Resource("charge_price_request")
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceRequest(
|
||||
@Json(name = "data_adapter")
|
||||
val dataAdapter: String,
|
||||
val station: ChargepriceStation,
|
||||
val options: ChargepriceOptions,
|
||||
@ToMany("tariffs")
|
||||
val tariffs: List<ChargepriceTariff>? = null,
|
||||
@ToOne("vehicle")
|
||||
val vehicle: ChargepriceCar? = null,
|
||||
@RelationshipsObject var relationships: Relationships? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceStation(
|
||||
val longitude: Double,
|
||||
val latitude: Double,
|
||||
@@ -56,11 +61,13 @@ data class ChargepriceStation(
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceChargepoint(
|
||||
val power: Double,
|
||||
val plug: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceOptions(
|
||||
@Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null,
|
||||
val energy: Double? = null,
|
||||
@@ -73,142 +80,107 @@ data class ChargepriceOptions(
|
||||
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null
|
||||
)
|
||||
|
||||
@JsonApi(type = "tariff")
|
||||
class ChargepriceTariff() : Resource() {
|
||||
lateinit var provider: String
|
||||
lateinit var name: String
|
||||
@field:Json(name = "direct_payment")
|
||||
var directPayment: Boolean = false
|
||||
@field:Json(name = "provider_customer_tariff")
|
||||
var providerCustomerTariff: Boolean = false
|
||||
@field:Json(name = "supported_cuntries")
|
||||
lateinit var supportedCountries: Set<String>
|
||||
@field:Json(name = "charge_card_id")
|
||||
lateinit var chargeCardId: String // GE charge card ID
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as ChargepriceTariff
|
||||
|
||||
if (provider != other.provider) return false
|
||||
if (name != other.name) return false
|
||||
if (directPayment != other.directPayment) return false
|
||||
if (providerCustomerTariff != other.providerCustomerTariff) return false
|
||||
if (supportedCountries != other.supportedCountries) return false
|
||||
if (chargeCardId != other.chargeCardId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + provider.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + directPayment.hashCode()
|
||||
result = 31 * result + providerCustomerTariff.hashCode()
|
||||
result = 31 * result + supportedCountries.hashCode()
|
||||
result = 31 * result + chargeCardId.hashCode()
|
||||
return result
|
||||
}
|
||||
@Resource("tariff")
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceTariff(
|
||||
@Id val id_: String?,
|
||||
val provider: String,
|
||||
val name: String,
|
||||
@Json(name = "direct_payment")
|
||||
val directPayment: Boolean = false,
|
||||
@Json(name = "provider_customer_tariff")
|
||||
val providerCustomerTariff: Boolean = false,
|
||||
@Json(name = "supported_countries")
|
||||
val supportedCountries: Set<String>,
|
||||
@Json(name = "charge_card_id")
|
||||
val chargeCardId: String?, // GE charge card ID
|
||||
) : Parcelable {
|
||||
val id: String
|
||||
get() = id_!!
|
||||
}
|
||||
|
||||
@JsonApi(type = "car")
|
||||
class ChargepriceCar : Resource(), Equatable {
|
||||
lateinit var name: String
|
||||
lateinit var brand: String
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource("car")
|
||||
@Parcelize
|
||||
data class ChargepriceCar(
|
||||
@Id val id_: String?,
|
||||
val name: String,
|
||||
val brand: String,
|
||||
|
||||
@field:Json(name = "dc_charge_ports")
|
||||
lateinit var dcChargePorts: List<String>
|
||||
lateinit var manufacturer: HasOne<ChargepriceBrand>
|
||||
@Json(name = "dc_charge_ports")
|
||||
val dcChargePorts: List<String>
|
||||
) : Equatable, Parcelable {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as ChargepriceCar
|
||||
|
||||
if (name != other.name) return false
|
||||
if (brand != other.brand) return false
|
||||
if (dcChargePorts != other.dcChargePorts) return false
|
||||
if (manufacturer != other.manufacturer) return false
|
||||
|
||||
return true
|
||||
companion object {
|
||||
private val acConnectors = listOf(
|
||||
Chargepoint.CEE_BLAU,
|
||||
Chargepoint.CEE_ROT,
|
||||
Chargepoint.SCHUKO,
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG
|
||||
)
|
||||
private val plugMapping = mapOf(
|
||||
"ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"tesla_suc" to Chargepoint.SUPERCHARGER,
|
||||
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"chademo" to Chargepoint.CHADEMO
|
||||
)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + brand.hashCode()
|
||||
result = 31 * result + dcChargePorts.hashCode()
|
||||
result = 31 * result + manufacturer.hashCode()
|
||||
return result
|
||||
}
|
||||
val id: String
|
||||
get() = id_!!
|
||||
|
||||
private val acConnectors = listOf(
|
||||
Chargepoint.CEE_BLAU,
|
||||
Chargepoint.CEE_ROT,
|
||||
Chargepoint.SCHUKO,
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG
|
||||
)
|
||||
private val plugMapping = mapOf(
|
||||
"ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"tesla_suc" to Chargepoint.SUPERCHARGER,
|
||||
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"chademo" to Chargepoint.CHADEMO
|
||||
)
|
||||
val compatibleEvmapConnectors: List<String>
|
||||
get() = dcChargePorts.map {
|
||||
plugMapping[it]
|
||||
}.filterNotNull().plus(acConnectors)
|
||||
}
|
||||
|
||||
@JsonApi(type = "brand")
|
||||
class ChargepriceBrand : Resource()
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource("brand")
|
||||
@Parcelize
|
||||
data class ChargepriceBrand(
|
||||
@Id val id: String?
|
||||
) : Parcelable
|
||||
|
||||
@JsonApi(type = "charge_price")
|
||||
class ChargePrice : Resource(), Equatable, Cloneable {
|
||||
lateinit var provider: String
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource("charge_price")
|
||||
@Parcelize
|
||||
data class ChargePrice(
|
||||
val provider: String,
|
||||
@Json(name = "tariff_name")
|
||||
val tariffName: String,
|
||||
val url: String,
|
||||
@Json(name = "monthly_min_sales")
|
||||
val monthlyMinSales: Double = 0.0,
|
||||
@Json(name = "total_monthly_fee")
|
||||
val totalMonthlyFee: Double = 0.0,
|
||||
@Json(name = "flat_rate")
|
||||
val flatRate: Boolean = false,
|
||||
|
||||
@field:Json(name = "tariff_name")
|
||||
lateinit var tariffName: String
|
||||
lateinit var url: String
|
||||
@Json(name = "direct_payment")
|
||||
val directPayment: Boolean = false,
|
||||
|
||||
@field:Json(name = "monthly_min_sales")
|
||||
var monthlyMinSales: Double = 0.0
|
||||
@Json(name = "provider_customer_tariff")
|
||||
val providerCustomerTariff: Boolean = false,
|
||||
val currency: String,
|
||||
|
||||
@field:Json(name = "total_monthly_fee")
|
||||
var totalMonthlyFee: Double = 0.0
|
||||
@Json(name = "start_time")
|
||||
val startTime: Int = 0,
|
||||
val tags: List<ChargepriceTag>,
|
||||
|
||||
@field:Json(name = "flat_rate")
|
||||
var flatRate: Boolean = false
|
||||
|
||||
@field:Json(name = "direct_payment")
|
||||
var directPayment: Boolean = false
|
||||
|
||||
@field:Json(name = "provider_customer_tariff")
|
||||
var providerCustomerTariff: Boolean = false
|
||||
lateinit var currency: String
|
||||
|
||||
@field:Json(name = "start_time")
|
||||
var startTime: Int = 0
|
||||
lateinit var tags: List<ChargepriceTag>
|
||||
|
||||
@field:Json(name = "charge_point_prices")
|
||||
lateinit var chargepointPrices: List<ChargepointPrice>
|
||||
|
||||
@field:Json(name = "branding")
|
||||
var branding: ChargepriceBranding? = null
|
||||
|
||||
var tariff: HasOne<ChargepriceTariff>? = null
|
||||
@Json(name = "charge_point_prices")
|
||||
val chargepointPrices: List<ChargepointPrice>,
|
||||
|
||||
@Json(name = "branding")
|
||||
val branding: ChargepriceBranding? = null,
|
||||
|
||||
@ToOne("tariff")
|
||||
val tariffId: String?
|
||||
) : Equatable, Cloneable, Parcelable {
|
||||
fun formatMonthlyFees(ctx: Context): String {
|
||||
return listOfNotNull(
|
||||
if (totalMonthlyFee > 0) {
|
||||
@@ -219,69 +191,10 @@ class ChargePrice : Resource(), Equatable, Cloneable {
|
||||
} else null
|
||||
).joinToString(", ")
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as ChargePrice
|
||||
|
||||
if (provider != other.provider) return false
|
||||
if (tariffName != other.tariffName) return false
|
||||
if (url != other.url) return false
|
||||
if (monthlyMinSales != other.monthlyMinSales) return false
|
||||
if (totalMonthlyFee != other.totalMonthlyFee) return false
|
||||
if (flatRate != other.flatRate) return false
|
||||
if (directPayment != other.directPayment) return false
|
||||
if (providerCustomerTariff != other.providerCustomerTariff) return false
|
||||
if (currency != other.currency) return false
|
||||
if (startTime != other.startTime) return false
|
||||
if (tags != other.tags) return false
|
||||
if (chargepointPrices != other.chargepointPrices) return false
|
||||
if (branding != other.branding) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + provider.hashCode()
|
||||
result = 31 * result + tariffName.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + monthlyMinSales.hashCode()
|
||||
result = 31 * result + totalMonthlyFee.hashCode()
|
||||
result = 31 * result + flatRate.hashCode()
|
||||
result = 31 * result + directPayment.hashCode()
|
||||
result = 31 * result + providerCustomerTariff.hashCode()
|
||||
result = 31 * result + currency.hashCode()
|
||||
result = 31 * result + startTime
|
||||
result = 31 * result + tags.hashCode()
|
||||
result = 31 * result + chargepointPrices.hashCode()
|
||||
result = 31 * result + branding.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
public override fun clone(): ChargePrice {
|
||||
return ChargePrice().apply {
|
||||
chargepointPrices = this@ChargePrice.chargepointPrices
|
||||
currency = this@ChargePrice.currency
|
||||
directPayment = this@ChargePrice.directPayment
|
||||
flatRate = this@ChargePrice.flatRate
|
||||
monthlyMinSales = this@ChargePrice.monthlyMinSales
|
||||
provider = this@ChargePrice.provider
|
||||
providerCustomerTariff = this@ChargePrice.providerCustomerTariff
|
||||
startTime = this@ChargePrice.startTime
|
||||
tags = this@ChargePrice.tags
|
||||
tariffName = this@ChargePrice.tariffName
|
||||
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
|
||||
url = this@ChargePrice.url
|
||||
tariff = this@ChargePrice.tariff
|
||||
branding = this@ChargePrice.branding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepointPrice(
|
||||
val power: Double,
|
||||
val plug: String,
|
||||
@@ -289,7 +202,7 @@ data class ChargepointPrice(
|
||||
@Json(name = "price_distribution") val priceDistribution: PriceDistribution,
|
||||
@Json(name = "blocking_fee_start") val blockingFeeStart: Int?,
|
||||
@Json(name = "no_price_reason") var noPriceReason: String?
|
||||
) {
|
||||
) : Parcelable {
|
||||
fun formatDistribution(ctx: Context): String {
|
||||
fun percent(value: Double): String {
|
||||
return ctx.getString(R.string.percent_format, value * 100) + "\u00a0"
|
||||
@@ -332,19 +245,28 @@ data class ChargepointPrice(
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepriceBranding(
|
||||
@Json(name = "background_color") val backgroundColor: String,
|
||||
@Json(name = "text_color") val textColor: String,
|
||||
@Json(name = "logo_url") val logoUrl: String
|
||||
)
|
||||
) : Parcelable
|
||||
|
||||
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) {
|
||||
val isOnlyKwh =
|
||||
kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) :
|
||||
Parcelable {
|
||||
val isOnlyKwh
|
||||
get() = kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
|
||||
}
|
||||
|
||||
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable,
|
||||
Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceMeta(
|
||||
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
|
||||
)
|
||||
@@ -358,13 +280,97 @@ enum class ChargepriceInclude {
|
||||
EXCLUSIVE
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepriceRequestTariffMeta(
|
||||
val include: ChargepriceInclude
|
||||
)
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceChargepointMeta(
|
||||
val power: Double,
|
||||
val plug: String,
|
||||
val energy: Double,
|
||||
val duration: Double
|
||||
)
|
||||
)
|
||||
|
||||
@Resource("user_feedback")
|
||||
sealed class ChargepriceUserFeedback(
|
||||
val notes: String,
|
||||
val email: String,
|
||||
val context: String,
|
||||
val language: String
|
||||
) {
|
||||
init {
|
||||
if (email.isBlank() || email.length > 100 || !Patterns.EMAIL_ADDRESS.matcher(email)
|
||||
.matches()
|
||||
) {
|
||||
throw IllegalArgumentException("invalid email")
|
||||
}
|
||||
if (!ChargepriceApi.supportedLanguages.contains(language)) {
|
||||
throw IllegalArgumentException("invalid language")
|
||||
}
|
||||
if (context.length > 500) throw IllegalArgumentException("invalid context")
|
||||
if (notes.length > 1000) throw IllegalArgumentException("invalid notes")
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource(type = "missing_price")
|
||||
class ChargepriceMissingPriceFeedback(
|
||||
val tariff: String,
|
||||
val cpo: String,
|
||||
val price: String,
|
||||
@Json(name = "poi_link") val poiLink: String,
|
||||
notes: String,
|
||||
email: String,
|
||||
context: String,
|
||||
language: String
|
||||
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||
init {
|
||||
if (tariff.isBlank() || tariff.length > 100) throw IllegalArgumentException("invalid tariff")
|
||||
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
|
||||
if (price.isBlank() || price.length > 100) throw IllegalArgumentException("invalid price")
|
||||
if (poiLink.isBlank() || poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource(type = "wrong_price")
|
||||
class ChargepriceWrongPriceFeedback(
|
||||
val tariff: String,
|
||||
val cpo: String,
|
||||
@Json(name = "displayed_price") val displayedPrice: String,
|
||||
@Json(name = "actual_price") val actualPrice: String,
|
||||
@Json(name = "poi_link") val poiLink: String,
|
||||
notes: String,
|
||||
email: String,
|
||||
context: String,
|
||||
language: String,
|
||||
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||
init {
|
||||
if (tariff.length > 100) throw IllegalArgumentException("invalid tariff")
|
||||
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
|
||||
if (displayedPrice.length > 100) throw IllegalArgumentException("invalid displayedPrice")
|
||||
if (actualPrice.length > 100) throw IllegalArgumentException("invalid actualPrice")
|
||||
if (poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource(type = "missing_vehicle")
|
||||
class ChargepriceMissingVehicleFeedback(
|
||||
val brand: String,
|
||||
val model: String,
|
||||
notes: String,
|
||||
email: String,
|
||||
context: String,
|
||||
language: String,
|
||||
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||
init {
|
||||
if (brand.length > 100) throw IllegalArgumentException("invalid brand")
|
||||
if (model.length > 100) throw IllegalArgumentException("invalid model")
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,8 @@ class GoingElectricApiWrapper(
|
||||
private val clusterThreshold = 11f
|
||||
val api = GoingElectricApi.create(apikey, baseurl, context)
|
||||
|
||||
override fun getName() = "GoingElectric.de"
|
||||
override val name = "GoingElectric.de"
|
||||
override val id = "going_electric"
|
||||
|
||||
override suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
@@ -467,17 +468,17 @@ class GoingElectricApiWrapper(
|
||||
sp.getString(R.string.filter_networks), "networks",
|
||||
networkMap, manyChoices = true
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.categories), "categories",
|
||||
categoryMap,
|
||||
manyChoices = true
|
||||
),
|
||||
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults"),
|
||||
BooleanFilter(sp.getString(R.string.filter_barrierfree), "barrierfree"),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_chargecards), "chargecards",
|
||||
chargecardMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults")
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.categories), "categories",
|
||||
categoryMap,
|
||||
manyChoices = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ class OpenChargeMapApiWrapper(
|
||||
private val clusterThreshold = 11
|
||||
val api = OpenChargeMapApi.create(apikey, baseurl, context)
|
||||
|
||||
override fun getName() = "OpenChargeMap.org"
|
||||
override val name = "OpenChargeMap.org"
|
||||
override val id = "open_charge_map"
|
||||
|
||||
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
||||
if (value == null || value.all) null else value.values.joinToString(",")
|
||||
|
||||
@@ -2,7 +2,10 @@ package net.vonforst.evmap.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -13,6 +16,7 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
@@ -20,13 +24,15 @@ 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.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.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
|
||||
import java.text.NumberFormat
|
||||
|
||||
class ChargepriceFragment : Fragment() {
|
||||
@@ -34,10 +40,12 @@ class ChargepriceFragment : Fragment() {
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
|
||||
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
savedStateViewModelFactory { state ->
|
||||
ChargepriceViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key)
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url),
|
||||
state
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -45,6 +53,33 @@ class ChargepriceFragment : Fragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedElementEnterTransition = MaterialContainerTransform()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
prefs.chargepriceCounter += 1
|
||||
if ((prefs.chargepriceCounter - 30).mod(50) == 0) {
|
||||
showDonationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
vm.reloadPrefs()
|
||||
}
|
||||
|
||||
private fun showDonationDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.chargeprice_donation_dialog_title)
|
||||
.setMessage(R.string.chargeprice_donation_dialog_detail)
|
||||
.setNegativeButton(R.string.ok) { di, _ ->
|
||||
di.cancel()
|
||||
}
|
||||
.setPositiveButton(R.string.donate) { di, _ ->
|
||||
di.dismiss()
|
||||
findNavController().navigate(R.id.action_chargeprice_to_donateFragment)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -76,9 +111,7 @@ class ChargepriceFragment : Fragment() {
|
||||
|
||||
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
|
||||
val charger = fragmentArgs.charger
|
||||
val dataSource = fragmentArgs.dataSource
|
||||
vm.charger.value = charger
|
||||
vm.dataSource.value = dataSource
|
||||
if (vm.chargepoint.value == null) {
|
||||
vm.chargepoint.value = charger.chargepointsMerged.get(0)
|
||||
}
|
||||
@@ -143,11 +176,11 @@ class ChargepriceFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.imgChargepriceLogo.setOnClickListener {
|
||||
(requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${dataSource}")
|
||||
(requireActivity() as MapsActivity).openUrl(ChargepriceApi.getPoiUrl(charger))
|
||||
}
|
||||
|
||||
binding.btnSettings.setOnClickListener {
|
||||
findNavController().navigate(R.id.action_chargeprice_to_settingsFragment)
|
||||
findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment)
|
||||
}
|
||||
|
||||
binding.batteryRange.setLabelFormatter { value: Float ->
|
||||
|
||||
@@ -4,13 +4,12 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import net.vonforst.evmap.databinding.DialogDataSourceSelectBinding
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import java.util.*
|
||||
import net.vonforst.evmap.ui.MaterialDialogFragment
|
||||
|
||||
class DataSourceSelectDialog : AppCompatDialogFragment() {
|
||||
class DataSourceSelectDialog : MaterialDialogFragment() {
|
||||
private lateinit var binding: DialogDataSourceSelectBinding
|
||||
var okListener: ((String) -> Unit)? = null
|
||||
|
||||
@@ -28,7 +27,7 @@ class DataSourceSelectDialog : AppCompatDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
override fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
@@ -41,16 +40,12 @@ class DataSourceSelectDialog : AppCompatDialogFragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// dialog with 95% screen height
|
||||
dialog?.window?.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
(resources.displayMetrics.heightPixels * 0.95).toInt()
|
||||
)
|
||||
setFullSize()
|
||||
}
|
||||
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
override fun initView(view: View, savedInstanceState: Bundle?) {
|
||||
val args = requireArguments()
|
||||
binding.btnCancel.visibility =
|
||||
if (args.getBoolean("cancel_enabled")) View.VISIBLE else View.GONE
|
||||
|
||||
@@ -20,23 +20,23 @@ import com.car2go.maps.model.LatLng
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
import net.vonforst.evmap.adapter.FavoritesAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
|
||||
import net.vonforst.evmap.databinding.ItemFavoriteBinding
|
||||
import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.model.Favorite
|
||||
import net.vonforst.evmap.model.FavoriteWithDetail
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
class FavoritesFragment : Fragment() {
|
||||
private lateinit var binding: FragmentFavoritesBinding
|
||||
private var locationClient: LostApiClient? = null
|
||||
private lateinit var locationEngine: LocationEngine
|
||||
private var toDelete: Favorite? = null
|
||||
private var deleteSnackbar: Snackbar? = null
|
||||
private lateinit var adapter: FavoritesAdapter
|
||||
@@ -52,8 +52,8 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
locationClient = LostApiClient.Builder(requireContext())
|
||||
.addConnectionCallbacks(this).build()
|
||||
|
||||
locationEngine = FusionEngine(requireContext())
|
||||
|
||||
enterTransition = MaterialFadeThrough()
|
||||
exitTransition = MaterialFadeThrough()
|
||||
@@ -109,8 +109,6 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
}
|
||||
createTouchHelper().attachToRecyclerView(binding.favsList)
|
||||
|
||||
locationClient!!.connect()
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
vm.reloadAvailability() {
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
@@ -118,27 +116,17 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
val context = this.context ?: return
|
||||
if (context.checkAnyLocationPermission()) {
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient!!)
|
||||
if (location != null) {
|
||||
vm.location.value = LatLng(location.latitude, location.longitude)
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
if (requireContext().checkAnyLocationPermission()) {
|
||||
val location = locationEngine.getLastKnownLocation()
|
||||
location?.let {
|
||||
vm.location.value = LatLng(it.latitude, it.longitude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionSuspended() {
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
locationClient?.let {
|
||||
if (it.isConnected) it.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(fav: FavoriteWithDetail) {
|
||||
val position =
|
||||
vm.listData.value?.indexOfFirst { it.fav.favorite.favoriteId == fav.favorite.favoriteId }
|
||||
|
||||
@@ -3,9 +3,11 @@ package net.vonforst.evmap.fragment
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
@@ -22,7 +24,7 @@ import net.vonforst.evmap.ui.showEditTextDialog
|
||||
import net.vonforst.evmap.viewmodel.FilterViewModel
|
||||
|
||||
|
||||
class FilterFragment : Fragment() {
|
||||
class FilterFragment : Fragment(), MenuProvider {
|
||||
private lateinit var binding: FragmentFilterBinding
|
||||
private val vm: FilterViewModel by viewModels()
|
||||
|
||||
@@ -40,9 +42,6 @@ class FilterFragment : Fragment() {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
vm.filterProfile.observe(viewLifecycleOwner) {}
|
||||
|
||||
return binding.root
|
||||
@@ -50,6 +49,7 @@ class FilterFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
@@ -81,12 +81,11 @@ class FilterFragment : Fragment() {
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.filter, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_apply -> {
|
||||
lifecycleScope.launch {
|
||||
@@ -99,7 +98,7 @@ class FilterFragment : Fragment() {
|
||||
saveProfile()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.os.Bundle
|
||||
import android.text.method.KeyListener
|
||||
import android.view.*
|
||||
@@ -18,15 +16,12 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.view.*
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -54,6 +49,7 @@ import com.car2go.maps.model.BitmapDescriptor
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.Marker
|
||||
import com.car2go.maps.model.MarkerOptions
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
@@ -63,26 +59,26 @@ 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.mapzen.android.lost.api.LocationListener
|
||||
import com.mapzen.android.lost.api.LocationRequest
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
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.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.location.Priority
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.*
|
||||
@@ -98,14 +94,13 @@ import kotlin.collections.contains
|
||||
import kotlin.collections.set
|
||||
|
||||
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
|
||||
LostApiClient.ConnectionCallbacks, LocationListener {
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback, MenuProvider {
|
||||
private lateinit var binding: FragmentMapBinding
|
||||
private val vm: MapViewModel by viewModels()
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
private var mapFragment: MapFragment? = null
|
||||
private var map: AnyMap? = null
|
||||
private lateinit var locationClient: LostApiClient
|
||||
private lateinit var locationEngine: LocationEngine
|
||||
private var requestingLocationUpdates = false
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
|
||||
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
|
||||
@@ -116,6 +111,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var searchResultIcon: BitmapDescriptor? = null
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
private var previousChargepointIds: Set<Long>? = null
|
||||
private var mapTopPadding: Int = 0
|
||||
|
||||
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
@@ -149,10 +145,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
locationClient = LostApiClient.Builder(requireContext())
|
||||
.addConnectionCallbacks(this)
|
||||
.build()
|
||||
locationClient.connect()
|
||||
locationEngine = FusionEngine(requireContext())
|
||||
clusterIconGenerator = ClusterIconGenerator(requireContext())
|
||||
|
||||
enterTransition = MaterialFadeThrough()
|
||||
@@ -173,7 +166,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val provider = prefs.mapProvider
|
||||
if (mapFragment == null) {
|
||||
mapFragment =
|
||||
requireActivity().supportFragmentManager.findFragmentByTag(mapFragmentTag) as MapFragment?
|
||||
childFragmentManager.findFragmentByTag(mapFragmentTag) as MapFragment?
|
||||
}
|
||||
if (mapFragment == null || mapFragment!!.priority[0] != provider) {
|
||||
mapFragment = MapFragment()
|
||||
@@ -186,7 +179,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
MapFragment.GOOGLE,
|
||||
MapFragment.MAPBOX
|
||||
)
|
||||
requireActivity().supportFragmentManager
|
||||
childFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.map, mapFragment!!, mapFragmentTag)
|
||||
.commit()
|
||||
@@ -199,21 +192,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
searchResultIcon = null
|
||||
}
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { v, insets ->
|
||||
ViewCompat.onApplyWindowInsets(binding.root, insets)
|
||||
|
||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.systemWindowInsetTop
|
||||
topMargin = systemWindowInsetTop
|
||||
}
|
||||
|
||||
// margin of layers button
|
||||
// margin of layers button: status bar height + toolbar height + margin
|
||||
val density = resources.displayMetrics.density
|
||||
// status bar height + toolbar height + margin
|
||||
val margin =
|
||||
if (binding.toolbarContainer.layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
insets.systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
||||
systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
||||
} else {
|
||||
insets.systemWindowInsetTop + (12 * density).toInt()
|
||||
systemWindowInsetTop + (12 * density).toInt()
|
||||
}
|
||||
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
@@ -221,6 +216,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
|
||||
// set map padding so that compass is not obstructed by toolbar
|
||||
mapTopPadding = systemWindowInsetTop + (48 * density).toInt() + (16 * density).toInt()
|
||||
// if we actually use map.setPadding here, Mapbox will re-trigger onApplyWindowInsets
|
||||
// and cause an infinite loop. So we rely on onMapReady being called later than
|
||||
// onApplyWindowInsets.
|
||||
|
||||
insets
|
||||
}
|
||||
|
||||
@@ -236,6 +238,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
mapFragment!!.getMapAsync(this)
|
||||
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
|
||||
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
|
||||
@@ -310,19 +314,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
vm.reloadPrefs()
|
||||
if (requestingLocationUpdates && requireContext().checkAnyLocationPermission()
|
||||
&& locationClient.isConnected
|
||||
) {
|
||||
requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
|
||||
val context = context ?: return@registerForActivityResult
|
||||
if (context.checkAnyLocationPermission()) {
|
||||
enableLocation(moveTo = true, animate = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
binding.fabLocate.setOnClickListener {
|
||||
if (!requireContext().checkFineLocationPermission()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION),
|
||||
REQUEST_LOCATION_PERMISSION
|
||||
requestPermissionLauncher.launch(
|
||||
arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
|
||||
)
|
||||
}
|
||||
if (requireContext().checkAnyLocationPermission()) {
|
||||
@@ -351,16 +361,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
val dataSource = when (vm.apiType) {
|
||||
GoingElectricApiWrapper::class.java -> "going_electric"
|
||||
OpenChargeMapApiWrapper::class.java -> "open_charge_map"
|
||||
else -> throw IllegalArgumentException("unsupported data source")
|
||||
}
|
||||
val extras =
|
||||
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
|
||||
findNavController().navigate(
|
||||
R.id.action_map_to_chargepriceFragment,
|
||||
ChargepriceFragmentArgs(charger, dataSource).toBundle(),
|
||||
ChargepriceFragmentArgs(charger).toBundle(),
|
||||
null, extras
|
||||
)
|
||||
}
|
||||
@@ -388,7 +393,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.apiType == GoingElectricApiWrapper::class.java) {
|
||||
if (vm.apiId == "going_electric") {
|
||||
// instructions specific to GoingElectric
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
@@ -593,35 +598,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
updateFavoriteToggle()
|
||||
})
|
||||
vm.searchResult.observe(viewLifecycleOwner, Observer { place ->
|
||||
val map = this.map ?: return@Observer
|
||||
searchResultMarker?.remove()
|
||||
searchResultMarker = null
|
||||
|
||||
if (place != null) {
|
||||
// disable location following when search result is shown
|
||||
vm.myLocationEnabled.value = false
|
||||
if (place.viewport != null) {
|
||||
map.animateCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
|
||||
} else {
|
||||
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
||||
}
|
||||
|
||||
if (searchResultIcon == null) {
|
||||
searchResultIcon =
|
||||
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
|
||||
}
|
||||
searchResultMarker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.z(placeSearchZ)
|
||||
.position(place.latLng)
|
||||
.icon(searchResultIcon)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
} else {
|
||||
binding.search.setText("")
|
||||
}
|
||||
|
||||
updateBackPressedCallback()
|
||||
displaySearchResult(place, moveCamera = true)
|
||||
})
|
||||
vm.layersMenuOpen.observe(viewLifecycleOwner, Observer { open ->
|
||||
binding.fabLayers.visibility = if (open) View.INVISIBLE else View.VISIBLE
|
||||
@@ -638,6 +615,40 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
|
||||
private fun displaySearchResult(place: PlaceWithBounds?, moveCamera: Boolean) {
|
||||
val map = this.map ?: return
|
||||
searchResultMarker?.remove()
|
||||
searchResultMarker = null
|
||||
|
||||
if (place != null) {
|
||||
// disable location following when search result is shown
|
||||
if (moveCamera) {
|
||||
vm.myLocationEnabled.value = false
|
||||
if (place.viewport != null) {
|
||||
map.animateCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
|
||||
} else {
|
||||
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
||||
}
|
||||
}
|
||||
|
||||
if (searchResultIcon == null) {
|
||||
searchResultIcon =
|
||||
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
|
||||
}
|
||||
searchResultMarker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.z(placeSearchZ)
|
||||
.position(place.latLng)
|
||||
.icon(searchResultIcon)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
} else {
|
||||
binding.search.setText("")
|
||||
}
|
||||
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
|
||||
private fun updateBackPressedCallback() {
|
||||
backPressedCallback.isEnabled =
|
||||
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|
||||
@@ -726,6 +737,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
.withStartPosition(position)
|
||||
.withHiddenStatusBar(false)
|
||||
.show()
|
||||
|
||||
}
|
||||
@@ -798,7 +810,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
it.name
|
||||
}
|
||||
}
|
||||
AlertDialog.Builder(activity)
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.charge_cards)
|
||||
.setItems(names.toTypedArray()) { _, i ->
|
||||
val card = data[i]
|
||||
@@ -888,7 +900,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
map.setTrafficEnabled(vm.mapTrafficEnabled.value ?: false)
|
||||
|
||||
// set padding so that compass is not obstructed by toolbar
|
||||
map.setPadding(0, binding.toolbarContainer.height, 0, 0)
|
||||
map.setPadding(0, mapTopPadding, 0, 0)
|
||||
|
||||
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
map.setMapStyle(
|
||||
@@ -957,7 +969,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
lifecycleScope.launch {
|
||||
val address = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
Geocoder(requireContext()).getFromLocationName(locationName, 1).getOrNull(0)
|
||||
Geocoder(requireContext()).getFromLocationName(locationName, 1)
|
||||
?.getOrNull(0)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
@@ -996,7 +1009,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
if (vm.searchResult.value != null) {
|
||||
// show search result (after configuration change)
|
||||
vm.searchResult.postValue(vm.searchResult.value)
|
||||
displaySearchResult(vm.searchResult.value, moveCamera = !positionSet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,16 +1020,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
map.uiSettings.setMyLocationButtonEnabled(false)
|
||||
if (moveTo) {
|
||||
vm.myLocationEnabled.value = true
|
||||
if (locationClient.isConnected) {
|
||||
moveToLastLocation(map, animate)
|
||||
requestLocationUpdates()
|
||||
}
|
||||
moveToLastLocation(map, animate)
|
||||
requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
|
||||
private fun moveToLastLocation(map: AnyMap, animate: Boolean) {
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
|
||||
val location = locationEngine.getLastKnownLocation()
|
||||
if (location != null) {
|
||||
val latLng = LatLng(location.latitude, location.longitude)
|
||||
vm.location.value = latLng
|
||||
@@ -1041,7 +1052,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
// update icons of existing markers (connector filter may have changed)
|
||||
for ((marker, charger) in markers) {
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
@@ -1065,7 +1076,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
// animate marker if it is visible, otherwise remove immediately
|
||||
if (bounds.contains(marker.position)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
val fault = charger.faultReport != null
|
||||
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||
val fav =
|
||||
@@ -1085,7 +1096,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
for (charger in chargers) {
|
||||
if (!map1.contains(charger.id)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
val fault = charger.faultReport != null
|
||||
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||
val fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
|
||||
@@ -1133,23 +1144,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_LOCATION_PERMISSION -> {
|
||||
if ((grantResults.isNotEmpty() && grantResults.any { it == PackageManager.PERMISSION_GRANTED })) {
|
||||
enableLocation(moveTo = true, animate = true)
|
||||
}
|
||||
}
|
||||
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.map, menu)
|
||||
|
||||
val filterItem = menu.findItem(R.id.menu_filter)
|
||||
@@ -1264,6 +1259,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
})
|
||||
})
|
||||
popup.setTouchModal(false)
|
||||
popup.show()
|
||||
}
|
||||
|
||||
@@ -1287,42 +1283,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getRootView(): View {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
val map = this.map ?: return
|
||||
val context = this.context ?: return
|
||||
if (vm.myLocationEnabled.value == true) {
|
||||
if (context.checkAnyLocationPermission()) {
|
||||
moveToLastLocation(map, false)
|
||||
requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(ACCESS_FINE_LOCATION)
|
||||
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
|
||||
private fun requestLocationUpdates() {
|
||||
val request: LocationRequest = LocationRequest.create()
|
||||
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
|
||||
.setInterval(5000)
|
||||
LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, request, this)
|
||||
locationEngine.requestLocationUpdates(
|
||||
Priority.HIGH_ACCURACY,
|
||||
5000,
|
||||
locationListener
|
||||
)
|
||||
requestingLocationUpdates = true
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun removeLocationUpdates() {
|
||||
if (locationClient.isConnected) {
|
||||
LocationServices.FusedLocationApi.removeLocationUpdates(locationClient, this)
|
||||
if (context?.checkAnyLocationPermission() == true) {
|
||||
locationEngine.removeUpdates(locationListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionSuspended() {
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location?) {
|
||||
val map = this.map ?: return
|
||||
if (location == null || vm.myLocationEnabled.value == false) return
|
||||
private val locationListener = LocationListenerCompat { location ->
|
||||
val map = this.map ?: return@LocationListenerCompat
|
||||
if (vm.myLocationEnabled.value == false) return@LocationListenerCompat
|
||||
|
||||
val latLng = LatLng(location.latitude, location.longitude)
|
||||
val oldLoc = vm.location.value
|
||||
@@ -1347,8 +1336,5 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (locationClient.isConnected) {
|
||||
locationClient.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,16 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.databinding.DialogMultiSelectBinding
|
||||
import net.vonforst.evmap.ui.MaterialDialogFragment
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.collections.HashSet
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
class MultiSelectDialog : MaterialDialogFragment() {
|
||||
companion object {
|
||||
fun getInstance(
|
||||
title: String,
|
||||
@@ -43,7 +39,7 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
private lateinit var items: List<MultiSelectItem>
|
||||
private lateinit var binding: DialogMultiSelectBinding
|
||||
|
||||
override fun onCreateView(
|
||||
override fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
@@ -54,19 +50,10 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val density = resources.displayMetrics.density
|
||||
val width = resources.displayMetrics.widthPixels
|
||||
val maxWidth = (500 * density).roundToInt()
|
||||
|
||||
// dialog with 95% screen height
|
||||
dialog?.window?.setLayout(
|
||||
if (width < maxWidth) WindowManager.LayoutParams.MATCH_PARENT else maxWidth,
|
||||
(resources.displayMetrics.heightPixels * 0.95).toInt()
|
||||
)
|
||||
setFullSize(maxWidthDp = 500)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
override fun initView(view: View, savedInstanceState: Bundle?) {
|
||||
val args = requireArguments()
|
||||
val data = args.getSerializable("data") as HashMap<String, String>
|
||||
val selected = args.getSerializable("selected") as HashSet<String>
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
@@ -44,6 +45,14 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
"contributors" -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.about_contributors)
|
||||
.setMessage(getString(R.string.about_contributors_text) + "\n\n" + getString(R.string.about_contributors_list))
|
||||
.setPositiveButton(R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
true
|
||||
}
|
||||
"github_link" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.github_link))
|
||||
true
|
||||
|
||||
@@ -16,7 +16,8 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
viewModelFactory {
|
||||
SettingsViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key)
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -69,7 +70,8 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
R.plurals.chargeprice_some_tariffs_selected,
|
||||
n,
|
||||
n
|
||||
) + "\n" + getString(R.string.pref_my_tariffs_summary)
|
||||
) + "\n" + requireContext().resources
|
||||
.getQuantityString(R.plurals.pref_my_tariffs_summary, n)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
viewModelFactory {
|
||||
SettingsViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key)
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,24 +2,33 @@ package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.preference.ListPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.getAppLocale
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
|
||||
class UiSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
lateinit var langPref: ListPreference
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_ui, rootKey)
|
||||
|
||||
langPref = findPreference("language")!!
|
||||
langPref.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAppLocale(newValue as String)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
langPref.value = getAppLocale()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"language" -> {
|
||||
activity?.let {
|
||||
it.finish();
|
||||
it.startActivity(it.intent);
|
||||
}
|
||||
}
|
||||
"darkmode" -> {
|
||||
updateNightMode(prefs)
|
||||
}
|
||||
|
||||
@@ -4,18 +4,16 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.DialogOpensourceDonationsBinding
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.roundToInt
|
||||
import net.vonforst.evmap.ui.MaterialDialogFragment
|
||||
|
||||
class OpensourceDonationsDialogFramgent : AppCompatDialogFragment() {
|
||||
class OpensourceDonationsDialogFragment : MaterialDialogFragment() {
|
||||
private lateinit var binding: DialogOpensourceDonationsBinding
|
||||
|
||||
override fun onCreateView(
|
||||
override fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
@@ -24,9 +22,7 @@ class OpensourceDonationsDialogFramgent : AppCompatDialogFragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
override fun initView(view: View, savedInstanceState: Bundle?) {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
binding.btnOk.setOnClickListener {
|
||||
prefs.opensourceDonationsDialogShown = true
|
||||
@@ -44,14 +40,5 @@ class OpensourceDonationsDialogFramgent : AppCompatDialogFragment() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val density = resources.displayMetrics.density
|
||||
val width = resources.displayMetrics.widthPixels
|
||||
val maxWidth = (500 * density).roundToInt()
|
||||
|
||||
dialog?.window?.setLayout(
|
||||
if (width < maxWidth) WindowManager.LayoutParams.MATCH_PARENT else maxWidth,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
}
|
||||
271
app/src/main/java/net/vonforst/evmap/location/FusionEngine.kt
Normal file
@@ -0,0 +1,271 @@
|
||||
package net.vonforst.evmap.location
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
|
||||
/**
|
||||
* Location engine that fuses GPS and network locations.
|
||||
*
|
||||
* Simplified version of
|
||||
* https://github.com/lostzen/lost/blob/master/lost/src/main/java/com/mapzen/android/lost/internal/FusionEngine.java
|
||||
*/
|
||||
class FusionEngine(context: Context) : LocationEngine(context),
|
||||
LocationListenerCompat {
|
||||
|
||||
/**
|
||||
* Location updates more than 60 seconds old are considered stale.
|
||||
*/
|
||||
private val RECENT_UPDATE_THRESHOLD_IN_MILLIS = (60 * 1000).toLong()
|
||||
private val RECENT_UPDATE_THRESHOLD_IN_NANOS = RECENT_UPDATE_THRESHOLD_IN_MILLIS * 1000000
|
||||
private val TAG = FusionEngine::class.java.simpleName
|
||||
|
||||
private val locationManager =
|
||||
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private var gpsLocation: Location? = null
|
||||
private var networkLocation: Location? = null
|
||||
|
||||
private val supportsSystemFusedProvider: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.allProviders.contains(
|
||||
LocationManager.FUSED_PROVIDER
|
||||
)
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
override fun getLastKnownLocation(): Location? {
|
||||
if (supportsSystemFusedProvider) {
|
||||
try {
|
||||
return locationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Permissions not granted for fused provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
val minTime = SystemClock.elapsedRealtimeNanos() - RECENT_UPDATE_THRESHOLD_IN_NANOS
|
||||
var bestLocation: Location? = null
|
||||
var bestAccuracy = Float.MAX_VALUE
|
||||
var bestTime = Long.MIN_VALUE
|
||||
for (provider in locationManager.allProviders) {
|
||||
try {
|
||||
val location = locationManager.getLastKnownLocation(provider)
|
||||
if (location != null) {
|
||||
val accuracy = location.accuracy
|
||||
val time = location.elapsedRealtimeNanos
|
||||
if (time > minTime && accuracy < bestAccuracy) {
|
||||
bestLocation = location
|
||||
bestAccuracy = accuracy
|
||||
bestTime = time
|
||||
} else if (time < minTime && bestAccuracy == Float.MAX_VALUE && time > bestTime) {
|
||||
bestLocation = location
|
||||
bestTime = time
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Permissions not granted for provider: $provider", e)
|
||||
}
|
||||
}
|
||||
return bestLocation
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
override fun enable() {
|
||||
var networkInterval = Long.MAX_VALUE
|
||||
var gpsInterval = Long.MAX_VALUE
|
||||
var passiveInterval = Long.MAX_VALUE
|
||||
for ((priority, interval) in requests) {
|
||||
when (priority) {
|
||||
Priority.HIGH_ACCURACY -> {
|
||||
if (interval < gpsInterval) {
|
||||
gpsInterval = interval
|
||||
}
|
||||
if (interval < networkInterval) {
|
||||
networkInterval = interval
|
||||
}
|
||||
}
|
||||
Priority.BALANCED_POWER_ACCURACY, Priority.LOW_POWER -> if (interval < networkInterval) {
|
||||
networkInterval = interval
|
||||
}
|
||||
Priority.NO_POWER -> if (interval < passiveInterval) {
|
||||
passiveInterval = interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsSystemFusedProvider && gpsInterval < Long.MAX_VALUE) {
|
||||
try {
|
||||
enableFused(gpsInterval)
|
||||
checkLastKnownFused()
|
||||
return
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Permissions not granted for fused provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
var checkGps = false
|
||||
if (gpsInterval < Long.MAX_VALUE) {
|
||||
enableGps(gpsInterval)
|
||||
checkGps = true
|
||||
}
|
||||
if (networkInterval < Long.MAX_VALUE) {
|
||||
enableNetwork(networkInterval)
|
||||
if (checkGps) {
|
||||
val lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
val lastNetwork =
|
||||
locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
|
||||
if (lastGps != null && lastNetwork != null) {
|
||||
val useGps = lastGps.isBetterThan(lastNetwork)
|
||||
if (useGps) {
|
||||
checkLastKnownGps()
|
||||
} else {
|
||||
checkLastKnownNetwork()
|
||||
}
|
||||
} else if (lastGps != null) {
|
||||
checkLastKnownGps()
|
||||
} else {
|
||||
checkLastKnownNetwork()
|
||||
}
|
||||
} else {
|
||||
checkLastKnownNetwork()
|
||||
}
|
||||
}
|
||||
if (passiveInterval < Long.MAX_VALUE) {
|
||||
enablePassive(passiveInterval)
|
||||
checkLastKnownPassive()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
override fun disable() {
|
||||
locationManager.removeUpdates(this)
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun enableGps(interval: Long) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
interval,
|
||||
0f,
|
||||
this,
|
||||
looper
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Unable to register for GPS updates.", e)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun enableNetwork(interval: Long) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.NETWORK_PROVIDER,
|
||||
interval,
|
||||
0f,
|
||||
this,
|
||||
looper
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Unable to register for network updates.", e)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun enablePassive(interval: Long) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.PASSIVE_PROVIDER,
|
||||
interval,
|
||||
0f,
|
||||
this,
|
||||
looper
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Unable to register for passive updates.", e)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun enableFused(interval: Long) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.FUSED_PROVIDER,
|
||||
interval,
|
||||
0f,
|
||||
this,
|
||||
looper
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Unable to register for passive updates.", e)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun checkLastKnownGps() {
|
||||
checkLastKnownAndNotify(LocationManager.GPS_PROVIDER)
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun checkLastKnownNetwork() {
|
||||
checkLastKnownAndNotify(LocationManager.NETWORK_PROVIDER)
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun checkLastKnownPassive() {
|
||||
checkLastKnownAndNotify(LocationManager.PASSIVE_PROVIDER)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun checkLastKnownFused() {
|
||||
checkLastKnownAndNotify(LocationManager.FUSED_PROVIDER)
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
private fun checkLastKnownAndNotify(provider: String) {
|
||||
val location = locationManager.getLastKnownLocation(provider)
|
||||
location?.let { onLocationChanged(it) }
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
if (LocationManager.FUSED_PROVIDER == location.provider) {
|
||||
requests.forEach { it.listener.onLocationChanged(location) }
|
||||
} else if (LocationManager.GPS_PROVIDER == location.provider) {
|
||||
gpsLocation = location
|
||||
if (gpsLocation.isBetterThan(networkLocation)) {
|
||||
requests.forEach { it.listener.onLocationChanged(location) }
|
||||
}
|
||||
} else if (LocationManager.NETWORK_PROVIDER == location.provider) {
|
||||
networkLocation = location
|
||||
if (networkLocation.isBetterThan(gpsLocation)) {
|
||||
requests.forEach { it.listener.onLocationChanged(location) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Location?.isBetterThan(other: Location?): Boolean {
|
||||
if (this == null) {
|
||||
return false
|
||||
}
|
||||
if (other == null) {
|
||||
return true
|
||||
}
|
||||
if (this.elapsedRealtimeNanos
|
||||
> other.elapsedRealtimeNanos + RECENT_UPDATE_THRESHOLD_IN_NANOS
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (!this.hasAccuracy()) {
|
||||
return false
|
||||
}
|
||||
return if (!other.hasAccuracy()) {
|
||||
true
|
||||
} else this.accuracy < other.accuracy
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package net.vonforst.evmap.location
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.os.Looper
|
||||
import androidx.annotation.RequiresPermission
|
||||
|
||||
/**
|
||||
* Base class for [com.mapzen.android.lost.internal.FusionEngine].
|
||||
*/
|
||||
abstract class LocationEngine(protected val context: Context) {
|
||||
protected val requests: MutableList<LocationRequest> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Return most best recent location available.
|
||||
*/
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
abstract fun getLastKnownLocation(): Location?
|
||||
|
||||
/**
|
||||
* Enables the engine on receiving a valid location request.
|
||||
*
|
||||
* @param request Valid location request to enable.
|
||||
*/
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
fun requestLocationUpdates(priority: Priority, intervalMs: Long, listener: LocationListener) {
|
||||
requests.add(LocationRequest(priority, intervalMs, listener))
|
||||
enable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the engine when no requests remain, otherwise updates the engine's configuration.
|
||||
*
|
||||
* @param requests Valid location request to enable.
|
||||
*/
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
fun removeUpdates(listener: LocationListener) {
|
||||
this.requests.removeIf { it.listener == listener }
|
||||
disable()
|
||||
if (this.requests.isNotEmpty()) enable()
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
fun removeAllRequests() {
|
||||
requests.clear()
|
||||
disable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclass should perform all operations required to enable the engine. (ex. Register for
|
||||
* location updates.)
|
||||
*/
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
protected abstract fun enable()
|
||||
|
||||
/**
|
||||
* Subclass should perform all operations required to disable the engine. (ex. Remove location
|
||||
* updates.)
|
||||
*/
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
protected abstract fun disable()
|
||||
protected val looper: Looper
|
||||
get() = context.mainLooper
|
||||
|
||||
interface Callback {
|
||||
fun reportLocation(location: Location)
|
||||
}
|
||||
}
|
||||
|
||||
data class LocationRequest(
|
||||
val priority: Priority,
|
||||
val intervalMs: Long,
|
||||
val listener: LocationListener
|
||||
)
|
||||
|
||||
enum class Priority {
|
||||
HIGH_ACCURACY,
|
||||
BALANCED_POWER_ACCURACY,
|
||||
LOW_POWER,
|
||||
NO_POWER
|
||||
}
|
||||
@@ -167,9 +167,9 @@ data class Cost(
|
||||
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
|
||||
if (freecharging != null && freeparking != null) {
|
||||
val charging =
|
||||
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
if (freecharging) ctx.getString(R.string.charging_free) else ctx.getString(R.string.charging_paid)
|
||||
val parking =
|
||||
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
if (freeparking) ctx.getString(R.string.parking_free) else ctx.getString(R.string.parking_paid)
|
||||
return if (emoji) {
|
||||
"⚡ $charging · \uD83C\uDD7F️ $parking"
|
||||
} else {
|
||||
@@ -177,7 +177,7 @@ data class Cost(
|
||||
}
|
||||
} else if (freecharging != null) {
|
||||
val charging =
|
||||
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
if (freecharging) ctx.getString(R.string.charging_free) else ctx.getString(R.string.charging_paid)
|
||||
return if (emoji) {
|
||||
"⚡ $charging"
|
||||
} else {
|
||||
@@ -185,7 +185,7 @@ data class Cost(
|
||||
}
|
||||
} else if (freeparking != null) {
|
||||
val parking =
|
||||
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
if (freeparking) ctx.getString(R.string.parking_free) else ctx.getString(R.string.parking_paid)
|
||||
return if (emoji) {
|
||||
"\uD83C\uDD7F $parking"
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package net.vonforst.evmap.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
|
||||
class NavHostFragment : NavHostFragment() {
|
||||
override fun onCreateNavController(navController: NavController) {
|
||||
super.onCreateNavController(navController)
|
||||
navController.navigatorProvider.addNavigator(
|
||||
override fun onCreateNavHostController(navHostController: NavHostController) {
|
||||
super.onCreateNavHostController(navHostController)
|
||||
navHostController.navigatorProvider.addNavigator(
|
||||
CustomNavigator(
|
||||
requireContext()
|
||||
)
|
||||
|
||||
@@ -1,34 +1,124 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
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
|
||||
|
||||
@Dao
|
||||
interface ChargeLocationsDao {
|
||||
abstract class ChargeLocationsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg locations: ChargeLocation)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertBlocking(vararg locations: ChargeLocation)
|
||||
abstract suspend fun insert(vararg locations: ChargeLocation)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg locations: ChargeLocation)
|
||||
abstract suspend fun delete(vararg locations: ChargeLocation)
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM chargelocation")
|
||||
fun getAllChargeLocations(): LiveData<List<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")
|
||||
suspend fun getAllChargeLocationsAsync(): 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")
|
||||
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
|
||||
private val chargeLocationsDao = db.chargeLocationsDao()
|
||||
|
||||
@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>
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,28 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.await
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Dao
|
||||
abstract class FilterValueDao {
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getBooleanFilterValues(
|
||||
protected abstract suspend fun getBooleanFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<BooleanFilterValue>>
|
||||
): List<BooleanFilterValue>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getMultipleChoiceFilterValues(
|
||||
protected abstract suspend fun getMultipleChoiceFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<MultipleChoiceFilterValue>>
|
||||
): List<MultipleChoiceFilterValue>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getSliderFilterValues(
|
||||
protected abstract suspend fun getSliderFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<SliderFilterValue>>
|
||||
): List<SliderFilterValue>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
|
||||
@@ -54,27 +51,22 @@ abstract class FilterValueDao {
|
||||
dataSource: String
|
||||
)
|
||||
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>> =
|
||||
open suspend fun getFilterValuesAsync(
|
||||
filterStatus: Long,
|
||||
dataSource: String
|
||||
): List<FilterValue> =
|
||||
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
|
||||
MutableLiveData(emptyList())
|
||||
emptyList()
|
||||
} else {
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
val sources = listOf(
|
||||
getBooleanFilterValues(filterStatus, dataSource),
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource),
|
||||
getBooleanFilterValues(filterStatus, dataSource) +
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource) +
|
||||
getSliderFilterValues(filterStatus, dataSource)
|
||||
)
|
||||
for (source in sources) {
|
||||
addSource(source) {
|
||||
val values = sources.map { it.value }
|
||||
if (values.all { it != null }) {
|
||||
value = values.filterNotNull().flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
|
||||
emit(getFilterValuesAsync(filterStatus, dataSource))
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun insert(vararg values: FilterValue) {
|
||||
values.forEach {
|
||||
@@ -98,7 +90,7 @@ abstract class FilterValueDao {
|
||||
if (filterStatus == FILTERS_CUSTOM) return
|
||||
|
||||
deleteFilterValuesForProfile(FILTERS_CUSTOM, dataSource)
|
||||
val values = getFilterValues(filterStatus, dataSource).await().onEach {
|
||||
val values = getFilterValuesAsync(filterStatus, dataSource).onEach {
|
||||
it.profile = FILTERS_CUSTOM
|
||||
}
|
||||
insert(*values.toTypedArray())
|
||||
|
||||
@@ -87,6 +87,7 @@ 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
|
||||
|
||||
@@ -79,6 +79,7 @@ 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
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.storage
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
@@ -74,8 +75,15 @@ class PreferenceDataSource(val context: Context) {
|
||||
}
|
||||
|
||||
|
||||
val language: String
|
||||
get() = sp.getString("language", "default")!!
|
||||
/**
|
||||
* Sets app language. Will be removed and set to null with the next update because storage is
|
||||
* handled by AppCompat.
|
||||
*/
|
||||
var language: String?
|
||||
get() = sp.getString("language", null)
|
||||
set(lang) {
|
||||
sp.edit().putString("language", lang).apply()
|
||||
}
|
||||
|
||||
val darkmode: String
|
||||
get() = sp.getString("darkmode", "default")!!
|
||||
@@ -86,11 +94,14 @@ class PreferenceDataSource(val context: Context) {
|
||||
context.getString(R.string.pref_map_provider_default)
|
||||
)!!
|
||||
|
||||
val searchProvider: String
|
||||
var searchProvider: String
|
||||
get() = sp.getString(
|
||||
"search_provider",
|
||||
context.getString(R.string.pref_search_provider_default)
|
||||
)!!
|
||||
set(value) {
|
||||
sp.edit().putString("search_provider", value).apply()
|
||||
}
|
||||
|
||||
var mapType: AnyMap.Type
|
||||
get() = AnyMap.Type.valueOf(sp.getString("map_type", null) ?: AnyMap.Type.NORMAL.toString())
|
||||
@@ -198,9 +209,41 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putLong("app_start_counter", value).apply()
|
||||
}
|
||||
|
||||
/** Counter for how many times the price comparison page was opened,
|
||||
* introduced with Version 1.3.4 **/
|
||||
var chargepriceCounter: Long
|
||||
get() = sp.getLong("chargeprice_counter", 0)
|
||||
set(value) {
|
||||
sp.edit().putLong("chargeprice_counter", value).apply()
|
||||
}
|
||||
|
||||
var opensourceDonationsDialogShown: Boolean
|
||||
get() = sp.getBoolean("opensource_donations_dialog_shown", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("opensource_donations_dialog_shown", value).apply()
|
||||
}
|
||||
|
||||
var placeSearchResultAndroidAuto: LatLng?
|
||||
get() = if (sp.contains("place_search_result_android_auto_lat")) {
|
||||
LatLng(
|
||||
Double.fromBits(sp.getLong("place_search_result_android_auto_lat", 0L)),
|
||||
Double.fromBits(sp.getLong("place_search_result_android_auto_lng", 0L))
|
||||
)
|
||||
} else null
|
||||
set(value) {
|
||||
if (value == null) {
|
||||
sp.edit().remove("place_search_result_android_auto_lat")
|
||||
.remove("place_search_result_android_auto_lng").apply()
|
||||
} else {
|
||||
sp.edit().putLong("place_search_result_android_auto_lat", value.latitude.toBits())
|
||||
.putLong("place_search_result_android_auto_lng", value.longitude.toBits())
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
var placeSearchResultAndroidAutoName: String?
|
||||
get() = sp.getString("place_search_result_android_auto_name", null)
|
||||
set(value) {
|
||||
sp.edit().putString("place_search_result_android_auto_name", value).apply()
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,19 @@ abstract class RecentAutocompletePlaceDao {
|
||||
limit: Int? = null
|
||||
): List<RecentAutocompletePlace>
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource AND primaryText LIKE '%' || :query || '%' ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract suspend fun searchAsync(
|
||||
query: String,
|
||||
dataSource: String,
|
||||
limit: Int? = null
|
||||
): List<RecentAutocompletePlace>
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract fun getAll(dataSource: String, limit: Int? = null): List<RecentAutocompletePlace>
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract suspend fun getAllAsync(
|
||||
dataSource: String,
|
||||
limit: Int? = null
|
||||
): List<RecentAutocompletePlace>
|
||||
}
|
||||
@@ -32,7 +32,7 @@ fun View.exitCircularReveal(block: () -> Unit) {
|
||||
duration = 350
|
||||
interpolator = DecelerateInterpolator(1f)
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
visibility = View.INVISIBLE
|
||||
block()
|
||||
super.onAnimationEnd(animation)
|
||||
|
||||
@@ -389,9 +389,10 @@ private fun colorToTransparent(color: Int, targetAlpha: Float = 31f / 255): Int
|
||||
val green = Color.green(color)
|
||||
val blue = Color.blue(color)
|
||||
|
||||
val newRed = ((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
|
||||
val newGreen = ((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
|
||||
val newBlue = ((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
|
||||
val newRed = kotlin.math.max(((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt(), 0)
|
||||
val newGreen =
|
||||
kotlin.math.max(((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt(), 0)
|
||||
val newBlue = kotlin.math.max(((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt(), 0)
|
||||
|
||||
return Color.argb((targetAlpha * 255).roundToInt(), newRed, newGreen, newBlue)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (prefs.darkmode) {
|
||||
"on" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
"off" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateAppLocale(language: String) {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
if (language in listOf("", "default")) {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
} else {
|
||||
LocaleListCompat.forLanguageTags(language)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun getAppLocale(): String? {
|
||||
val locales = AppCompatDelegate.getApplicationLocales()
|
||||
return if (locales.isEmpty) {
|
||||
"default"
|
||||
} else {
|
||||
val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() }
|
||||
LocaleListCompat.forLanguageTags(BuildConfig.supportedLocales).getFirstMatch(arr)
|
||||
?.toLanguageTag()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.Gravity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private fun dialogEditText(ctx: Context): Pair<View, EditText> {
|
||||
val container = FrameLayout(ctx)
|
||||
@@ -24,30 +30,19 @@ private fun dialogEditText(ctx: Context): Pair<View, EditText> {
|
||||
|
||||
fun showEditTextDialog(
|
||||
ctx: Context,
|
||||
customize: (AlertDialog.Builder, EditText) -> Unit
|
||||
customize: (MaterialAlertDialogBuilder, EditText) -> Unit
|
||||
): AlertDialog {
|
||||
val (container, input) = dialogEditText(ctx)
|
||||
val dialogBuilder = AlertDialog.Builder(ctx)
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(ctx)
|
||||
.setView(container)
|
||||
|
||||
customize(dialogBuilder, input)
|
||||
|
||||
val dialog = dialogBuilder.show()
|
||||
|
||||
|
||||
// move dialog to top
|
||||
val attrs = dialog.window?.attributes?.apply {
|
||||
gravity = Gravity.TOP
|
||||
}
|
||||
dialog.window?.attributes = attrs
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
|
||||
// focus and show keyboard
|
||||
input.requestFocus()
|
||||
input.postDelayed({
|
||||
val imm =
|
||||
ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT)
|
||||
}, 100)
|
||||
input.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
val text = input.text
|
||||
@@ -60,4 +55,63 @@ fun showEditTextDialog(
|
||||
false
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
/**
|
||||
* DialogFragment that uses Material styling.
|
||||
* This needs a bit of a workaround, see also
|
||||
* https://github.com/material-components/material-components-android/issues/540 and
|
||||
* https://dev.to/bhullnatik/how-to-use-material-dialogs-with-dialogfragment-28i1
|
||||
*/
|
||||
abstract class MaterialDialogFragment : AppCompatDialogFragment() {
|
||||
|
||||
private lateinit var dialogView: View
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext(), theme).apply {
|
||||
dialogView =
|
||||
createView(LayoutInflater.from(requireContext()), null, savedInstanceState)
|
||||
|
||||
setView(dialogView)
|
||||
}.create()
|
||||
initView(dialogView, savedInstanceState)
|
||||
return dialog
|
||||
}
|
||||
|
||||
abstract fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View
|
||||
|
||||
abstract fun initView(view: View, savedInstanceState: Bundle?)
|
||||
|
||||
override fun getView(): View {
|
||||
return dialogView
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
// make sure that custom view fills whole dialog height
|
||||
(view.parent as View).layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
(view.parent.parent as View).layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
(view.parent.parent.parent as View).layoutParams.height =
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the dialog fill the whole width & height of the screen, with an optional maximum
|
||||
* width in dp. Call this during onStart.
|
||||
*/
|
||||
fun setFullSize(maxWidthDp: Int? = null) {
|
||||
val width = resources.displayMetrics.widthPixels
|
||||
val maxWidth = if (maxWidthDp != null) {
|
||||
val density = resources.displayMetrics.density
|
||||
(maxWidthDp * density).roundToInt()
|
||||
} else null
|
||||
|
||||
dialog?.window?.setLayout(
|
||||
if (maxWidth == null || width < maxWidth) WindowManager.LayoutParams.MATCH_PARENT else maxWidth,
|
||||
WindowManager.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (prefs.darkmode) {
|
||||
"on" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
"off" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.appcompat.view.menu.MenuPopupHelper
|
||||
import androidx.appcompat.widget.MenuPopupWindow
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
|
||||
/**
|
||||
* Reflection workaround to make setTouchModal accessible for
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun PopupMenu.setTouchModal(modal: Boolean) {
|
||||
try {
|
||||
val mPopup = javaClass.getDeclaredField("mPopup").let { field ->
|
||||
field.isAccessible = true
|
||||
field.get(this)
|
||||
} as MenuPopupHelper
|
||||
val mPopup2 = mPopup.javaClass.getDeclaredMethod("getPopup").let { method ->
|
||||
method.isAccessible = true
|
||||
method.invoke(mPopup)
|
||||
}
|
||||
val mPopup3 = mPopup2.javaClass.getDeclaredField("mPopup").let { field ->
|
||||
field.isAccessible = true
|
||||
field.get(mPopup2)
|
||||
} as MenuPopupWindow
|
||||
mPopup3.setTouchModal(modal)
|
||||
} catch (e: NoSuchFieldException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: NoSuchMethodException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package net.vonforst.evmap.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
companion object {
|
||||
fun wrap(context: Context, language: String): ContextWrapper {
|
||||
val sysConfig: Configuration = context.applicationContext.resources.configuration
|
||||
val appConfig: Configuration = context.resources.configuration
|
||||
|
||||
if (language == "" || language == "default") {
|
||||
// set default locale
|
||||
Locale.setDefault(ConfigurationCompat.getLocales(sysConfig)[0])
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
appConfig.setLocales(sysConfig.locales)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
appConfig.locale = sysConfig.locale
|
||||
}
|
||||
} else {
|
||||
// set selected locale
|
||||
val locale = Locale(language)
|
||||
Locale.setDefault(locale)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
appConfig.setLocale(locale)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
appConfig.locale = locale
|
||||
}
|
||||
}
|
||||
|
||||
return LocaleContextWrapper(context.createConfigurationContext(appConfig))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import jsonapi.Meta
|
||||
import jsonapi.Relationship
|
||||
import jsonapi.Relationships
|
||||
import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.banana.jsonapi2.HasMany
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
import moe.banana.jsonapi2.JsonBuffer
|
||||
import moe.banana.jsonapi2.ResourceIdentifier
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
@@ -16,30 +16,48 @@ import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class ChargepriceViewModel(application: Application, chargepriceApiKey: String) :
|
||||
class ChargepriceViewModel(
|
||||
application: Application,
|
||||
chargepriceApiKey: String,
|
||||
chargepriceApiUrl: String,
|
||||
private val state: SavedStateHandle
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = ChargepriceApi.create(chargepriceApiKey)
|
||||
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
val charger: MutableLiveData<ChargeLocation> by lazy {
|
||||
MutableLiveData<ChargeLocation>()
|
||||
}
|
||||
|
||||
val dataSource: MutableLiveData<String> by lazy {
|
||||
MutableLiveData<String>()
|
||||
state.getLiveData("charger")
|
||||
}
|
||||
|
||||
val chargepoint: MutableLiveData<Chargepoint> by lazy {
|
||||
MutableLiveData<Chargepoint>()
|
||||
state.getLiveData("chargepoint")
|
||||
}
|
||||
|
||||
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
if (prefs.chargepriceMyVehicles.isEmpty()) {
|
||||
value = Resource.success(emptyList())
|
||||
} else {
|
||||
value = Resource.loading(null)
|
||||
loadVehicles()
|
||||
private val vehicleIds: MutableLiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>().apply {
|
||||
value = prefs.chargepriceMyVehicles
|
||||
}
|
||||
}
|
||||
|
||||
val vehicles: LiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
addSource(vehicleIds.distinctUntilChanged()) { vehicleIds ->
|
||||
if (vehicleIds.isEmpty()) {
|
||||
value = Resource.success(emptyList())
|
||||
} else {
|
||||
value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
value = try {
|
||||
val result = api.getVehicles()
|
||||
Resource.success(result.filter {
|
||||
it.id in vehicleIds
|
||||
})
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
observeForever {
|
||||
vehicle.value = it.data?.firstOrNull()
|
||||
@@ -48,7 +66,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
}
|
||||
|
||||
val vehicle: MutableLiveData<ChargepriceCar> by lazy {
|
||||
MutableLiveData<ChargepriceCar>()
|
||||
state.getLiveData("vehicle")
|
||||
}
|
||||
|
||||
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
|
||||
@@ -94,21 +112,24 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
}
|
||||
}
|
||||
|
||||
val chargePrices: MediatorLiveData<Resource<List<ChargePrice>>> by lazy {
|
||||
val chargePrices: MutableLiveData<Resource<List<ChargePrice>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
value = state["chargePrices"] ?: Resource.loading(null)
|
||||
listOf(
|
||||
charger,
|
||||
dataSource,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors,
|
||||
myTariffs, myTariffsAll
|
||||
).forEach {
|
||||
addSource(it) {
|
||||
addSource(it.distinctUntilChanged()) {
|
||||
if (!batteryRangeSliderDragging.value!!) loadPrices()
|
||||
}
|
||||
}
|
||||
observeForever {
|
||||
// persist data in case fragment gets recreated
|
||||
state["chargePrices"] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,15 +161,15 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.clone().apply {
|
||||
cp.copy(
|
||||
chargepointPrices = filteredPrices
|
||||
}
|
||||
)
|
||||
}
|
||||
}.filterNotNull()
|
||||
.sortedBy { it.chargepointPrices.first().price }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariff?.get()?.id in myTariffs
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -157,6 +178,10 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadPrefs() {
|
||||
vehicleIds.value = prefs.chargepriceMyVehicles
|
||||
}
|
||||
|
||||
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
|
||||
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
|
||||
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
|
||||
@@ -210,10 +235,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
val charger = charger.value
|
||||
val car = vehicle.value
|
||||
val compatibleConnectors = vehicleCompatibleConnectors.value
|
||||
val dataSource = dataSource.value
|
||||
val myTariffs = myTariffs.value
|
||||
val myTariffsAll = myTariffsAll.value
|
||||
if (charger == null || car == null || compatibleConnectors == null || dataSource == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
|
||||
if (charger == null || car == null || compatibleConnectors == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
|
||||
chargePrices.value = Resource.error(null, null)
|
||||
return
|
||||
}
|
||||
@@ -223,34 +247,39 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
loadPricesJob?.cancel()
|
||||
loadPricesJob = viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getChargePrices(ChargepriceRequest().apply {
|
||||
dataAdapter = dataSource
|
||||
station = cpStation
|
||||
vehicle = HasOne(car)
|
||||
tariffs = if (!myTariffsAll) {
|
||||
HasMany<ChargepriceTariff>(*myTariffs!!.map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
it
|
||||
val result = api.getChargePrices(
|
||||
ChargepriceRequest(
|
||||
dataAdapter = ChargepriceApi.getDataAdapter(charger),
|
||||
station = cpStation,
|
||||
vehicle = car,
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange.value!!.map { it.toDouble() },
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
|
||||
),
|
||||
relationships = if (!myTariffsAll) {
|
||||
Relationships(
|
||||
"tariffs" to Relationship.ToMany(
|
||||
(myTariffs ?: emptySet()).map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
id = it
|
||||
)
|
||||
},
|
||||
meta = Meta.from(
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
|
||||
ChargepriceApi.moshi
|
||||
)
|
||||
)
|
||||
)
|
||||
}.toTypedArray()).apply {
|
||||
meta = JsonBuffer.create(
|
||||
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange.value!!.map { it.toDouble() },
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
|
||||
)
|
||||
}, ChargepriceApi.getChargepriceLanguage())
|
||||
val meta =
|
||||
result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta
|
||||
chargePrices.value = Resource.success(result)
|
||||
} else null
|
||||
), ChargepriceApi.getChargepriceLanguage()
|
||||
)
|
||||
|
||||
val meta = result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||
chargePrices.value = Resource.success(result.data)
|
||||
chargePriceMeta.value = Resource.success(meta)
|
||||
} catch (e: IOException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
@@ -261,17 +290,4 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVehicles() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getVehicles()
|
||||
vehicles.value = Resource.success(result.filter {
|
||||
it.id in prefs.chargepriceMyVehicles
|
||||
})
|
||||
} catch (e: IOException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,15 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
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 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 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>>
|
||||
@@ -49,23 +19,21 @@ fun filtersWithValue(
|
||||
addSource(it) {
|
||||
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))
|
||||
}
|
||||
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) =
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
var source: LiveData<List<FilterValue>>? = null
|
||||
addSource(filterStatus) { status ->
|
||||
source?.let { removeSource(it) }
|
||||
source = getFilterValues(status, dataSource)
|
||||
addSource(source!!) { result ->
|
||||
value = result
|
||||
}
|
||||
}
|
||||
filterStatus.switchMap {
|
||||
getFilterValues(it, dataSource)
|
||||
}
|
||||
@@ -8,21 +8,17 @@ 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 var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
|
||||
|
||||
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 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 val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
db.filterValueDao().getFilterValues(FILTERS_CUSTOM, prefs.dataSource)
|
||||
@@ -86,4 +82,11 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
// set selected profile
|
||||
prefs.filterStatus = profileId
|
||||
}
|
||||
|
||||
suspend fun deleteCurrentProfile() {
|
||||
filterProfile.value?.let {
|
||||
db.filterProfileDao().delete(it)
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,28 +6,25 @@ import androidx.lifecycle.*
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
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.goingelectric.GEChargepoint
|
||||
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 java.io.IOException
|
||||
|
||||
@Parcelize
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
|
||||
@@ -42,16 +39,19 @@ internal fun getClusterDistance(zoom: Float): Int? {
|
||||
|
||||
class MapViewModel(application: Application, private val state: SavedStateHandle) :
|
||||
AndroidViewModel(application) {
|
||||
val apiType: Class<ChargepointApi<ReferenceData>>
|
||||
get() = api.value!!.javaClass
|
||||
val apiId: String
|
||||
get() = repo.api.value!!.id
|
||||
val apiName: String
|
||||
get() = api.value!!.getName()
|
||||
get() = repo.api.value!!.name
|
||||
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var api = MutableLiveData<ChargepointApi<ReferenceData>>().apply {
|
||||
value = createApi(prefs.dataSource, application)
|
||||
}
|
||||
private val db = AppDatabase.getInstance(application)
|
||||
private val prefs = PreferenceDataSource(application)
|
||||
private val repo = ChargeLocationsRepository(
|
||||
createApi(prefs.dataSource, application),
|
||||
viewModelScope,
|
||||
db,
|
||||
prefs
|
||||
)
|
||||
|
||||
val bottomSheetState: MutableLiveData<Int> by lazy {
|
||||
state.getLiveData("bottomSheetState")
|
||||
@@ -69,39 +69,20 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
private val filterValues: LiveData<List<FilterValue>> =
|
||||
private val filterValues: LiveData<List<FilterValue>> = repo.api.switchMap {
|
||||
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 {
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
val filterProfiles: LiveData<List<FilterProfile>> = repo.api.switchMap {
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
|
||||
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 chargeCardMap = repo.chargeCardMap
|
||||
|
||||
val filtersCount: LiveData<Int> by lazy {
|
||||
MediatorLiveData<Int>().apply {
|
||||
@@ -119,7 +100,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, referenceData).forEach {
|
||||
listOf(filtersWithValue, repo.api).forEach {
|
||||
addSource(it) {
|
||||
reloadChargepoints()
|
||||
}
|
||||
@@ -139,21 +120,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
|
||||
state.getLiveData("chargerSparse")
|
||||
}
|
||||
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocation>>().apply {
|
||||
listOf(chargerSparse, referenceData).forEach {
|
||||
addSource(it) { _ ->
|
||||
val charger = chargerSparse.value
|
||||
val refData = referenceData.value
|
||||
if (charger != null && refData != null) {
|
||||
loadChargerDetails(charger, refData)
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
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 charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocation>>().apply {
|
||||
addSource(chargerDetails) {
|
||||
@@ -258,7 +235,9 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
|
||||
fun reloadPrefs() {
|
||||
filterStatus.value = prefs.filterStatus
|
||||
api.value = createApi(prefs.dataSource, getApplication())
|
||||
if (prefs.dataSource != apiId) {
|
||||
repo.api.value = createApi(prefs.dataSource, getApplication())
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFilters() {
|
||||
@@ -270,7 +249,11 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
|
||||
suspend fun copyFiltersToCustom() {
|
||||
filterStatus.value?.let { db.filterValueDao().copyFiltersToCustom(it, prefs.dataSource) }
|
||||
filterStatus.value?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.filterValueDao().copyFiltersToCustom(it, prefs.dataSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMapType(type: AnyMap.Type) {
|
||||
@@ -294,8 +277,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
fun reloadChargepoints() {
|
||||
val pos = mapPosition.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
val referenceData = referenceData.value ?: return
|
||||
chargepointLoader(Triple(pos, filters, referenceData))
|
||||
chargepointLoader(pos to filters)
|
||||
}
|
||||
|
||||
private val miniMarkerThreshold = 13f
|
||||
@@ -311,7 +293,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
minPower >= 100 -> {
|
||||
// when only showing high-power chargers we can use large markers
|
||||
zoom < clusterThreshold
|
||||
// because the density is much lower
|
||||
false
|
||||
}
|
||||
else -> {
|
||||
zoom < miniMarkerThreshold
|
||||
@@ -321,17 +304,16 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private var chargepointsInternal: LiveData<Resource<List<ChargepointListItem>>>? = null
|
||||
private var chargepointLoader =
|
||||
throttleLatest(
|
||||
500L,
|
||||
viewModelScope
|
||||
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
|
||||
) { data: Pair<MapPosition, FilterValues> ->
|
||||
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
|
||||
@@ -354,41 +336,40 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
return@throttleLatest
|
||||
}
|
||||
|
||||
if (api is GoingElectricApiWrapper) {
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
|
||||
filteredChargeCards.value =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
|
||||
.toSet()
|
||||
val result = repo.getChargepoints(mapPosition.bounds, mapPosition.zoom, filters)
|
||||
chargepointsInternal?.let { chargepoints.removeSource(it) }
|
||||
chargepointsInternal = result
|
||||
chargepoints.addSource(result) {
|
||||
chargepoints.value = it
|
||||
|
||||
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
|
||||
if (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")
|
||||
} else if (apiId == "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
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -403,42 +384,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
chargerSparse.value = null
|
||||
repo.getChargepointDetail(chargerId).observeForever { response ->
|
||||
if (response.status == Status.SUCCESS) {
|
||||
chargerSparse.value = response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadChargerById(chargerId: Long) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
chargerSparse.value = null
|
||||
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
|
||||
} else {
|
||||
chargerSparse.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,13 @@ import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import java.io.IOException
|
||||
|
||||
class SettingsViewModel(application: Application, chargepriceApiKey: String) :
|
||||
class SettingsViewModel(
|
||||
application: Application,
|
||||
chargepriceApiKey: String,
|
||||
chargepriceApiUrl: String
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = ChargepriceApi.create(chargepriceApiKey)
|
||||
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
||||
@@ -16,6 +16,16 @@ inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
|
||||
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <VM : ViewModel> savedStateViewModelFactory(crossinline f: (SavedStateHandle) -> VM) =
|
||||
object : AbstractSavedStateViewModelFactory() {
|
||||
override fun <T : ViewModel> create(
|
||||
key: String,
|
||||
modelClass: Class<T>,
|
||||
handle: SavedStateHandle
|
||||
) = f(handle) as T
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
@@ -24,9 +34,13 @@ enum class Status {
|
||||
|
||||
/**
|
||||
* A generic class that holds a value with its loading status.
|
||||
* @param <T>
|
||||
</T> */
|
||||
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
|
||||
*
|
||||
* Note that this class implements Parcelable for convenience, but will give a runtime error when
|
||||
* trying to write it to a Parcel if the type parameter does not implement Parcelable.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Resource<out T>(val status: Status, val data: @RawValue T?, val message: String?) :
|
||||
Parcelable {
|
||||
companion object {
|
||||
fun <T> success(data: T?): Resource<T> {
|
||||
return Resource(Status.SUCCESS, data, null)
|
||||
@@ -90,4 +104,41 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group
|
||||
android:scaleX="0.184"
|
||||
android:scaleY="0.184"
|
||||
android:translateX="0.96"
|
||||
android:translateY="0.96">
|
||||
<path
|
||||
android:pathData="M27.1,88.3l-2.2,-19.2l-3.3,0.3l2.2,19.2L27.1,88.3zM39,86.9l-2.2,-19.2l-3.3,0.3l2.2,19.2L39,86.9z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
<path
|
||||
android:pathData="M45.2,113c-1,1.3 -1.8,2.1 -2,2.2c-3,2.4 -5.4,3.1 -7.4,2.2c-3.5,-1.7 -3.2,-8.2 -3.1,-8.9l2.4,0.1c-0.1,1.8 0.2,5.8 1.8,6.6c0.9,0.5 2.5,-0.1 4.6,-1.8l0,0c0,0 6.7,-6.7 5.3,-12c-1.6,-6.4 5.8,-15.5 8.2,-18.6l0.3,-0.3l2,1.5l-0.3,0.5c-7.5,9.2 -8.3,14 -7.7,16.4C50.5,105.4 47.4,110.4 45.2,113z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
<path
|
||||
android:pathData="M19.7,88.1l0.9,7.9l7.3,4.9l9.8,-1l6,-6.4l-0.9,-7.9L19.7,88.1z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
<path
|
||||
android:pathData="M37.6,99.7l-9.8,1l2.1,8.7l7.7,-0.9V99.7L37.6,99.7zM44.6,79l0.8,7.2l-28.2,3.2l-0.8,-7.2L44.6,79z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
<path
|
||||
android:pathData="M66.7,0C46.5,0 30.1,16.4 30.1,36.6c0,27.6 30.8,42 34.5,81.4c0.1,1.2 1,2 2.2,2c1.2,0 2.1,-0.8 2.2,-2c3.7,-39.4 34.5,-53.8 34.5,-81.4C103.3,16.2 86.9,0 66.7,0zM78.4,34.7L64.3,59V40.8h-6V18.7c0,0 20.2,0 20.1,-0.1l-8.1,16.2H78.4z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
</group>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_appicon_notification.png
Normal file
|
After Width: | Height: | Size: 583 B |
BIN
app/src/main/res/drawable-mdpi/ic_appicon_notification.png
Normal file
|
After Width: | Height: | Size: 393 B |
BIN
app/src/main/res/drawable-xhdpi/ic_appicon_notification.png
Normal file
|
After Width: | Height: | Size: 778 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_appicon_notification.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,12 +1,15 @@
|
||||
<vector android:height="44.11976dp"
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportHeight="368.4"
|
||||
android:viewportWidth="233.8"
|
||||
android:width="28dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6c4.4,-21.1 15.4,-40.6 30.6,-55.7C53.3,14 81.1,1.8 109.8,0z" />
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M90.9,57.3v68.2h18.6v55.8l43.4,-74.4h-24.8l24.8,-49.6H90.9z" />
|
||||
android:height="44.11976dp">
|
||||
<path
|
||||
android:pathData="M117,367.4c-0.4,-0.3 -0.8,-0.6 -1.2,-0.9c-1.6,-1.2 -3.1,-2.3 -4.2,-3.7c-2.9,-26.9 -9.6,-51.7 -20.1,-74c-12.4,-27.3 -30.1,-52.4 -47.1,-75.8c-8.7,-12 -19.8,-27.9 -28.8,-45.2C2.3,143.6 -2.1,115.9 3.2,89.9c4.3,-20.4 15,-40 30.3,-55.2C53.6,15.1 81.5,2.8 109.9,1l13.5,0c34.4,1.9 66.9,18.9 86.9,45.4c12.8,16.3 20.8,37.5 22.5,59.8l0,8c-0.7,38.8 -23.7,70.9 -45.9,101.9c-1.7,2.3 -3.3,4.6 -5,6.9c-24.4,34.5 -50.3,76.1 -57.3,123.3c-0.5,2 -0.7,4.3 -0.9,6.5C123.3,359 122.8,364.9 117,367.4z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
<path
|
||||
android:pathData="M123.3,2c34.1,1.9 66.3,18.8 86.2,45c12.6,16.1 20.5,37.1 22.3,59.1l0,8c-0.7,38.5 -23.6,70.5 -45.7,101.3c-1.7,2.3 -3.3,4.6 -5,6.9c-24.5,34.6 -50.5,76.3 -57.4,123.7c-0.5,2.1 -0.7,4.4 -0.9,6.7c-0.5,5.9 -1,11 -5.8,13.4c-0.2,-0.2 -0.5,-0.4 -0.7,-0.5c-1.5,-1.1 -2.9,-2 -3.8,-3.3c-2.9,-26.9 -9.7,-51.8 -20.1,-74C80,261 62.3,235.8 45.2,212.4c-8.7,-11.9 -19.8,-27.8 -28.8,-45.1C3.3,143.3 -1,115.9 4.2,90.1c4.2,-20.2 14.9,-39.6 30,-54.7C54.2,16 81.7,3.8 109.9,2H123.3M123.4,0h-13.6c-28.7,1.8 -56.5,14 -77,34C17.6,49.1 6.6,68.6 2.2,89.7c-5.4,26.7 -0.5,54.8 12.5,78.6C23,184.2 33,199 43.6,213.6c17.5,24 34.7,48.5 47,75.6c10.9,23.2 17.3,48.4 20,73.9c1.5,2.3 4,3.7 6.2,5.4c9.3,-3.5 7.1,-14.3 8.9,-22c6.7,-45.6 30.9,-85.9 57.1,-122.9c23.3,-32.8 50.2,-67.3 51,-109.4v-8.1c-1.7,-21.7 -9.2,-43.1 -22.7,-60.3C190.5,18.5 157.3,1.9 123.4,0L123.4,0z"
|
||||
android:fillColor="#808080" />
|
||||
<path
|
||||
android:pathData="M90.9,57.3v68.2h18.6v55.8l43.4,-74.4h-24.8l24.8,-49.6C152.9,57.3 90.9,57.3 90.9,57.3z"
|
||||
android:fillColor="#808080" />
|
||||
</vector>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
android:height="16dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#dddddd"
|
||||
android:pathData="M12,12m-8.5,0a8.5,8.5 0,1 1,17 0a8.5,8.5 0,1 1,-17 0" />
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,12m-7.5,0a7.5,7.5 0,1 1,15 0a7.5,7.5 0,1 1,-15 0" />
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<path
|
||||
android:fillColor="#B5B5B5"
|
||||
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path
|
||||
android:pathData="M123.3,2c34.1,1.9 66.3,18.8 86.2,45c12.6,16.1 20.5,37.1 22.3,59.1l0,8c-0.7,38.5 -23.6,70.5 -45.7,101.3c-1.7,2.3 -3.3,4.6 -5,6.9c-24.5,34.6 -50.5,76.3 -57.4,123.7c-0.5,2.1 -0.7,4.4 -0.9,6.7c-0.5,5.9 -1,11 -5.8,13.4c-0.2,-0.2 -0.5,-0.4 -0.7,-0.5c-1.5,-1.1 -2.9,-2 -3.8,-3.3c-2.9,-26.9 -9.7,-51.8 -20.1,-74C80,261 62.3,235.8 45.2,212.4c-8.7,-11.9 -19.8,-27.8 -28.8,-45.1C3.3,143.3 -1,115.9 4.2,90.1c4.2,-20.2 14.9,-39.6 30,-54.7C54.2,16 81.7,3.8 109.9,2H123.3M123.4,0h-13.6c-28.7,1.8 -56.5,14 -77,34C17.6,49.1 6.6,68.6 2.2,89.7c-5.4,26.7 -0.5,54.8 12.5,78.6C23,184.2 33,199 43.6,213.6c17.5,24 34.7,48.5 47,75.6c10.9,23.2 17.3,48.4 20,73.9c1.5,2.3 4,3.7 6.2,5.4c9.3,-3.5 7.1,-14.3 8.9,-22c6.7,-45.6 30.9,-85.9 57.1,-122.9c23.3,-32.8 50.2,-67.3 51,-109.4v-8.1c-1.7,-21.7 -9.2,-43.1 -22.7,-60.3C190.5,18.5 157.3,1.9 123.4,0L123.4,0z"
|
||||
android:fillColor="#808080" />
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/chip_background" />
|
||||
<item android:drawable="@color/my_tariff_background" />
|
||||
<item android:drawable="?selectableItemBackground" />
|
||||
</layer-list>
|
||||
@@ -3,7 +3,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
@@ -18,7 +19,6 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:headerLayout="@layout/nav_header"
|
||||
app:menu="@menu/drawer" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
@@ -17,7 +17,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/topPanel"
|
||||
android:id="@+id/topPane"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="88dp"
|
||||
android:background="@color/colorPrimary"
|
||||
@@ -46,7 +46,7 @@
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/topPanel" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/topPane" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText1"
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
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.power)}"
|
||||
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 && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView2"
|
||||
@@ -242,4 +242,4 @@
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
</layout>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll && myTariffs.contains(item.tariff.get().id), item.branding.backgroundColor)}">
|
||||
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll && myTariffs.contains(item.tariffId), item.branding.backgroundColor)}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtTariff"
|
||||
@@ -167,7 +167,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tintNullable="@{BindingAdaptersKt.isDarkMode(context) ? @android:color/white : null}"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="24dp"
|
||||
android:fitsSystemWindows="true"
|
||||
android:id="@+id/nav_header">
|
||||
|
||||
<include layout="@layout/app_logo" />
|
||||
|
||||
@@ -4,50 +4,54 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph">
|
||||
|
||||
<fragment
|
||||
<navigation
|
||||
android:id="@+id/map"
|
||||
android:name="net.vonforst.evmap.fragment.MapFragment"
|
||||
android:label=""
|
||||
tools:layout="@layout/fragment_map">
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterFragment"
|
||||
app:destination="@id/filter"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterProfilesFragment"
|
||||
app:destination="@id/filter_profiles"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_chargepriceFragment"
|
||||
app:destination="@id/chargeprice" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_opensource_donations"
|
||||
app:destination="@id/opensource_donations" />
|
||||
<argument
|
||||
android:name="locationName"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="chargerId"
|
||||
android:defaultValue="0L"
|
||||
app:argType="long" />
|
||||
<argument
|
||||
android:name="latLng"
|
||||
android:defaultValue="@null"
|
||||
app:argType="com.car2go.maps.model.LatLng"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="appStart"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
</fragment>
|
||||
app:startDestination="@id/map_frag">
|
||||
<fragment
|
||||
android:id="@+id/map_frag"
|
||||
android:name="net.vonforst.evmap.fragment.MapFragment"
|
||||
android:label=""
|
||||
tools:layout="@layout/fragment_map">
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterFragment"
|
||||
app:destination="@id/filter"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterProfilesFragment"
|
||||
app:destination="@id/filter_profiles"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_chargepriceFragment"
|
||||
app:destination="@id/chargeprice" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_opensource_donations"
|
||||
app:destination="@id/opensource_donations" />
|
||||
<argument
|
||||
android:name="locationName"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="chargerId"
|
||||
android:defaultValue="0L"
|
||||
app:argType="long" />
|
||||
<argument
|
||||
android:name="latLng"
|
||||
android:defaultValue="@null"
|
||||
app:argType="com.car2go.maps.model.LatLng"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="appStart"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
<fragment
|
||||
android:id="@+id/about"
|
||||
android:name="net.vonforst.evmap.fragment.preference.AboutFragment"
|
||||
@@ -85,15 +89,19 @@
|
||||
android:name="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
|
||||
android:label="@string/settings_android_auto"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
<navigation
|
||||
android:id="@+id/favs"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
android:label="@string/menu_favs"
|
||||
tools:layout="@layout/fragment_favorites">
|
||||
<action
|
||||
android:id="@+id/action_favs_to_map"
|
||||
app:destination="@id/map" />
|
||||
</fragment>
|
||||
app:startDestination="@id/favs_frag">
|
||||
<fragment
|
||||
android:id="@+id/favs_frag"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
android:label="@string/menu_favs"
|
||||
tools:layout="@layout/fragment_favorites">
|
||||
<action
|
||||
android:id="@+id/action_favs_to_map"
|
||||
app:destination="@id/map" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
<fragment
|
||||
android:id="@+id/filter"
|
||||
android:name="net.vonforst.evmap.fragment.FilterFragment"
|
||||
@@ -110,18 +118,18 @@
|
||||
android:label="@string/chargeprice_title"
|
||||
tools:layout="@layout/fragment_chargeprice">
|
||||
<action
|
||||
android:id="@+id/action_chargeprice_to_settingsFragment"
|
||||
app:destination="@id/settings"
|
||||
android:id="@+id/action_chargeprice_to_chargepriceSettingsFragment"
|
||||
app:destination="@id/settings_chargeprice"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_chargeprice_to_donateFragment"
|
||||
app:destination="@id/donate" />
|
||||
<argument
|
||||
android:name="charger"
|
||||
app:argType="net.vonforst.evmap.model.ChargeLocation" />
|
||||
<argument
|
||||
android:name="dataSource"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/donate"
|
||||
@@ -130,7 +138,7 @@
|
||||
tools:layout="@layout/fragment_donate" />
|
||||
<dialog
|
||||
android:id="@+id/opensource_donations"
|
||||
android:name="net.vonforst.evmap.fragment.updatedialogs.OpensourceDonationsDialogFramgent"
|
||||
android:name="net.vonforst.evmap.fragment.updatedialogs.OpensourceDonationsDialogFragment"
|
||||
android:label="@string/donation_dialog_title"
|
||||
tools:layout="@layout/dialog_opensource_donations">
|
||||
<action
|
||||
@@ -152,6 +160,8 @@
|
||||
android:label="OnboardingFragment">
|
||||
<action
|
||||
android:id="@+id/action_onboarding_to_map"
|
||||
app:destination="@id/map" />
|
||||
app:destination="@id/map"
|
||||
app:popUpTo="@id/onboarding"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_language_names">
|
||||
<item>Gerätesprache verwenden</item>
|
||||
<item>Englisch</item>
|
||||
<item>Deutsch</item>
|
||||
</string-array>
|
||||
<string-array name="pref_darkmode_names">
|
||||
<item>Geräteeinstellung verwenden</item>
|
||||
<item>immer an</item>
|
||||
<item>immer aus</item>
|
||||
</string-array>
|
||||
<string-array name="pref_data_source_names">
|
||||
<item>GoingElectric.de</item>
|
||||
<item>Open Charge Map</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -3,8 +3,8 @@
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Anschlüsse</string>
|
||||
<string name="no_maps_app_found">Keine Navigations-App gefunden</string>
|
||||
<string name="no_browser_app_found">Kein Webbrowser gefunden</string>
|
||||
<string name="no_maps_app_found">Bitte installiere eine Navigations-App</string>
|
||||
<string name="no_browser_app_found">Bitte installiere einen Webbrowser</string>
|
||||
<string name="address">Adresse</string>
|
||||
<string name="operator">Betreiber</string>
|
||||
<string name="network">Verbund</string>
|
||||
@@ -19,10 +19,12 @@
|
||||
<string name="cost_detail"><![CDATA[<b>Laden:</b> %1$s · <b>Parken:</b> %2$s]]></string>
|
||||
<string name="cost_detail_charging"><![CDATA[<b>%s laden</b>]]></string>
|
||||
<string name="cost_detail_parking"><![CDATA[<b>%s parken</b>]]></string>
|
||||
<string name="free">Kostenlos</string>
|
||||
<string name="paid">Kostenpflichtig</string>
|
||||
<string name="amenities">Ladeweile</string>
|
||||
<string name="general_info">Allgemeine Hinweise</string>
|
||||
<string name="charging_free">Kostenlos</string>
|
||||
<string name="charging_paid">Kostenpflichtig</string>
|
||||
<string name="parking_free">Kostenlos</string>
|
||||
<string name="parking_paid">Kostenpflichtig</string>
|
||||
<string name="amenities">Ausstattung</string>
|
||||
<string name="general_info">Allgemein</string>
|
||||
<string name="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
|
||||
<string name="realtime_data_loading">Prüfe Echtzeitstatus…</string>
|
||||
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
|
||||
@@ -35,7 +37,7 @@
|
||||
<string name="about">Über EVMap</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="github_link_title">Quellcode</string>
|
||||
<string name="oss_licenses">Open Source-Lizenzen</string>
|
||||
<string name="oss_licenses">Lizenzen</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="settings_ui">Oberfläche</string>
|
||||
<string name="settings_map">Karte</string>
|
||||
@@ -43,10 +45,10 @@
|
||||
<string name="copyright_summary">©2020–2022 Johan von Forstner</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="privacy">Datenschutzerklärung</string>
|
||||
<string name="fav_add">Zu Favoriten hinzufügen</string>
|
||||
<string name="fav_add">Als Favorit speichern</string>
|
||||
<string name="fav_remove">Aus Favoriten entfernen</string>
|
||||
<string name="pref_navigate_use_maps">Navigation sofort starten</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigationsbutton startet direkt Google Maps-Navigation</string>
|
||||
<string name="pref_navigate_use_maps">Sofort navigieren</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigationsbutton startet Routenführung mit Google Maps</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigationsbutton startet Karten-App mit Position der Ladesäule</string>
|
||||
<string name="coordinates">Koordinaten</string>
|
||||
<string name="share">Teilen</string>
|
||||
@@ -57,7 +59,7 @@
|
||||
<string name="filter_connectors">Anschlüsse</string>
|
||||
<string name="plug_type_1">Typ 1</string>
|
||||
<string name="plug_type_2">Typ 2</string>
|
||||
<string name="plug_type_3">Typ 3a</string>
|
||||
<string name="plug_type_3">Typ 3A</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
@@ -69,18 +71,17 @@
|
||||
<string name="none">keine</string>
|
||||
<string name="show_more">mehr…</string>
|
||||
<string name="show_less">weniger…</string>
|
||||
<string name="favorites_empty_state">Wenn du Ladestationen als Favorit markierst, tauchen sie hier auf.</string>
|
||||
<string name="favorites_empty_state">Als Favorit gespeicherte Ladestationen tauchen hier auf</string>
|
||||
<string name="donate">Spenden</string>
|
||||
<string name="donation_successful">Vielen Dank! ❤️</string>
|
||||
<string name="donation_failed">Etwas ist schiefgelaufen. 😕</string>
|
||||
<string name="donation_successful">Vielen Dank ❤️</string>
|
||||
<string name="donation_failed">Etwas ist schiefgelaufen 😕</string>
|
||||
<string name="map_type_normal">Standard</string>
|
||||
<string name="map_type_satellite">Satellit</string>
|
||||
<string name="map_type_terrain">Gelände</string>
|
||||
<string name="map_type">Kartentyp</string>
|
||||
<string name="map_details">Kartendetails</string>
|
||||
<string name="map_traffic">Verkehr</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="faq_desc">Häufig gestellte Fragen</string>
|
||||
<string name="faq">Häufig gestellte Fragen</string>
|
||||
<string name="menu_filters_active">Filter aktiv</string>
|
||||
<string name="filters_activated">Filter aktiviert</string>
|
||||
<string name="filters_deactivated">Filter deaktiviert</string>
|
||||
@@ -97,10 +98,8 @@
|
||||
<string name="edit">bearbeiten</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Sprache</string>
|
||||
<string name="pref_language_summary">App-Sprache ändern</string>
|
||||
<string name="pref_language">App-Sprache</string>
|
||||
<string name="pref_darkmode">Dunkles Design</string>
|
||||
<string name="pref_darkmode_summary">Einstellen, wann der Nachtmodus genutzt wird</string>
|
||||
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
|
||||
<string name="retry">Wiederholen</string>
|
||||
<string name="filter_open_247">24 Stunden geöffnet</string>
|
||||
@@ -150,14 +149,16 @@
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="save_as_profile">Als Profil speichern</string>
|
||||
<string name="save_profile_enter_name">Geben Sie den Namen des Filterprofils ein:</string>
|
||||
<string name="filterprofiles_empty_state">Du hast noch keine Filterprofile gespeichert.</string>
|
||||
<string name="filterprofiles_empty_state">Du hast keine Filterprofile gespeichert</string>
|
||||
<string name="welcome_to_evmap">Willkommen bei EVMap</string>
|
||||
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe.</string>
|
||||
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe</string>
|
||||
<string name="welcome_2_title">Auf die Leistung kommt es an</string>
|
||||
<string name="welcome_2">Die Farbe einer Ladestation auf der Karte zeigt dir die maximale Ladeleistung.</string>
|
||||
<string name="welcome_2_detail">Du kannst die Farben im Menü unter “Über EVMap → FAQ” erneut ansehen)</string>
|
||||
<string name="welcome_2">Die Farbe einer Ladestation zeigt dir die maximale Ladeleistung</string>
|
||||
<string name="welcome_2_detail">Die Farben kannst du unter “Über EVMap → Häufig gestellte Fragen” erneut ansehen</string>
|
||||
<string name="donation_dialog_title">Danke, dass du EVMap nutzt!</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source, ich entwickle es in meiner Freizeit. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Durch die steigende Beliebtheit der App müssen allerdings auch laufende Kosten, z.B. für den Zugriff auf die Datenquellen, gedeckt werden. Daher freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Um die laufenden Kosten für den für die Datenquellen zu decken, freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
|
||||
<string name="chargeprice_donation_dialog_title">Du bist ein richtiger Sparfuchs!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Anscheinend nutzt du den Preisvergleich sehr gern. Mit einer Spende für EVMap kannst du helfen, die Kosten für den Datenzugriff zu decken.</string>
|
||||
<string name="deleted_filterprofile">„%s” gelöscht</string>
|
||||
<string name="undo">Rückgängig</string>
|
||||
<string name="rename">Umbenennen</string>
|
||||
@@ -168,7 +169,7 @@
|
||||
</plurals>
|
||||
<string name="navigate">Navigieren</string>
|
||||
<string name="verified">Verifiziert</string>
|
||||
<string name="verified_desc">Verifiziert von der %s Community – nicht zwangsläufig auch aktuell verfügbar.</string>
|
||||
<string name="verified_desc">Ladestation wurde mindestens einmal von einem Mitglied der %s Community getestet</string>
|
||||
<string name="charge_price_format">%1$.2f %2$s</string>
|
||||
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
|
||||
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
|
||||
@@ -178,67 +179,56 @@
|
||||
<string name="chargeprice_session_fee">Startgebühr</string>
|
||||
<string name="chargeprice_per_kwh">pro kWh</string>
|
||||
<string name="chargeprice_per_minute">pro min</string>
|
||||
<string name="chargeprice_blocking_fee">Blockiergeb. >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Keine geeigneten Tarife für diese Ladestation bei Chargeprice.app gefunden.</string>
|
||||
<string name="chargeprice_blocking_fee">Blockiergeb. >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Keine Tarife für diese Ladestation bei Chargeprice.app gefunden</string>
|
||||
<string name="powered_by_chargeprice">powered by Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Fixkosten: %1$.2f %2$s/Monat</string>
|
||||
<string name="chargeprice_min_spend">Mindestumsatz: %1$.2f %2$s/Monat</string>
|
||||
<string name="settings_chargeprice">Preisvergleich</string>
|
||||
<string name="pref_my_vehicle">Meine Fahrzeuge</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Nur Tarife ohne monatliche Gebühren</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Tarife mit monatlichen Gebühren ausschließen</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Exklusive Energiekunden-Tarife anzeigen</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Anbieter bieten für ihre Kunden (z.B. Haushaltsstrom, Gas) günstigere Tarife an</string>
|
||||
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus.</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Energieversorger bieten für ihre Kunden spezielle Tarife an</string>
|
||||
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus</string>
|
||||
<string name="chargeprice_battery_range">Laden von %1$.0f%% bis %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Laden von</string>
|
||||
<string name="chargeprice_battery_range_to">bis</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, ca. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_vehicle">Fahrzeug</string>
|
||||
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
|
||||
<string name="close">schließen</string>
|
||||
<string name="edit_on_goingelectric_info">Logge dich zuerst bei GoingElectric.de ein, falls hier nur eine leere Seite erscheint</string>
|
||||
<string name="close">Schließen</string>
|
||||
<string name="chargeprice_title">Preise</string>
|
||||
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Kein kompatibler Anschluss an dieser Ladestation</string>
|
||||
<string name="pref_chargeprice_currency">Währung</string>
|
||||
<string name="pref_my_tariffs">Meine Tarife</string>
|
||||
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
|
||||
<string name="pref_my_tariffs_summary">(werden im Preisvergleich hervorgehoben)</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
<item quantity="one">(wird im Preisvergleich hervorgehoben)</item>
|
||||
<item quantity="other">(werden im Preisvergleich hervorgehoben)</item>
|
||||
</plurals>
|
||||
<string name="license">Lizenz</string>
|
||||
<string name="settings_charger_data">Ladesäulen</string>
|
||||
<string name="pref_data_source">Datenquelle</string>
|
||||
<string-array name="pref_chargeprice_currency_names">
|
||||
<item>Schweizer Franken (CHF)</item>
|
||||
<item>Tschechische Krone (CZK)</item>
|
||||
<item>Dänische Krone (DKK)</item>
|
||||
<item>Euro (EUR)</item>
|
||||
<item>Britisches Pfund (GBP)</item>
|
||||
<item>Kroatische Kuna (HRK)</item>
|
||||
<item>Ungarischer Forint (HUF)</item>
|
||||
<item>Isländische Krone (ISK)</item>
|
||||
<item>Norwegische Krone (NOK)</item>
|
||||
<item>Polnischer Złoty (PLN)</item>
|
||||
<item>Schwedische Krone (SEK)</item>
|
||||
<item>US-Dollar (USD)</item>
|
||||
</string-array>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d Tarif ausgewählt</item>
|
||||
<item quantity="other">%d Tarife ausgewählt</item>
|
||||
</plurals>
|
||||
<string name="unknown_operator">Unbekannter Betreiber</string>
|
||||
<string name="data_sources_description">EVMap unterstützt verschiedene Datenquellen für Ladestationen. Bitte wähle aus, welche du nutzen möchtest. Du kannst sie später in den Einstellungen der App ändern.</string>
|
||||
<string name="data_sources_description">Bitte wähle eine Datenquelle für Ladestationen aus. Du kannst sie später in den Einstellungen der App ändern.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in Deutschland, Österreich, Schweiz und vielen angrenzenden Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt & offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
|
||||
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in den deutschsprachigen Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt und offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
|
||||
<string name="next">weiter</string>
|
||||
<string name="get_started">Los geht\'s</string>
|
||||
<string name="got_it">Alles klar</string>
|
||||
<string name="lets_go">Und los</string>
|
||||
<string name="crash_report_text">Sorry, anscheinend ist EVMap abgestürzt. Bitte schicke einen Fehlerbericht an den Entwickler.</string>
|
||||
<string name="crash_report_text">EVMap ist abgestürzt. Bitte schicke einen Fehlerbericht an den Entwickler.</string>
|
||||
<string name="crash_report_comment_prompt">Du kannst unten noch einen Kommentar hinzufügen:</string>
|
||||
<string name="powered_by_mapbox">powered by Mapbox</string>
|
||||
<string name="pref_search_provider">Anbieter für Ortssuche</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Wenn du diese Funktion häufig nutzt, würde ich mich über eine Spende unter \"Über EVMap -> Spenden\" sehr freuen.]]></string>
|
||||
<string name="pref_search_provider_info">Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Über eine Spende unter \"Über EVMap -> Spenden\" würde ich mich sehr freuen.</string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string>
|
||||
<string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string>
|
||||
@@ -254,9 +244,30 @@
|
||||
<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"><![CDATA[Erlaubt das Laden mit >4.5 kW an AC-Stationen für Autos mit 1-phasigem Lader]]></string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Kartenrotation erlauben</string>
|
||||
<string name="pref_map_rotate_gestures_on">Karte kann mit Zweifingergeste rotiert werden</string>
|
||||
<string name="pref_map_rotate_gestures_off">Karte bleibt fest nach Norden ausgerichtet</string>
|
||||
<string name="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>
|
||||
<string name="refresh_live_data">Echtzeitstatus aktualisieren</string>
|
||||
</resources>
|
||||
<string name="autocomplete_connection_error">Vorschläge konnten nicht geladen werden</string>
|
||||
<string name="pref_language_device_default">Gerätesprache verwenden</string>
|
||||
<string name="pref_darkmode_device_default">Geräteeinstellung verwenden</string>
|
||||
<string name="pref_darkmode_always_on">immer an</string>
|
||||
<string name="pref_darkmode_always_off">immer aus</string>
|
||||
<string name="pref_chargeprice_currency_chf">Schweizer Franken (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Tschechische Krone (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Dänische Krone (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Britisches Pfund (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Ungarischer Forint (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Isländische Krone (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Norwegische Krone (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Polnischer Złoty (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Schwedische Krone (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">US-Dollar (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_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>
|
||||
</resources>
|
||||