Compare commits
557 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c48f33e265 | ||
|
|
8ba4897026 | ||
|
|
42916d71ca | ||
|
|
5ca7524e8b | ||
|
|
c37f72a26b | ||
|
|
6f0113c50d | ||
|
|
f99ea7ca9e | ||
|
|
788db0c10f | ||
|
|
db2213a50f | ||
|
|
ace4126035 | ||
|
|
d5d6e4f314 | ||
|
|
55999d15e6 | ||
|
|
b61ca609d3 | ||
|
|
b0afad2144 | ||
|
|
94842954e3 | ||
|
|
1bee5f7e13 | ||
|
|
d636cde70e | ||
|
|
2a4497fe7a | ||
|
|
10e3287d82 | ||
|
|
a0f7a389c8 | ||
|
|
1cabb8dccf | ||
|
|
85079bb888 | ||
|
|
10bfc21f54 | ||
|
|
ae33fce637 | ||
|
|
773d57b9a9 | ||
|
|
022f570322 | ||
|
|
a6c2b30325 | ||
|
|
2210e65e5c | ||
|
|
024e3cef35 | ||
|
|
687ef2ec0f | ||
|
|
9e61dce7be | ||
|
|
aad7a320d0 | ||
|
|
96df684b80 | ||
|
|
7903c027c7 | ||
|
|
06801c1898 | ||
|
|
c946b0fcd3 | ||
|
|
dd4fcc7550 | ||
|
|
2ce82b961b | ||
|
|
1be519b1ee | ||
|
|
01737f21d2 | ||
|
|
17ce9f420b | ||
|
|
6eb90498eb | ||
|
|
074e0bf904 | ||
|
|
41ac223e97 | ||
|
|
f7196bcce0 | ||
|
|
4f6092e5dc | ||
|
|
dfd42e1ffd | ||
|
|
895b24d406 | ||
|
|
3dea7993f3 | ||
|
|
ca90f1b37f | ||
|
|
fe0843e653 | ||
|
|
0f42ae84de | ||
|
|
2748b0a3db | ||
|
|
14798dee6a | ||
|
|
1cb48f7e0e | ||
|
|
dc0f4d3eab | ||
|
|
8ae954f37b | ||
|
|
1ed3b73285 | ||
|
|
2ba6a86b34 | ||
|
|
463ff61420 | ||
|
|
81b4e77a66 | ||
|
|
d16d48bf8f | ||
|
|
edfce541f6 | ||
|
|
26136dc482 | ||
|
|
0d11e450ac | ||
|
|
265b530936 | ||
|
|
8c5c7aeb58 | ||
|
|
23873dccdb | ||
|
|
6006790ffb | ||
|
|
f5fc32f420 | ||
|
|
90c6357093 | ||
|
|
69ca8723a5 | ||
|
|
20400b630a | ||
|
|
b22ca736cb | ||
|
|
ea906ec969 | ||
|
|
ec2b6d4f28 | ||
|
|
e7c2683ee2 | ||
|
|
d76051ec3a | ||
|
|
975ba2bcce | ||
|
|
dc067fd86b | ||
|
|
226ca3a60e | ||
|
|
af63ee350b | ||
|
|
21d4060ac9 | ||
|
|
3b9efa0302 | ||
|
|
95d93af0d6 | ||
|
|
17a6a253d4 | ||
|
|
f73545c01e | ||
|
|
e4fa1f2c78 | ||
|
|
b2b5cc63e8 | ||
|
|
84ba62f755 | ||
|
|
b29653049a | ||
|
|
4159491589 | ||
|
|
4e67f434cd | ||
|
|
5e58d52a0d | ||
|
|
eddc1f9b61 | ||
|
|
b5054b4dc9 | ||
|
|
926799bb1d | ||
|
|
f038138620 | ||
|
|
1c44e5ae3d | ||
|
|
c58543fe3f | ||
|
|
a5db42322f | ||
|
|
bb0d2e35d4 | ||
|
|
38c8c5510f | ||
|
|
8d1d15ad68 | ||
|
|
954203bf18 | ||
|
|
524e9fcfc0 | ||
|
|
ae2041d26b | ||
|
|
698c832518 | ||
|
|
17c1a11675 | ||
|
|
d04661e925 | ||
|
|
02316fceb9 | ||
|
|
9bf7a90302 | ||
|
|
2697389b49 | ||
|
|
cd0e381707 | ||
|
|
e5ed5eeafe | ||
|
|
b25c61fbea | ||
|
|
d472be1676 | ||
|
|
24fa85929e | ||
|
|
4a67ffd956 | ||
|
|
fab66d1f84 | ||
|
|
0783c6c272 | ||
|
|
c5714c8592 | ||
|
|
cb4b571721 | ||
|
|
0bfa80bbe0 | ||
|
|
d77f13682d | ||
|
|
0c19eb5833 | ||
|
|
a5abedae55 | ||
|
|
8405f4f4fa | ||
|
|
f435180c03 | ||
|
|
c2c3e96e97 | ||
|
|
9100a6f442 | ||
|
|
5403549e0a | ||
|
|
c95f1e7c24 | ||
|
|
f8d5b78112 | ||
|
|
246d456851 | ||
|
|
3d303b6535 | ||
|
|
135fce43c3 | ||
|
|
ee354d2cd1 | ||
|
|
350f18df8e | ||
|
|
dda151abf5 | ||
|
|
a86f1397f4 | ||
|
|
086cc51dd3 | ||
|
|
0de91bc107 | ||
|
|
3436bcd870 | ||
|
|
22c150d557 | ||
|
|
675abb5011 | ||
|
|
af2a2cfcae | ||
|
|
f74526fdd6 | ||
|
|
c5bbca0428 | ||
|
|
6167079c0e | ||
|
|
c3836a92ad | ||
|
|
dccce1a0a0 | ||
|
|
74d79640a8 | ||
|
|
0eb6ece780 | ||
|
|
ae15b13591 | ||
|
|
4962eb7268 | ||
|
|
abe360d7c2 | ||
|
|
2aa1fcf5bd | ||
|
|
221e5f49bc | ||
|
|
df6f26ad56 | ||
|
|
1210efd3b9 | ||
|
|
097be8c92b | ||
|
|
16031884ac | ||
|
|
c0b4c56eda | ||
|
|
9587ee948d | ||
|
|
890eec4419 | ||
|
|
c972c871d4 | ||
|
|
e4da902430 | ||
|
|
7a5d4b4107 | ||
|
|
80642b1731 | ||
|
|
6dab611c1b | ||
|
|
d9fc43af68 | ||
|
|
2fd0fa7e22 | ||
|
|
b04284fb16 | ||
|
|
7b3bd84d18 | ||
|
|
773d052819 | ||
|
|
4e0ad98e17 | ||
|
|
d8e572338a | ||
|
|
ff86eeff95 | ||
|
|
47f57992fb | ||
|
|
0ae59358ca | ||
|
|
576e0b9c42 | ||
|
|
3878b27154 | ||
|
|
2166ac076a | ||
|
|
c489df2aaf | ||
|
|
56712ff1af | ||
|
|
e2cf332f34 | ||
|
|
0b541d498d | ||
|
|
1bdc576300 | ||
|
|
fb5da76834 | ||
|
|
ad922f0667 | ||
|
|
773b35d9ce | ||
|
|
a3347c9d62 | ||
|
|
da671b8dd3 | ||
|
|
6d877e13e4 | ||
|
|
8ab1d7170c | ||
|
|
1f75d722cd | ||
|
|
11bd4b2cec | ||
|
|
dcc03da237 | ||
|
|
295c00ea55 | ||
|
|
8d6756d57d | ||
|
|
71acd28b74 | ||
|
|
e79c1168ff | ||
|
|
9833159fa8 | ||
|
|
88ace5ba82 | ||
|
|
0ed82d15ff | ||
|
|
0f525a6c48 | ||
|
|
a91a5ce52e | ||
|
|
cd3b1db90d | ||
|
|
6e3e34c642 | ||
|
|
8ce7f5cae2 | ||
|
|
fae3bb2038 | ||
|
|
9490aa7110 | ||
|
|
66a27d19f3 | ||
|
|
09cf6cb087 | ||
|
|
4d23c916a9 | ||
|
|
fec5de1de1 | ||
|
|
89957ef738 | ||
|
|
a8e9bcd9eb | ||
|
|
0c3e3b0c35 | ||
|
|
78f9b7162c | ||
|
|
600a294ab2 | ||
|
|
1b8bedcd6d | ||
|
|
1b7b5121e6 | ||
|
|
e469ce83e5 | ||
|
|
ef68e6039e | ||
|
|
2ad673f8aa | ||
|
|
5b55087337 | ||
|
|
cb8a81823d | ||
|
|
742950b62c | ||
|
|
9bb8825ab7 | ||
|
|
a0c41290cd | ||
|
|
85240f0145 | ||
|
|
7cc50d7127 | ||
|
|
3d0ebc0b85 | ||
|
|
4e83e37d61 | ||
|
|
94030c010c | ||
|
|
6b287c4084 | ||
|
|
1f6fe04b7d | ||
|
|
91d5ce02e2 | ||
|
|
22bd9ed9e8 | ||
|
|
19142e0b59 | ||
|
|
06fe347c73 | ||
|
|
adead1ac3c | ||
|
|
f722ae5d7a | ||
|
|
33a14c581c | ||
|
|
b80ddf8851 | ||
|
|
848745359d | ||
|
|
5d47ca2e3a | ||
|
|
c28a5382d4 | ||
|
|
f8bdae78cd | ||
|
|
9891cf8e88 | ||
|
|
172e66fe15 | ||
|
|
4a925d10bd | ||
|
|
c7fc0a34ed | ||
|
|
9780f6d2c0 | ||
|
|
f6afb2a8cb | ||
|
|
b0371c1b20 | ||
|
|
2625acffda | ||
|
|
7418748c0f | ||
|
|
3c3edf9ab4 | ||
|
|
d52ec0c63f | ||
|
|
eebb060f1a | ||
|
|
19c0f311ad | ||
|
|
9e280d3150 | ||
|
|
0998ed1f67 | ||
|
|
d7a644cb78 | ||
|
|
f2d98f9d82 | ||
|
|
09d6647ec0 | ||
|
|
1d3e3417aa | ||
|
|
20ae25cf8a | ||
|
|
087178193b | ||
|
|
60d54c989b | ||
|
|
c0555e7965 | ||
|
|
49c2fb3494 | ||
|
|
c1ec46917e | ||
|
|
ac11cddd42 | ||
|
|
6267e897d4 | ||
|
|
8a0224707b | ||
|
|
09ded65b4e | ||
|
|
c8f333ce89 | ||
|
|
4989aedd8b | ||
|
|
4beb1f92ad | ||
|
|
f24b7d1c2c | ||
|
|
094a26980f | ||
|
|
098f815ac9 | ||
|
|
6c51693b8d | ||
|
|
a360c71ecb | ||
|
|
b9da72e449 | ||
|
|
b0aeb8af98 | ||
|
|
1867d1bf7a | ||
|
|
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 | ||
|
|
0aef554395 | ||
|
|
35f5185893 | ||
|
|
f8378eb338 | ||
|
|
0bf56701cc | ||
|
|
aa5c36d1aa | ||
|
|
93787fae74 | ||
|
|
65b6c817fa | ||
|
|
f022823093 | ||
|
|
63bb161e09 | ||
|
|
d0de607222 | ||
|
|
abec208768 | ||
|
|
fa2b7bf180 | ||
|
|
258a04b14e | ||
|
|
1cedb2bccd | ||
|
|
20409343fd | ||
|
|
24720d7670 | ||
|
|
096ef902b7 | ||
|
|
e70ab68ff8 | ||
|
|
a69447bb95 | ||
|
|
326493f5c1 | ||
|
|
6adfda8c33 | ||
|
|
d02dd41127 | ||
|
|
41bafbcf46 | ||
|
|
c135e87be5 | ||
|
|
f6fd8866da | ||
|
|
3c485ff0c0 | ||
|
|
0ca8fb0eee | ||
|
|
dc9f47df8a | ||
|
|
4fab0fbf04 | ||
|
|
7bdd277c92 | ||
|
|
3c3d6de867 | ||
|
|
b9d79994f1 | ||
|
|
133a2be961 | ||
|
|
cd934ff448 | ||
|
|
518cf11dc8 | ||
|
|
2f3d4dd90e | ||
|
|
c8b2c34f47 | ||
|
|
57a16ec5f8 | ||
|
|
a4bbb15f64 | ||
|
|
a7c18fc325 | ||
|
|
13034df25e | ||
|
|
6e22f26e54 | ||
|
|
54332e8f77 | ||
|
|
c25190e340 | ||
|
|
fc38a9750a | ||
|
|
4765a18324 | ||
|
|
4f98dd1617 | ||
|
|
c35ca24906 | ||
|
|
8b8ba26a9f | ||
|
|
0b6647a539 | ||
|
|
b4184dc2de | ||
|
|
c105cbfff1 | ||
|
|
5d3633c33d | ||
|
|
c685086176 | ||
|
|
653a84037e | ||
|
|
e182f8ae82 | ||
|
|
8213aab375 | ||
|
|
9c0ffc9cc3 | ||
|
|
91be9ae0a6 | ||
|
|
15506ce007 | ||
|
|
83ef20d515 | ||
|
|
3c5201e5ca | ||
|
|
0db3d1939a | ||
|
|
09113591a7 | ||
|
|
c32580e0f5 | ||
|
|
b89a5a91ac | ||
|
|
6e178058d9 | ||
|
|
f10bda63e5 | ||
|
|
12b4bf359a | ||
|
|
95bff6d8d4 | ||
|
|
2434e5e75e | ||
|
|
ba55f1bc8a | ||
|
|
57747a9f01 | ||
|
|
f12ed008dd | ||
|
|
2f0543a639 | ||
|
|
d7a769a0c1 | ||
|
|
0f1d98bd61 | ||
|
|
9ce5685ca1 | ||
|
|
a364c27e2b | ||
|
|
870bcd25f5 | ||
|
|
84d8b6ca54 | ||
|
|
73968324b8 | ||
|
|
7d1093b8c0 | ||
|
|
dc7fe22614 | ||
|
|
f082165369 | ||
|
|
d5aa1da7ba | ||
|
|
ab30cd6eab | ||
|
|
322d6bea07 | ||
|
|
038da0856e | ||
|
|
9548748a64 | ||
|
|
392c2ecc9a | ||
|
|
04723a5c58 | ||
|
|
3352328930 | ||
|
|
8229b92d43 | ||
|
|
f73881a69a | ||
|
|
a53659f835 | ||
|
|
ea94d5bf03 | ||
|
|
85bf04504b | ||
|
|
f591829cb5 | ||
|
|
d69b5b3d3f | ||
|
|
d1f5714bf5 | ||
|
|
14229a9c90 | ||
|
|
e505fea043 | ||
|
|
ac3d0b0eb0 | ||
|
|
04aa8d1160 | ||
|
|
7b3735f8e8 | ||
|
|
9545d729f1 |
8
.github/workflows/apikeys-ci.xml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">ci</string>
|
||||
<string name="mapbox_key" translatable="false">ci</string>
|
||||
<string name="goingelectric_key" translatable="false">ci</string>
|
||||
<string name="chargeprice_key" translatable="false">ci</string>
|
||||
<string name="openchargemap_key" translatable="false">ci</string>
|
||||
<string name="fronyx_key" translatable="false">ci</string>
|
||||
</resources>
|
||||
87
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
name: Release
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Build and upload release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
- name: Decrypt keystore
|
||||
run: openssl aes-256-cbc -K ${{ secrets.encrypted_53968681344a_key }} -iv ${{ secrets.encrypted_53968681344a_iv }} -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
|
||||
- name: Extract version code
|
||||
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s\+[0-9]*" app/build.gradle | awk '{ print $2 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
||||
|
||||
- name: Build app release
|
||||
env:
|
||||
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
||||
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
||||
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
|
||||
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
||||
run: ./gradlew assembleRelease --no-daemon
|
||||
|
||||
- name: release
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
with:
|
||||
draft: false
|
||||
prerelease: false
|
||||
release_name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref }}
|
||||
body_path: fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: upload Google artifact
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/googleNormal/release/app-google-normal-release.apk
|
||||
asset_name: app-google-normal-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
- name: upload Foss artifact
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fossNormal/release/app-foss-normal-release.apk
|
||||
asset_name: app-foss-normal-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
- name: upload Google Automotive artifact
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/googleAutomotive/release/app-google-automotive-release.apk
|
||||
asset_name: app-google-automotive-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
- name: upload Foss Automotive artifact
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
||||
asset_name: app-foss-automotive-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
36
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
name: Tests
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
name: Build and Test (${{ matrix.buildvariant }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
buildvariant: [ FossNormal, FossAutomotive, GoogleNormal, GoogleAutomotive ]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Copy apikeys.xml
|
||||
run: cp .github/workflows/apikeys-ci.xml app/src/main/res/values/apikeys.xml
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assemble${{ matrix.buildvariant }}Debug --no-daemon
|
||||
- name: Run unit tests
|
||||
run: ./gradlew test${{ matrix.buildvariant }}DebugUnitTest --no-daemon
|
||||
- name: Run Android Lint
|
||||
run: ./gradlew lint${{ matrix.buildvariant }}Debug --no-daemon
|
||||
46
.travis.yml
@@ -1,46 +0,0 @@
|
||||
language: java
|
||||
dist: focal
|
||||
env:
|
||||
global:
|
||||
- secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY=
|
||||
- secure: Hoko50vP+Mwm/O4CWvPvjMxd1gGhi+Bultjyy1WpludSmZFCfKz45Mj1EqzeYk6MeMLvGOEkSLB5wjdXdgJ9j5gEOF5K34k4vESJA7+DqDO3I7Xw9cnWgOXdFqB0qGHar0TVP3Dfg7ZcRgtmeX6t2uoELFLiS9GvTnbZXk4PCUybd979Xi8XHjEQV7+3EZbSjtsI4GFeIK1rrjd0I+UM88zYrWnz1KhdCjWvQb4iZjo+ib6NmGGEMqR7jJPRZz3KB01Y+n5h21qq9/Nv31zQIqt2B5nRxy3vBvvqKapgprIk+hVOpNnBU8w89uWUU6tYUeFk0t7z1TYWjgaBrMmGCM+aKkQ2q5F/ygNzDwB+KkpJx709Yhf0ZX8g2yvkdz+Ok7moYuvmrOPOf4E/U3BlfZxbGtRD2bGYbDgHLFYlTn6v5J2kJHDJAz31yvF5jvJejDaPp2IBVfoMRy7ZJFmGUNHGd9Se6bRwxS++AoobP5WDrBiUXNe2KKDMs3e7vbaO+hLbZ9XHpjeWIJGUfvtTee8EHZqF/8A3ju53V4/R0ehlOv2UZbpYNqcmwrsy9/R4pgMfDkG3Q054LmYrxD8DIC9b8excVMwWRP5aQ1TqZnxO2B1/vJU87RcnGl3jekeHTdHXbRq9BMV4dAdPfB9X3nGIi2GgV0iTBk+24xccOc0=
|
||||
- secure: RsN/2nBVv0byMzwchuAhDti1AWKECg3Uzqk6Ahgaqg02zx8GHj60j0qNax7KuMUg70X3G4b3m8ZzndAU0wcJ98UyIku7ofUYgXHm0XYKTJwiyyhrKJ8pZ4qoeCoRkitIGIitlb84fSufVamcoLyLNbLUsO5OzL+Uscyhq/BwAhyOhj5FB9DM+GE9ntanQ4m3k4guMyMctR9CGS6Tk4LKJ1WCm0AnjTPalc+we+7yORY2J/9d6VHLKfaFXbpuWmrdnFfDZqxqcUODsxPVE0LuUtXyKQDTjjfQc8106Z/z19AJS+oLm/2UND84PD3MqsjX6EX0/9k3fY0OsoCuQAivDRmtevQ0bDQrTAyeBLcfnPCw/MYiJWyBcaYBAYK42EAfsFTDBRIduFB/Dpvg+8GuLZSdm4xVYpTlQ2pMtlGNWIGIRQsjk9LZ8swq8QBMiiF/bpMGKdfmnyQj8jkEOCWaAzkR9O+4E4Y7PuBENef9XuV0hLMryCrML2YXigxAKkEUOPhfnG+AbEY+g2kAMp+2EtXaG3tsGxoMhEYA+uRd3o2cacQMMwRpnePH5DYg6F05mrvdHPpt+P9UR1iHaHmjBPAYeksmrdP8bS088zcnePgiL6+6N0m3+l8Krihmxg5RyWjnH18IwX4RO+xg4x3cW+zaScCDbbotDMEYtChF+Hs=
|
||||
- secure: LQHMdhaPUlCuJPFrCPpUphJSY6xzAFI/7RrcAVLtLcPhGdS+MeNifIkkAH7MeitTHroOC0dGkZ4bg/8/7bKfgwY4vPH9P50kZcnX5mI6zfBHgNYJzuthj+vJH9RAtkdQOW9Fe1uPIx8R9GUWUOVnkoJh0PQ1gDXdZW5fePqUtn1kYrcCCBE+Bhe3wz6QzTBqGS1nsVRTxQfSJNGi9uH1oi9kQGgQFuCCiJ/P0A6MIhSItkOfuggx/iorA+iASbhWkB4nXYQBbFe/ZhFJWbVfgYlOM0HtpKh8B2AqKw21Em32JoovCbUof4adkY7cH8/4Rt9SujC9YOw+a6oM+e//jJT0sie77V7zl670j+qODTuNvV4qVUwtoxShyc1Sfbd+Xb0xn/OC7DzBg97YuYCF/84yyuq12rl/cofynWE1L5YvGNSJk241XUw98Bvl0MK4VIfQvG9zJP0HnQZcWKt6kFOIEJSCRbmkd2tPPAZFBXBQf/bvpULOoKwneGJZBSapRoCyGwemM+EAzVB9UOXAqsXZ4FHkt1SSJVrTVwgxvXpCfmF6LZPhbz6nvouRWGsC/GdWjrHtdW5lEOvS27qKEL5rXwQ0o+71ZICGo8j4E0GOHXyi857qZhvO7cbOnts+iiawXiWzPXv2gGGabuqPwcU8JPEoWdaiIaeGUczfjBU=
|
||||
- secure: fvPVjj3l+TZ7HF5aGn/pmrkipGIrz+MkKNy3I7pnCJSuD/oVp9nQ5ePP/dAhaRThaW+fQbq7hOmCquPAtfoN9CUnHNV2f2l9RavDQIxdqvpXqY13A0BFffZho6A6H2kO7k6kQQPQEhl4SMJjObnX12/YDaTVx3b7aIroEJ8DyY62xGTsjExtaAksuFwUEekjh0MoWICvyBoDfrYhpiEVI2721rGMHu7FIXwmE38+jj7wwZd3Bp37yI9NY/b3ZQ/HUKyYDuoAL0xl5/GaQlRepD0v2xWQUQ40NArHLfMoscXi55UaENuswCg7rt9os8jCcZ8FkZf1cVsQ71JrE0uxgs00Jfjy2QKM5u1XUZefl1Nw5cfCDTWXIEGsz9OGiidFLehWUupX/6C6wr1BStdlRt+6Pt/FXsYHxO/qog++cKqHjOJRXi+raGAb99HhQ/hLnLUMKl5DIWlKF9DImXiOpfYxrgCJc3y91vNX6noJyWYs6PvErMukTsXFHen+fM0NtfTFoKW682oILvXjoeFvuzKpk49+rcpkJbRi5+Zdo/duSPp/flwvC4LOMi0RZOO9TNMhWKdkyWweDr1HEpvQn6RS87rpHzQwRDvm85F+PkZLMMqyWpuxBWbJf0jVbew21KvTJWamuizsIgCebFh0SSxgObzmMbAIFCkzL0PRsms=
|
||||
- ANDROID_HOME=$HOME/android-sdk
|
||||
before_install:
|
||||
- openssl aes-256-cbc -K $encrypted_53968681344a_key -iv $encrypted_53968681344a_iv -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
|
||||
install:
|
||||
# Download and unzip the Android command line tools (if not already there thanks to the cache mechanism)
|
||||
# Latest version of this file available here: https://developer.android.com/studio/#command-tools
|
||||
- if test ! -e $HOME/android-cmdline-tools/cmdline-tools.zip ; then curl https://dl.google.com/android/repository/commandlinetools-linux-6609375_latest.zip > $HOME/android-cmdline-tools/cmdline-tools.zip ; fi
|
||||
- unzip -qq -n $HOME/android-cmdline-tools/cmdline-tools.zip -d $HOME/android-cmdline-tools
|
||||
# Install or update Android SDK components (will not do anything if already up to date thanks to the cache mechanism)
|
||||
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'platform-tools' > /dev/null
|
||||
# Latest version of build-tools available here: https://developer.android.com/studio/releases/build-tools.html
|
||||
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'build-tools;29.0.3' > /dev/null
|
||||
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'platforms;android-29' > /dev/null
|
||||
script:
|
||||
- "./gradlew lintFossDebug testFossDebugUnitTest lintGoogleDebug testGoogleDebugUnitTest"
|
||||
- "./gradlew assembleRelease"
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
cache:
|
||||
directories:
|
||||
- "$HOME/.gradle/caches/"
|
||||
- "$HOME/.gradle/wrapper/"
|
||||
- "$HOME/.android/build-cache"
|
||||
- "$HOME/android-cmdline-tools"
|
||||
- "$HOME/android-sdk"
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: "XQR4GUrGkPKYVV0xMbJifX/ewKAnenBPlM/pPacQ9irAmYNYa/yEkySz4x1K6MP8cEnuJbxHFakcDqhNRCqD7Cq2NcnCi3qtTEXHK6ApLoVl/92eyiWxu/bYlidOEZb+YPcVNtTR253NiI8GYda+CrhLd4uCmsAgES+XPFJd/t2esMlDOSAp7xalZv/zFhhlB9+SevfPFMc6kkrqeHpKnMs9SK8ltVQmh3nch2KjtDvqgDW6d3nuwn7/HAer6/HY86hmA4Rh6Mo2cV6OloX0bdJ7hvA1GOT4p3+K3lWbTRxzE0o1DXAtT7+D158iKvxHFPuF3h+CTjSlLeiss6kQZL9nFjw/KhAvu+GJOp37PcMoI++mpMiFoWPlzKpp17BVKIDinYbgi8kiU4zG+QHhe2cY85SbfAplXUaysq7uzxEZwEUYHSAHNahshVooXRqvuzkthcH0/nvinfeXrzx2xDvQ3if1NENMRgttwewU0kvU61iKUwpcf/UN2bHK3DaPes0VzSH4PTHAGjoRpksDfqUwb7S8YxbYr+44aMbSPYN8Lbjda0BxPSKWwHM5/pi7FBJN1a1w3t7sV/EiACWUWr8OovmX4ljyCybbR0w9cPzRC1zAYeSUHslLXMTW2Pp9h594RnYh3q3VfeYlFCikFvuvrafwXmTkz35uhLb+2ws="
|
||||
file:
|
||||
- app/build/outputs/apk/foss/release/app-foss-release.apk
|
||||
- app/build/outputs/apk/google/release/app-google-release.apk
|
||||
on:
|
||||
repo: johan12345/EVMap
|
||||
tags: true
|
||||
skip_cleanup: 'true'
|
||||
@@ -113,7 +113,7 @@ GEM
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.4.0)
|
||||
jmespath (1.6.1)
|
||||
json (2.3.1)
|
||||
jwt (2.2.1)
|
||||
memoist (0.16.2)
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2021 Johan von Forstner
|
||||
Copyright (c) 2020-2023 Johan von Forstner and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
44
README.md
@@ -1,7 +1,8 @@
|
||||
EVMap [](https://app.travis-ci.com/johan12345/EVMap)
|
||||
EVMap [](https://github.com/ev-map/EVMap/actions)
|
||||
=====
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
|
||||
<a href="https://ev-map.app" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/></a>
|
||||
|
||||
Android app to find electric vehicle charging stations.
|
||||
|
||||
@@ -20,7 +21,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.
|
||||
@@ -28,7 +29,7 @@ Features
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
|
||||
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
|
||||
|
||||
Development setup
|
||||
-----------------
|
||||
@@ -41,9 +42,38 @@ 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` variants only use Mapbox data and should run on most Android devices, even without
|
||||
Google Play Services.
|
||||
- `fossNormal` is intended to run on smartphones and tablets, and also includes the Android
|
||||
Auto app for use on the car display (however for that to work, the Android Auto app is
|
||||
necessary, which in turn does require Google Play Services).
|
||||
- `fossAutomotive` can be installed directly on
|
||||
[Android Automotive OS (AAOS)](https://source.android.com/docs/automotive/start/what_automotive)
|
||||
headunits without Google services.
|
||||
It does not provide the usual smartphone UI, and requires an implementation of the
|
||||
[AOSP template app host](https://source.android.com/docs/automotive/hmi/aosp_host)
|
||||
to be installed. If you are an OEM and would like to distribute EVMap to your AAOS vehicles,
|
||||
please [get in touch](mailto:evmap@vonforst.net).
|
||||
- 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` can be installed directly on car infotainment systems running the
|
||||
Google-flavored Android Automotive OS (Google Automotive Services /
|
||||
["Google built-in"](https://built-in.google/cars/)).
|
||||
It does not provide the usual smartphone UI, and requires the
|
||||
[Google Automotive App Host](https://play.google.com/store/apps/details?id=com.google.android.apps.automotive.templates.host)
|
||||
to run, which should be preinstalled on those cars and can be updated through the Play Store.
|
||||
|
||||
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>
|
||||
|
||||
65
_img/app_logo.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 663.6 219.8" style="enable-background:new 0 0 663.6 219.8;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFB300;}
|
||||
.st1{fill:#90A4AE;}
|
||||
.st2{fill:#546E7A;}
|
||||
.st3{fill:#00E676;}
|
||||
.st4{fill:#FFFFFF;fill-opacity:0.2;}
|
||||
.st5{fill:#3E2723;fill-opacity:0.2;}
|
||||
.st6{opacity:0.45;enable-background:new ;}
|
||||
.st7{enable-background:new ;}
|
||||
.st8{fill:#1D1D1B;}
|
||||
</style>
|
||||
<g id="Ebene_2_1_">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0"
|
||||
d="M19.4,161.7l-4-35.1l-6.1,0.6l4,35.1L19.4,161.7z M41.2,159.1l-4-35.1l-6.1,0.6l4,35.1L41.2,159.1z" />
|
||||
<path class="st1" d="M52.6,206.9c-1.9,2.3-3.4,3.8-3.6,4c-5.5,4.4-9.9,5.7-13.5,4c-6.3-3.2-5.9-15-5.7-16.3l4.4,0.2
|
||||
c-0.2,3.4,0.4,10.6,3.4,12c1.7,0.8,4.6-0.2,8.5-3.4l0,0c0,0,12.3-12.3,9.7-22c-3-11.6,10.6-28.3,15-34l0.6-0.6l3.6,2.7l-0.6,0.8
|
||||
c-13.7,16.9-15.2,25.6-14.2,30C62.3,192.9,56.6,202,52.6,206.9z" />
|
||||
<path class="st1"
|
||||
d="M5.9,161.2l1.7,14.4l13.3,8.9l18-1.9l11-11.6l-1.7-14.4L5.9,161.2z" />
|
||||
<g>
|
||||
<path class="st2" d="M38.6,182.6l-18,1.9l3.8,15.8l14.2-1.7V182.6L38.6,182.6z M51.5,144.5l1.5,13.1l-51.5,5.9L0,150.4
|
||||
L51.5,144.5z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M91.9,0c-37,0-67,30-67,67c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6
|
||||
c6.8-72,63.2-98.4,63.2-148.9C158.8,29.8,128.8,0,91.9,0z" />
|
||||
<path class="st4" d="M91.9,1.5c36.8,0,66.5,29.6,67,66.1c0-0.2,0-0.4,0-0.6c0-37-30-67-67-67s-67,29.8-67,67c0,0.2,0,0.4,0,0.6
|
||||
C25.3,31.1,55.1,1.5,91.9,1.5L91.9,1.5z" />
|
||||
<path class="st5" d="M95.9,214.3c-0.2,2.1-1.9,3.6-4,3.6s-3.8-1.5-4-3.6c-6.5-71.8-62.5-98.2-63-148.1c0,0.4,0,0.6,0,1.1
|
||||
c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6c6.8-72,63.2-98.4,63.2-148.9c0-0.4,0-0.6,0-1.1
|
||||
C158.4,116,102.4,142.4,95.9,214.3L95.9,214.3z" />
|
||||
</g>
|
||||
<path class="st6"
|
||||
d="M76.5,34.3v40.6h11v33.2l25.8-44.4H98.5l14.8-29.6C113.4,34.3,76.5,34.3,76.5,34.3z" />
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<path class="st8"
|
||||
d="M307.9,102.9h-45.4v39.6h52.2v6.9h-60.4V52.3h60.1v6.9h-51.9v36.7h45.4V102.9z" />
|
||||
<path class="st8"
|
||||
d="M361.2,137.4l0.5,2.1l0.6-2.1l30.5-85.1h9l-36.1,97.1h-7.9l-36.1-97.1h8.9L361.2,137.4z" />
|
||||
<path class="st8" d="M427,52.4l35.8,85.7l35.9-85.7h10.9v97.1h-8.2v-42.3l0.7-43.3L466,149.5h-6.3l-36-85.3l0.7,42.7v42.5h-8.2
|
||||
V52.3H427V52.4z" />
|
||||
<path class="st8" d="M578,149.4c-0.8-2.3-1.3-5.6-1.5-10.1c-2.8,3.6-6.4,6.5-10.7,8.4c-4.3,2-8.9,3-13.8,3
|
||||
c-6.9,0-12.5-1.9-16.8-5.8s-6.4-8.8-6.4-14.7c0-7,2.9-12.6,8.8-16.7c5.8-4.1,14-6.1,24.4-6.1h14.5v-8.2c0-5.2-1.6-9.2-4.8-12.2
|
||||
s-7.8-4.4-13.9-4.4c-5.6,0-10.2,1.4-13.8,4.3c-3.6,2.8-5.5,6.3-5.5,10.3l-8-0.1c0-5.7,2.7-10.7,8-14.9s11.9-6.3,19.7-6.3
|
||||
c8,0,14.4,2,19,6s7,9.6,7.2,16.8v34.1c0,7,0.7,12.2,2.2,15.7v0.8H578V149.4z M552.9,143.7c5.3,0,10.1-1.3,14.3-3.9
|
||||
s7.3-6,9.2-10.3v-15.9h-14.3c-8,0.1-14.2,1.5-18.7,4.4c-4.5,2.8-6.7,6.7-6.7,11.6c0,4,1.5,7.4,4.5,10.1S548.1,143.7,552.9,143.7z
|
||||
" />
|
||||
<path class="st8" d="M663.6,114.1c0,11.2-2.5,20.2-7.5,26.8s-11.6,9.9-20,9.9c-9.9,0-17.4-3.5-22.7-10.4v36.8h-7.9V77.3h7.4
|
||||
l0.4,10.2C618.5,79.8,626,76,635.9,76c8.6,0,15.4,3.3,20.3,9.8c4.9,6.5,7.4,15.6,7.4,27.2V114.1z M655.6,112.7
|
||||
c0-9.2-1.9-16.5-5.7-21.8s-9-8-15.8-8c-4.9,0-9.1,1.2-12.6,3.5c-3.5,2.4-6.2,5.8-8.1,10.3v34.6c1.9,4.1,4.6,7.3,8.2,9.5
|
||||
c3.6,2.2,7.8,3.3,12.6,3.3c6.7,0,11.9-2.7,15.7-8C653.7,130.6,655.6,122.9,655.6,112.7z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
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,23 +1,25 @@
|
||||
<svg id="Ebene_5" data-name="Ebene 5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style>.cls-1,.cls-2{fill:none;}.cls-2{stroke:#000;stroke-miterlimit:10;stroke-width:2px;}
|
||||
</style>
|
||||
</defs>
|
||||
<title>connector_supercharger</title>
|
||||
<path class="cls-1" d="M12,12H36V36H12Z" />
|
||||
<path class="cls-2"
|
||||
d="M13.45,17.08a8.24,8.24,0,0,1-3.11.6,8.34,8.34,0,0,1-6-14.18H16.3a8.35,8.35,0,0,1,1.07,10.33" />
|
||||
<circle cx="10.34" cy="9.34" r="1.67" />
|
||||
<circle cx="15.35" cy="9.34" r="1.67" />
|
||||
<circle cx="12.84" cy="13.51" r="1.67" />
|
||||
<circle cx="7.84" cy="13.51" r="1.67" />
|
||||
<circle cx="5.34" cy="9.34" r="1.67" />
|
||||
<circle cx="7.84" cy="5.59" r="1" />
|
||||
<circle cx="12.84" cy="5.59" r="1.04" />
|
||||
<?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_5" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24"
|
||||
style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;fill:none;}
|
||||
.st1{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<path class="st0" d="M12,12h24v24H12V12z" />
|
||||
<path class="st1"
|
||||
d="M6.2,13.8C4.1,10.6,4.6,6.3,7.3,3.5h12c1.5,1.6,2.4,3.7,2.4,5.9c0,4.6-3.8,8.3-8.4,8.3c-1.1,0-2.1-0.2-3.1-0.6" />
|
||||
<circle cx="13.3" cy="9.3" r="1.7" />
|
||||
<circle cx="8.3" cy="9.3" r="1.7" />
|
||||
<circle cx="10.8" cy="13.5" r="1.7" />
|
||||
<circle cx="15.8" cy="13.5" r="1.7" />
|
||||
<circle cx="18.3" cy="9.3" r="1.7" />
|
||||
<circle cx="15.8" cy="5.6" r="1" />
|
||||
<circle cx="10.8" cy="5.6" r="1" />
|
||||
<g id="T">
|
||||
<path id="path35"
|
||||
d="M18.18,22.23l1-5.48c.93,0,1.22.1,1.27.52a2.15,2.15,0,0,0,.93-.7,6.91,6.91,0,0,0-2.46-.6l-.71.88h0L17.46,16a7,7,0,0,0-2.46.6,2.22,2.22,0,0,0,.94.7c0-.42.33-.52,1.26-.52l1,5.48" />
|
||||
<path id="path37"
|
||||
d="M18.18,15.72a7.9,7.9,0,0,1,3.28.66,2.65,2.65,0,0,0,.2-.4,9.24,9.24,0,0,0-7,0,2.61,2.61,0,0,0,.19.4,7.94,7.94,0,0,1,3.29-.66h0" />
|
||||
</g>
|
||||
</svg>
|
||||
<path id="path35" d="M5.4,22.3l1-5.5c0.9,0,1.3,0.1,1.3,0.5c0.4-0.1,0.7-0.4,0.9-0.7C7.8,16.3,7,16,6.1,16l-0.8,0.8l0,0L4.7,16
|
||||
c-0.8,0-1.7,0.3-2.5,0.6c0.2,0.3,0.6,0.6,0.9,0.7c0.1-0.4,0.3-0.5,1.3-0.5L5.4,22.3" />
|
||||
<path id="path37" d="M5.5,15.7L5.5,15.7c1.1,0,2.3,0.2,3.3,0.7c0.1-0.1,0.1-0.3,0.2-0.4c-2.2-0.9-4.8-0.9-7,0
|
||||
c0.1,0.1,0.1,0.3,0.2,0.4C3.2,15.9,4.3,15.7,5.5,15.7" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB 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 |
64
_img/powered_by_fronyx.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 566.9 254.9" style="enable-background:new 0 0 566.9 254.9;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#000044;}
|
||||
</style>
|
||||
<path class="st0" d="M60.8,86.3c-5.6,0-10,0.5-13.3,1.4c-3.3,0.9-5.7,2.7-7.1,5.2c-0.4,0.6-0.7,1.3-0.9,2c-1.1,3.3-1.4,6.8-1.4,10.2
|
||||
c0,2.1,0,4.2,0,6.2c0,0.1,0,0.2,0,0.3h22.3v20.9H38.1v71.2v7.6H14.5v-7.6v-71.2H0v-20.9h14.5V104c0-13.4,3.9-23.8,11.2-31.3
|
||||
c7.3-7.4,19-10.8,35.1-10V86.3 M157.1,161.2c0-15.5,11.2-27.3,25.7-27.3c14.5,0,25.5,11.8,25.5,27.3c0,15.6-11,27.3-25.5,27.3
|
||||
C168.3,188.5,157.1,176.9,157.1,161.2 M182.8,110.1c-27.9,0-49.7,22.2-49.7,51.1c0,29.1,21.8,51.3,49.7,51.3
|
||||
c27.9,0,49.4-22.2,49.4-51.3C232.1,132.3,210.7,110.1,182.8,110.1 M541.4,161.5c14.1,0,25.6-11.5,25.6-25.6
|
||||
c0-14.1-11.5-25.6-25.6-25.6c-14.1,0-25.6,11.5-25.6,25.6C515.8,150,527.2,161.5,541.4,161.5 M129.6,110.5c-2.2-0.2-4.3-0.4-6.6-0.4
|
||||
c-4.4,0-9.1,0.6-13.9,1.9c-4.8,1.3-8.5,3.4-10.7,6.5v-7H74.7V211h23.8v-58.2c0-5.4,1.4-9.5,4.1-12.3c4.4-4.5,10.6-6.5,16.7-6.8
|
||||
c1.7-0.1,3.5,0,5.3,0.1c1.6,0.2,3.2,0.5,4.7,0.7c0-1,0-2,0-3c0-2,0-3.9,0-5.9c0-2.2,0-4.5,0-6.7c0-1.9,0.2-3.8,0.2-5.7
|
||||
c0-0.9,0.2-1.7,0.2-2.6C129.6,110.6,129.6,110.5,129.6,110.5z M475.8,160.6l29.7-49h-28.7l-16.3,31.7l-16.5-31.7H415l30.1,49
|
||||
l-30.7,50.6h28.7l17.3-33.1l17.7,33.1h28.7L475.8,160.6z M356.9,254.8c14.9,0.5,26.7-3.1,34.7-10.9c7.6-7.4,11.6-18.1,12-33l0,0
|
||||
v-2.4v-97.1h-24.5c0,0,0,14.3,0,28.8v14.9v0.1v14.5c0,5.3-1.2,9.7-5.8,13.8c-4.3,3.9-10.9,5.1-15.4,5.1c-2.7,0-10.7-0.9-15.4-7.5
|
||||
c-2.4-3.4-3.9-9.1-4.2-14c-0.3-5-0.3-7.4-0.6-14.7c-1.1-30.2-26-47.6-55-40.3c-5,1.3-9,3.4-11.2,6.5v-7.2h-23.8V211h23.8v-52v-6
|
||||
c0-5.4,1.5-9.5,4.3-12.3c2.8-2.7,5.8-4.2,9.5-5.1c3.7-0.9,6.6-1,8.5-1c2.9,0,6.5,0.8,8,1.2c2.8,0.8,6.3,3.3,8.3,6.2
|
||||
c3.7,5.3,3.3,12.8,3.5,18.9c0.2,7.6,0.4,15.3,2.7,22.7c3.2,10.3,9.7,20,19.7,24.6c9,4.1,20,5.2,29.7,3.4c3-0.6,6.3-1.5,9.1-3
|
||||
c1.6-0.9,3.2-1.9,4.5-3.2c0.4,10.7,0.6,17.6-6,22.5c-1.8,1.4-4.3,2.5-6.5,2.9c-3.1,0.6-6.5,1-9.7,0.9L356.9,254.8z" />
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M97.6,89.8V39.2c0-1.6-0.1-3.2-0.2-4.8c-0.1-1.7-0.3-3.3-0.5-4.9h6.6l0.9,10h-1c0.9-3.3,2.7-5.9,5.5-7.9
|
||||
c2.7-1.9,6-2.9,9.8-2.9c3.8,0,7.1,0.9,9.9,2.6c2.8,1.7,4.9,4.2,6.5,7.5c1.6,3.2,2.4,7.2,2.4,11.8c0,4.5-0.8,8.4-2.3,11.7
|
||||
c-1.5,3.2-3.7,5.8-6.5,7.5c-2.8,1.8-6.1,2.6-9.9,2.6c-3.8,0-7-1-9.7-2.9c-2.7-1.9-4.6-4.5-5.5-7.8h0.9v28.1H97.6z M117.3,66.8
|
||||
c4,0,7.2-1.4,9.6-4.2c2.4-2.8,3.6-6.8,3.6-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2
|
||||
c-2.4,2.8-3.6,6.8-3.6,12.2c0,5.3,1.2,9.4,3.6,12.1C110.2,65.4,113.4,66.8,117.3,66.8z" />
|
||||
<path class="st0" d="M165.4,72.4c-4.1,0-7.6-0.9-10.6-2.6c-3-1.8-5.3-4.3-6.9-7.6c-1.6-3.3-2.4-7.2-2.4-11.6
|
||||
c0-4.5,0.8-8.4,2.4-11.7c1.6-3.2,3.9-5.8,6.9-7.5c3-1.8,6.5-2.6,10.5-2.6c4.1,0,7.6,0.9,10.6,2.6c3,1.8,5.3,4.3,7,7.5
|
||||
c1.7,3.2,2.5,7.1,2.5,11.7c0,4.5-0.8,8.4-2.5,11.6c-1.7,3.3-4,5.8-7,7.6C172.9,71.5,169.4,72.4,165.4,72.4z M165.4,66.8
|
||||
c4,0,7.1-1.4,9.5-4.2c2.4-2.8,3.5-6.8,3.5-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.5-4.2c-4,0-7.1,1.4-9.5,4.2
|
||||
c-2.4,2.8-3.5,6.8-3.5,12.2c0,5.3,1.2,9.4,3.5,12.1C158.2,65.4,161.4,66.8,165.4,66.8z" />
|
||||
<path class="st0" d="M207.2,71.6l-15.6-42.2h7.2l13,37.1h-2.2l13.4-37.1h5.9l13.2,37.1h-2.1l13.1-37.1h6.9l-15.7,42.2h-6.6
|
||||
l-13.6-37.7h3.4l-13.7,37.7H207.2z" />
|
||||
<path class="st0" d="M287.7,72.4c-6.6,0-11.8-1.9-15.6-5.8c-3.8-3.8-5.7-9.2-5.7-16c0-4.4,0.8-8.3,2.5-11.5c1.7-3.3,4-5.8,7.1-7.6
|
||||
c3-1.8,6.5-2.7,10.4-2.7c3.9,0,7.1,0.8,9.7,2.4c2.6,1.6,4.6,3.9,6,6.9c1.4,3,2.1,6.5,2.1,10.6v2.5h-32.7v-4.3h28.2l-1.4,1.1
|
||||
c0-4.5-1-8-3-10.5c-2-2.5-5-3.8-9-3.8c-4.2,0-7.5,1.5-9.8,4.4C274.2,41.1,273,45,273,50v0.8c0,5.3,1.3,9.3,3.9,12
|
||||
c2.6,2.7,6.3,4.1,11,4.1c2.5,0,4.9-0.4,7.1-1.1c2.2-0.8,4.3-2,6.3-3.7l2.4,4.8c-1.8,1.8-4.2,3.2-7,4.2
|
||||
C293.8,71.9,290.8,72.4,287.7,72.4z" />
|
||||
<path class="st0" d="M314.5,71.6v-32c0-1.7,0-3.4-0.1-5.1c-0.1-1.7-0.2-3.4-0.4-5h6.6l0.8,10.2l-1.2,0.1c0.6-2.5,1.5-4.6,2.9-6.2
|
||||
c1.4-1.6,3.1-2.8,5-3.7c1.9-0.8,3.9-1.2,6-1.2c0.8,0,1.6,0,2.2,0.1c0.6,0.1,1.2,0.2,1.8,0.4l-0.1,6c-0.8-0.3-1.6-0.5-2.3-0.5
|
||||
s-1.5-0.1-2.4-0.1c-2.5,0-4.6,0.6-6.4,1.8c-1.8,1.2-3.2,2.7-4.1,4.5s-1.4,3.8-1.4,5.9v24.9H314.5z" />
|
||||
<path class="st0" d="M363.9,72.4c-6.6,0-11.8-1.9-15.6-5.8c-3.8-3.8-5.7-9.2-5.7-16c0-4.4,0.8-8.3,2.5-11.5c1.7-3.3,4-5.8,7.1-7.6
|
||||
c3-1.8,6.5-2.7,10.4-2.7c3.9,0,7.1,0.8,9.7,2.4c2.6,1.6,4.6,3.9,6,6.9c1.4,3,2.1,6.5,2.1,10.6v2.5h-32.7v-4.3H376l-1.4,1.1
|
||||
c0-4.5-1-8-3-10.5c-2-2.5-5-3.8-9-3.8c-4.2,0-7.5,1.5-9.8,4.4c-2.4,2.9-3.5,6.9-3.5,11.9v0.8c0,5.3,1.3,9.3,3.9,12
|
||||
c2.6,2.7,6.3,4.1,11,4.1c2.5,0,4.9-0.4,7.1-1.1c2.2-0.8,4.3-2,6.3-3.7l2.4,4.8c-1.8,1.8-4.2,3.2-7,4.2
|
||||
C370,71.9,367,72.4,363.9,72.4z" />
|
||||
<path class="st0" d="M406.8,72.4c-3.7,0-6.9-0.9-9.7-2.6c-2.8-1.8-5-4.3-6.5-7.5c-1.5-3.2-2.3-7.1-2.3-11.7
|
||||
c0-4.6,0.8-8.5,2.3-11.8c1.5-3.2,3.7-5.7,6.5-7.5c2.8-1.7,6-2.6,9.7-2.6c3.8,0,7.1,1,9.9,2.9c2.8,1.9,4.6,4.5,5.6,7.7h-1V9.8h6.8
|
||||
v61.8h-6.7V61.5h0.9c-0.9,3.4-2.7,6-5.5,7.9C413.9,71.4,410.6,72.4,406.8,72.4z M408.2,66.8c4,0,7.2-1.4,9.6-4.2
|
||||
c2.4-2.8,3.6-6.8,3.6-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2c-2.4,2.8-3.6,6.8-3.6,12.2
|
||||
c0,5.3,1.2,9.4,3.6,12.1C401.1,65.4,404.3,66.8,408.2,66.8z" />
|
||||
<path class="st0" d="M484.3,72.4c-3.8,0-7.1-1-9.8-2.9c-2.7-1.9-4.6-4.6-5.5-7.9h0.9v10.1h-6.7V9.8h6.8v29.5h-1
|
||||
c1-3.2,2.8-5.8,5.5-7.7c2.7-1.9,6-2.9,9.8-2.9c3.8,0,7.1,0.9,9.9,2.6c2.8,1.8,4.9,4.3,6.5,7.5c1.5,3.2,2.3,7.1,2.3,11.7
|
||||
s-0.8,8.4-2.4,11.7c-1.6,3.2-3.7,5.8-6.5,7.5C491.4,71.5,488.1,72.4,484.3,72.4z M482.9,66.8c4,0,7.2-1.4,9.6-4.1
|
||||
c2.4-2.7,3.6-6.8,3.6-12.2s-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2c-2.4,2.8-3.6,6.8-3.6,12.2
|
||||
c0,5.3,1.2,9.4,3.6,12.1C475.8,65.4,478.9,66.8,482.9,66.8z" />
|
||||
<path class="st0" d="M511.9,90.7l-1.6-5.6c2.6-0.6,4.8-1.3,6.6-2.1c1.8-0.8,3.2-1.9,4.4-3.2c1.2-1.3,2.2-3,3-5l2.2-5l-0.2,2.9
|
||||
l-18.4-43.1h7.4l15.2,37h-2.2l15-37h7.1L531,75.1c-1.1,2.7-2.4,4.9-3.7,6.8c-1.3,1.8-2.8,3.3-4.3,4.5c-1.5,1.1-3.2,2.1-5.1,2.7
|
||||
S514.1,90.3,511.9,90.7z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 872 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 875 KiB After Width: | Height: | Size: 886 KiB |
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 872 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1005 KiB |
|
Before Width: | Height: | Size: 841 KiB After Width: | Height: | Size: 848 KiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 864 KiB After Width: | Height: | Size: 884 KiB |
|
Before Width: | Height: | Size: 841 KiB After Width: | Height: | Size: 848 KiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 114 KiB |
185
app/build.gradle
@@ -1,33 +1,43 @@
|
||||
plugins {
|
||||
id 'com.adarshr.test-logger' version '3.1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'pt.jcosta.resourceplaceholders'
|
||||
|
||||
def supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro"
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
versionCode 68
|
||||
versionName "1.2.0"
|
||||
targetSdkVersion 33
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 194
|
||||
versionName "1.6.7"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs supportedLocales.split(',')
|
||||
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
def isRunningOnTravis = System.getenv("CI") == "true"
|
||||
if (isRunningOnTravis) {
|
||||
def isRunningOnCI = System.getenv("CI") == "true"
|
||||
if (isRunningOnCI) {
|
||||
// configure keystore
|
||||
storeFile = file("../_ci/keystore.jks")
|
||||
storePassword = System.getenv("keystore_password")
|
||||
keyAlias = System.getenv("keystore_alias")
|
||||
keyPassword = System.getenv("keystore_alias_password")
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEYSTORE_ALIAS")
|
||||
keyPassword = System.getenv("KEYSTORE_ALIAS_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +54,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "dependencies"
|
||||
flavorDimensions "dependencies", "automotive"
|
||||
productFlavors {
|
||||
foss {
|
||||
dimension "dependencies"
|
||||
@@ -53,6 +63,15 @@ android {
|
||||
dimension "dependencies"
|
||||
versionNameSuffix "-google"
|
||||
}
|
||||
normal {
|
||||
dimension "automotive"
|
||||
}
|
||||
automotive {
|
||||
dimension "automotive"
|
||||
versionNameSuffix "-automotive"
|
||||
versionCode defaultConfig.versionCode + 1
|
||||
minSdkVersion 29
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -65,10 +84,29 @@ android {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs).configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding true
|
||||
}
|
||||
lint {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
warning 'MissingTranslation'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
|
||||
resourcePlaceholders {
|
||||
files = ['xml/shortcuts.xml']
|
||||
}
|
||||
namespace 'net.vonforst.evmap'
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
applicationVariants.all { variant ->
|
||||
@@ -85,7 +123,7 @@ android {
|
||||
variant.resValue "string", "openchargemap_key", openchargemapKey
|
||||
}
|
||||
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
|
||||
if (googleMapsKey != null && variant.flavorName == 'google') {
|
||||
if (googleMapsKey != null && variant.flavorName.startsWith('google')) {
|
||||
variant.resValue "string", "google_maps_key", googleMapsKey
|
||||
}
|
||||
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
|
||||
@@ -102,60 +140,87 @@ android {
|
||||
if (chargepriceKey != null) {
|
||||
variant.resValue "string", "chargeprice_key", chargepriceKey
|
||||
}
|
||||
def fronyxKey = env.FRONYX_API_KEY ?: project.findProperty("FRONYX_API_KEY")
|
||||
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
|
||||
fronyxKey = decode(project.findProperty("FRONYX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (fronyxKey != null) {
|
||||
variant.resValue "string", "fronyx_key", fronyxKey
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
packagingOptions {
|
||||
pickFirst 'lib/x86/libc++_shared.so'
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
googleNormalImplementation {}
|
||||
googleAutomotiveImplementation {}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation "androidx.activity:activity-ktx:1.7.2"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.7"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.5.0-rc01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'androidx.browser:browser:1.5.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||
implementation 'com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
||||
implementation 'com.squareup.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 'io.coil-kt:coil:1.1.0'
|
||||
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.11.0'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.15.0'
|
||||
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
|
||||
implementation 'io.coil-kt:coil:2.4.0'
|
||||
implementation 'com.github.ev-map: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-alpha02'
|
||||
googleImplementation 'androidx.car.app:app-projected:1.2.0-alpha02'
|
||||
def carAppVersion = '1.3.0-rc01'
|
||||
implementation "androidx.car.app:app:$carAppVersion"
|
||||
normalImplementation "androidx.car.app:app-projected:$carAppVersion"
|
||||
automotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '751daec281'
|
||||
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.1'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
def anyMapsVersion = '8f1226e1c5'
|
||||
implementation "com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
|
||||
implementation("com.github.ev-map.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'
|
||||
}
|
||||
// original version of mapbox-android-core
|
||||
googleImplementation 'com.mapbox.mapboxsdk:mapbox-android-core:2.0.1'
|
||||
// patched version that removes build-time dependency on GMS (-> no Google location services)
|
||||
fossImplementation 'com.github.ev-map:mapbox-events-android:a21c324501'
|
||||
|
||||
// Google Places
|
||||
implementation 'com.google.android.libraries.places:places:2.5.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
|
||||
googleImplementation 'com.google.android.libraries.places:places:3.1.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
|
||||
@@ -165,18 +230,19 @@ dependencies {
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// viewmodel library
|
||||
def lifecycle_version = "2.4.0"
|
||||
def lifecycle_version = "2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.4.0"
|
||||
def room_version = "2.5.1"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
implementation 'com.github.anboralabs:spatia-room:0.2.7'
|
||||
|
||||
// billing library
|
||||
def billing_version = "4.0.0"
|
||||
def billing_version = "6.0.0"
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
@@ -187,18 +253,31 @@ 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'
|
||||
debugImplementation 'com.facebook.flipper:flipper:0.190.0'
|
||||
debugImplementation 'com.facebook.soloader:soloader:0.10.5'
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.190.0'
|
||||
|
||||
// testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.11.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.9.2'
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.2.0'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
|
||||
// testing for car app
|
||||
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.9.2'
|
||||
testGoogleImplementation 'androidx.test:core:1.5.0'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.arch.core:core-testing:2.2.0'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.15.0"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
}
|
||||
|
||||
private static String decode(String s, String key) {
|
||||
@@ -211,4 +290,4 @@ private static byte[] xorWithKey(byte[] a, byte[] key) {
|
||||
out[i] = (byte) (a[i] ^ key[i%key.length]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.johan.evmap
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.johan.evmap", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.johan.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import co.anbora.labs.spatia.geometry.Mbr
|
||||
import co.anbora.labs.spatia.geometry.MultiPolygon
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.SavedRegion
|
||||
import net.vonforst.evmap.storage.SavedRegionDao
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.viewmodel.await
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SavedRegionDaoTest {
|
||||
private lateinit var database: AppDatabase
|
||||
private lateinit var dao: SavedRegionDao
|
||||
|
||||
@get:Rule
|
||||
var instantExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = AppDatabase.createInMemory(context)
|
||||
dao = database.savedRegionDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSavedRegion() {
|
||||
val ds = "test"
|
||||
|
||||
val ts1 = ZonedDateTime.of(2023, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant()
|
||||
val region1 = Mbr(9.0, 53.0, 10.0, 54.0, 4326).asPolygon()
|
||||
runBlocking {
|
||||
dao.insert(
|
||||
SavedRegion(
|
||||
region1,
|
||||
ds, ts1, null, false
|
||||
)
|
||||
)
|
||||
}
|
||||
assertEquals(region1, dao.getSavedRegion(ds, 0))
|
||||
runBlocking {
|
||||
assertTrue(dao.savedRegionCovers(53.1, 53.2, 9.1, 9.2, ds, 0).await())
|
||||
assertTrue(dao.savedRegionCoversRadius(53.05, 9.15, 10.0, ds, 0).await())
|
||||
assertFalse(dao.savedRegionCovers(52.1, 52.2, 9.1, 9.2, ds, 0).await())
|
||||
}
|
||||
|
||||
val ts2 = ZonedDateTime.of(2023, 1, 1, 1, 0, 0, 0, ZoneOffset.UTC).toInstant()
|
||||
val region2 = Mbr(9.0, 55.0, 10.0, 56.0, 4326).asPolygon()
|
||||
runBlocking {
|
||||
dao.insert(
|
||||
SavedRegion(
|
||||
region2,
|
||||
ds, ts2, null, false
|
||||
)
|
||||
)
|
||||
}
|
||||
assertEquals(MultiPolygon(listOf(region1, region2)), dao.getSavedRegion(ds, 0))
|
||||
assertEquals(region2, dao.getSavedRegion(ds, ts1.toEpochMilli()))
|
||||
|
||||
runBlocking {
|
||||
assertTrue(dao.savedRegionCovers(53.1, 53.2, 9.1, 9.2, ds, 0).await())
|
||||
assertTrue(dao.savedRegionCoversRadius(53.05, 9.15, 10.0, ds, 0).await())
|
||||
assertFalse(dao.savedRegionCovers(53.1, 55.2, 9.1, 9.2, ds, 0).await())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMakeCircle() {
|
||||
val lat = 53.0
|
||||
val lng = 10.0
|
||||
val radius = 10000.0
|
||||
val circle = runBlocking { dao.makeCircle(lat, lng, radius) }
|
||||
for (point in circle.points) {
|
||||
assertEquals(radius, distanceBetween(lat, lng, point.y, point.x), 10.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
app/src/automotive/AndroidManifest.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.car.permission.CAR_INFO" />
|
||||
<uses-permission android:name="android.car.permission.CAR_ENERGY" />
|
||||
<uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS" />
|
||||
<uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS" />
|
||||
<uses-permission android:name="android.car.permission.CAR_SPEED" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.type.automotive"
|
||||
android:required="true" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.car.templates_host"
|
||||
android:required="true" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
tools:replace="required"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.landscape"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="com.android.automotive"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
tools:node="remove" />
|
||||
|
||||
<activity
|
||||
android:name=".MapsActivity"
|
||||
tools:node="remove" />
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
|
||||
android:name="androidx.car.app.activity.CarAppActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="net.vonforst.evmap" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="distractionOptimized"
|
||||
android:value="true" />
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
5
app/src/automotive/res/values-de/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Zulassen</string>
|
||||
<string name="auto_location_permission_needed">Um EVMap auf deinem Fahrzeug zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
|
||||
</resources>
|
||||
5
app/src/automotive/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/automotive/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>
|
||||
5
app/src/automotive/res/values-nl/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Toestaan</string>
|
||||
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
|
||||
</resources>
|
||||
5
app/src/automotive/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Permitir</string>
|
||||
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
|
||||
</resources>
|
||||
2
app/src/automotive/res/values-ro/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
5
app/src/automotive/res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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>
|
||||
9
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||
android:exported="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
42
app/src/debug/java/net/vonforst/evmap/DebugInits.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
private val networkFlipperPlugin = NetworkFlipperPlugin()
|
||||
|
||||
fun addDebugInterceptors(context: Context) {
|
||||
if (Build.FINGERPRINT == "robolectric") return
|
||||
|
||||
SoLoader.init(context, false)
|
||||
val client = AndroidFlipperClient.getInstance(context)
|
||||
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
|
||||
client.addPlugin(networkFlipperPlugin)
|
||||
client.addPlugin(DatabasesFlipperPlugin(context))
|
||||
client.addPlugin(SharedPreferencesFlipperPlugin(context))
|
||||
client.start()
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
||||
// Flipper does not work during unit tests - so check whether we are running tests first
|
||||
var isRunningTest = true
|
||||
try {
|
||||
Class.forName("org.junit.Test")
|
||||
} catch (e: ClassNotFoundException) {
|
||||
isRunningTest = false
|
||||
}
|
||||
|
||||
if (!isRunningTest) {
|
||||
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
|
||||
}
|
||||
return this
|
||||
}
|
||||
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,16 +0,0 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
class OnboardingViewPagerAdapter(fragment: Fragment) :
|
||||
FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> WelcomeFragment()
|
||||
1 -> IconsFragment()
|
||||
2 -> DataSourceSelectFragment()
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
6
app/src/foss/res/values-de/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
|
||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
@@ -1,14 +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_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.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
|
||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||
</resources>
|
||||
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>
|
||||
6
app/src/foss/res/values-nl/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
|
||||
<string name="donate_paypal">Doneer via PayPal</string>
|
||||
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
6
app/src/foss/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap (Mapbox).</string>
|
||||
<string name="donate_paypal">Doar com o PayPal</string>
|
||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
|
||||
</resources>
|
||||
6
app/src/foss/res/values-ro/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Crezi ca EVMap este util? Sprijina dezvoltarea printr-o donatie pentru dezvoltator.</string>
|
||||
<string name="donate_paypal">Doneaza cu PayPal</string>
|
||||
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap (Mapbox).</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>
|
||||
5
app/src/foss/res/values/donottranslate.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
</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,20 +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>
|
||||
</resources>
|
||||
@@ -1,52 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="net.vonforst.evmap">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.car.app,androidx.car.app.projected" />
|
||||
|
||||
<queries>
|
||||
<package android:name="com.google.android.projection.gearhead" />
|
||||
</queries>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="@string/google_maps_key" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.car.app.theme"
|
||||
android:resource="@style/CarAppTheme" />
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.car.app.minCarApiLevel"
|
||||
android:value="1" />
|
||||
|
||||
<service
|
||||
android:name=".auto.CarAppService"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="androidx.car.app.CarAppService"
|
||||
android:category="androidx.car.app.category.CHARGING" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".auto.CarLocationService"
|
||||
android:foregroundServiceType="location"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.location.Location
|
||||
import android.os.IBinder
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.common.CarValue
|
||||
import androidx.car.app.hardware.info.CarHardwareLocation
|
||||
import androidx.car.app.hardware.info.CarSensors
|
||||
import androidx.car.app.validation.HostValidator
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
|
||||
|
||||
interface LocationAwareScreen {
|
||||
fun updateLocation(location: Location)
|
||||
}
|
||||
|
||||
class CarAppService : androidx.car.app.CarAppService() {
|
||||
override fun createHostValidator(): HostValidator {
|
||||
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
|
||||
HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
|
||||
} else {
|
||||
HostValidator.Builder(applicationContext)
|
||||
.addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateSession(): Session {
|
||||
return EVMapSession(this)
|
||||
}
|
||||
}
|
||||
|
||||
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
var mapScreen: LocationAwareScreen? = null
|
||||
set(value) {
|
||||
field = value
|
||||
location?.let { value?.updateLocation(it) }
|
||||
}
|
||||
private var location: Location? = null
|
||||
private var locationService: CarLocationService? = null
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
}
|
||||
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, ibinder: IBinder) {
|
||||
val binder: CarLocationService.LocalBinder = ibinder as CarLocationService.LocalBinder
|
||||
locationService = binder.service
|
||||
locationService?.requestLocationUpdates()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
locationService = null
|
||||
}
|
||||
}
|
||||
private var serviceBound = false
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
return WelcomeScreen(carContext, this)
|
||||
}
|
||||
|
||||
fun locationPermissionGranted() = carContext.checkAnyLocationPermission()
|
||||
|
||||
private val locationReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val location = intent.getParcelableExtra(CarLocationService.EXTRA_LOCATION) as Location?
|
||||
updateLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLocation(location: Location?) {
|
||||
val mapScreen = mapScreen
|
||||
if (location != null && mapScreen != null) {
|
||||
mapScreen.updateLocation(location)
|
||||
}
|
||||
this.location = location
|
||||
}
|
||||
|
||||
private fun onCarHardwareLocationReceived(loc: CarHardwareLocation) {
|
||||
if (loc.location.status == CarValue.STATUS_SUCCESS && loc.location.value != null) {
|
||||
updateLocation(loc.location.value)
|
||||
|
||||
// we successfully received a location from the car hardware,
|
||||
// so we don't need the smartphone location anymore.
|
||||
unbindLocationService()
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun bindLocationService() {
|
||||
if (!locationPermissionGranted()) return
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carSensors.addCarHardwareLocationListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
exec,
|
||||
::onCarHardwareLocationReceived
|
||||
)
|
||||
}
|
||||
serviceBound = cas.bindService(
|
||||
Intent(cas, CarLocationService::class.java),
|
||||
serviceConnection,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
private fun onStop() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
hardwareMan.carSensors.removeCarHardwareLocationListener(::onCarHardwareLocationReceived)
|
||||
}
|
||||
unbindLocationService()
|
||||
}
|
||||
|
||||
private fun unbindLocationService() {
|
||||
locationService?.removeLocationUpdates()
|
||||
if (serviceBound) {
|
||||
cas.unbindService(serviceConnection)
|
||||
serviceBound = false
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
private fun registerBroadcastReceiver() {
|
||||
LocalBroadcastManager.getInstance(cas).registerReceiver(
|
||||
locationReceiver,
|
||||
IntentFilter(CarLocationService.ACTION_BROADCAST)
|
||||
);
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
private fun unregisterBroadcastReceiver() {
|
||||
LocalBroadcastManager.getInstance(cas).unregisterReceiver(locationReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.location.Location
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.google.android.gms.location.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
|
||||
class CarLocationService : Service() {
|
||||
private lateinit var serviceHandler: Handler
|
||||
private lateinit var locationRequest: LocationRequest
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
private lateinit var locationCallback: LocationCallback
|
||||
private lateinit var fusedLocationClient: FusedLocationProviderClient
|
||||
private val binder: IBinder = LocalBinder(this)
|
||||
private var location: Location? = null
|
||||
|
||||
private val UPDATE_INTERVAL_IN_MILLISECONDS = 10000L
|
||||
private val FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = UPDATE_INTERVAL_IN_MILLISECONDS / 2
|
||||
|
||||
private val CHANNEL_ID = "car_location"
|
||||
private val NOTIFICATION_ID = 1000
|
||||
private val TAG = "CarLocationService"
|
||||
|
||||
companion object {
|
||||
const val ACTION_BROADCAST: String = BuildConfig.APPLICATION_ID + ".car_location_broadcast"
|
||||
const val EXTRA_LOCATION: String = BuildConfig.APPLICATION_ID + ".location"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
|
||||
locationCallback = object : LocationCallback() {
|
||||
override fun onLocationResult(locationResult: LocationResult) {
|
||||
super.onLocationResult(locationResult)
|
||||
onNewLocation(locationResult.lastLocation)
|
||||
}
|
||||
}
|
||||
createLocationRequest()
|
||||
getLastLocation()
|
||||
val handlerThread = HandlerThread(TAG)
|
||||
handlerThread.start()
|
||||
serviceHandler = Handler(handlerThread.looper)
|
||||
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Android O requires a Notification Channel.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name: CharSequence = getString(R.string.app_name)
|
||||
// Create the channel for the notification
|
||||
val mChannel =
|
||||
NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
|
||||
// Set the Notification Channel for the Notification Manager.
|
||||
notificationManager.createNotificationChannel(mChannel)
|
||||
}
|
||||
|
||||
startForeground(NOTIFICATION_ID, getNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [NotificationCompat] used as part of the foreground service.
|
||||
*/
|
||||
private fun getNotification(): Notification {
|
||||
val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentText(getString(R.string.auto_location_service))
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setTicker(getString(R.string.auto_location_service))
|
||||
.setWhen(System.currentTimeMillis())
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
private fun createLocationRequest() {
|
||||
locationRequest = LocationRequest()
|
||||
locationRequest.interval = UPDATE_INTERVAL_IN_MILLISECONDS
|
||||
locationRequest.fastestInterval = FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS
|
||||
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
|
||||
}
|
||||
|
||||
private fun onNewLocation(location: Location) {
|
||||
Log.i(TAG, "New location: $location")
|
||||
this.location = location
|
||||
|
||||
// Notify anyone listening for broadcasts about the new location.
|
||||
val intent = Intent(ACTION_BROADCAST)
|
||||
intent.putExtra(EXTRA_LOCATION, location)
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun getLastLocation() {
|
||||
try {
|
||||
fusedLocationClient.lastLocation
|
||||
.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful && task.result != null) {
|
||||
location = task.result
|
||||
} else {
|
||||
Log.w(TAG, "Failed to get location.")
|
||||
}
|
||||
}
|
||||
} catch (unlikely: SecurityException) {
|
||||
Log.e(TAG, "Lost location permission.$unlikely")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request for location updates. Note that in this sample we merely log the
|
||||
* [SecurityException].
|
||||
*/
|
||||
fun requestLocationUpdates() {
|
||||
Log.i(TAG, "Requesting location updates")
|
||||
startService(Intent(applicationContext, CarLocationService::class.java))
|
||||
try {
|
||||
fusedLocationClient.requestLocationUpdates(
|
||||
locationRequest,
|
||||
locationCallback, Looper.myLooper()
|
||||
)
|
||||
} catch (unlikely: SecurityException) {
|
||||
Log.e(TAG, "Lost location permission. Could not request updates. $unlikely")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes location updates. Note that in this sample we merely log the
|
||||
* [SecurityException].
|
||||
*/
|
||||
fun removeLocationUpdates() {
|
||||
Log.i(TAG, "Removing location updates")
|
||||
try {
|
||||
fusedLocationClient.removeLocationUpdates(locationCallback)
|
||||
stopSelf()
|
||||
} catch (unlikely: SecurityException) {
|
||||
Log.e(TAG, "Lost location permission. Could not remove updates. $unlikely")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
Log.i(TAG, "Service started")
|
||||
// Tells the system to not try to recreate the service after it has been killed.
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
class LocalBinder(val service: CarLocationService) : Binder()
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
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 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
|
||||
private var availability: ChargeLocationStatus? = null
|
||||
|
||||
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 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 iconGen =
|
||||
ChargerIconGenerator(carContext, null, oversize = 1.4f, height = imageSize)
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE)
|
||||
} else 2
|
||||
private val largeImageSupported =
|
||||
ctx.carAppApiLevel >= 4 // since API 4, Row.setImage is supported
|
||||
|
||||
init {
|
||||
referenceData.observe(this) {
|
||||
loadCharger()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (charger == null) loadCharger()
|
||||
|
||||
return PaneTemplate.Builder(
|
||||
Pane.Builder().apply {
|
||||
charger?.let { charger ->
|
||||
if (largeImageSupported && photo != null) {
|
||||
setImage(CarIcon.Builder(IconCompat.createWithBitmap(photo)).build())
|
||||
}
|
||||
generateRows(charger).forEach { addRow(it) }
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.navigate))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener {
|
||||
navigateToCharger(charger)
|
||||
}
|
||||
.build())
|
||||
charger.chargepriceData?.country?.let { country ->
|
||||
if (ChargepriceApi.isCountrySupported(country, charger.dataSource)) {
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.auto_prices))
|
||||
.setOnClickListener {
|
||||
screenManager.push(ChargepriceScreen(carContext, charger))
|
||||
}
|
||||
.build())
|
||||
}
|
||||
}
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
).apply {
|
||||
setTitle(chargerSparse.name)
|
||||
setHeaderAction(Action.BACK)
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.open_in_app))
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, MapsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_CHARGER_ID, chargerSparse.id)
|
||||
.putExtra(EXTRA_LAT, chargerSparse.coordinates.lat)
|
||||
.putExtra(EXTRA_LON, chargerSparse.coordinates.lng)
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
.build()
|
||||
).build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun generateRows(charger: ChargeLocation): List<Row> {
|
||||
val rows = mutableListOf<Row>()
|
||||
|
||||
// Row 1: address + chargepoints
|
||||
rows.add(Row.Builder().apply {
|
||||
setTitle(charger.address.toString())
|
||||
|
||||
if (photo == null) {
|
||||
// show just the icon
|
||||
val icon = iconGen.getBitmap(
|
||||
tint = getMarkerTint(charger),
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
} else if (!largeImageSupported) {
|
||||
// show the photo with icon
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
addText(generateChargepointsText(charger))
|
||||
}.build())
|
||||
if (maxRows <= 3) {
|
||||
// row 2: operator + cost + fault report
|
||||
rows.add(Row.Builder().apply {
|
||||
if (photo != null && !largeImageSupported) {
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
val operatorText = generateOperatorText(charger)
|
||||
setTitle(operatorText)
|
||||
|
||||
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
|
||||
charger.faultReport?.let { fault ->
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
fault.created?.atZone(ZoneId.systemDefault())
|
||||
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
} else {
|
||||
// row 2: operator + cost + cost description
|
||||
rows.add(Row.Builder().apply {
|
||||
if (photo != null && !largeImageSupported) {
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
val operatorText = generateOperatorText(charger)
|
||||
setTitle(operatorText)
|
||||
charger.cost?.let {
|
||||
addText(it.getStatusText(carContext, emoji = true))
|
||||
(it.descriptionShort ?: it.descriptionLong)?.let { addText(it) }
|
||||
}
|
||||
}.build())
|
||||
// row 3: fault report (if exists)
|
||||
charger.faultReport?.let { fault ->
|
||||
rows.add(Row.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
fault.created?.atZone(ZoneId.systemDefault())
|
||||
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
fault.description?.let {
|
||||
addText(
|
||||
HtmlCompat.fromHtml(
|
||||
it.replace("\n", " · "),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
// row 4: opening hours + location description
|
||||
charger.openinghours?.let { hours ->
|
||||
rows.add(Row.Builder().apply {
|
||||
setTitle(hours.getStatusText(carContext))
|
||||
hours.description?.let { addText(it) }
|
||||
charger.locationDescription?.let { addText(it) }
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
if (i > 0) chargepointsText.append(" · ")
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
)
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
return chargepointsText
|
||||
}
|
||||
|
||||
private fun generateOperatorText(charger: ChargeLocation) =
|
||||
if (charger.operator != null && charger.network != null) {
|
||||
if (charger.operator.contains(charger.network)) {
|
||||
charger.operator
|
||||
} else if (charger.network.contains(charger.operator)) {
|
||||
charger.network
|
||||
} else {
|
||||
"${charger.operator} · ${charger.network}"
|
||||
}
|
||||
} else if (charger.operator != null) {
|
||||
charger.operator
|
||||
} else if (charger.network != null) {
|
||||
charger.network
|
||||
} else {
|
||||
carContext.getString(R.string.unknown_operator)
|
||||
}
|
||||
|
||||
private fun navigateToCharger(charger: ChargeLocation) {
|
||||
val coord = charger.coordinates
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
|
||||
private fun loadCharger() {
|
||||
val referenceData = referenceData.value ?: return
|
||||
lifecycleScope.launch {
|
||||
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
|
||||
if (response.status == Status.SUCCESS) {
|
||||
val charger = response.data!!
|
||||
|
||||
val photo = charger.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val density = carContext.resources.displayMetrics.density
|
||||
val url = if (largeImageSupported) {
|
||||
photo.getUrl(
|
||||
width = (imageWidthLarge * density).roundToInt(),
|
||||
height = (imageHeightLarge * density).roundToInt()
|
||||
)
|
||||
} else {
|
||||
photo.getUrl(size = (imageSize * density).roundToInt())
|
||||
}
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
var img =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
|
||||
// draw icon on top of image
|
||||
val icon = iconGen.getBitmap(
|
||||
tint = getMarkerTint(charger),
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
|
||||
img = img.copy(Bitmap.Config.ARGB_8888, true)
|
||||
val iconSmall = icon.scale(
|
||||
(img.height * 0.4 / icon.height * icon.width).roundToInt(),
|
||||
(img.height * 0.4).roundToInt()
|
||||
)
|
||||
val canvas = Canvas(img)
|
||||
canvas.drawBitmap(
|
||||
iconSmall,
|
||||
0f,
|
||||
(img.height - iconSmall.height * 1.1).toFloat(),
|
||||
null
|
||||
)
|
||||
this@ChargerDetailScreen.photo = img
|
||||
}
|
||||
this@ChargerDetailScreen.charger = charger
|
||||
|
||||
availability = getAvailability(charger).data
|
||||
|
||||
invalidate()
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(ctx)
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
private val maxRows = 6
|
||||
private val checkIcon =
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check)).build()
|
||||
private val emptyIcon: CarIcon
|
||||
|
||||
init {
|
||||
val size = (ctx.resources.displayMetrics.density * 24).roundToInt()
|
||||
emptyIcon = Bitmap.createBitmap(
|
||||
size,
|
||||
size,
|
||||
Bitmap.Config.ARGB_8888
|
||||
).asCarIcon()
|
||||
}
|
||||
|
||||
init {
|
||||
filterProfiles.observe(this) {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val filterStatus =
|
||||
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
|
||||
?: FILTERS_DISABLED
|
||||
return ListTemplate.Builder().apply {
|
||||
filterProfiles.value?.let {
|
||||
setSingleList(buildFilterProfilesList(it.take(maxRows), filterStatus))
|
||||
} ?: setLoading(true)
|
||||
setTitle(carContext.getString(R.string.menu_filter))
|
||||
setHeaderAction(Action.BACK)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildFilterProfilesList(
|
||||
profiles: List<FilterProfile>,
|
||||
filterStatus: Long
|
||||
): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.no_filters))
|
||||
if (FILTERS_DISABLED == filterStatus) {
|
||||
setImage(checkIcon)
|
||||
} else {
|
||||
setImage(emptyIcon)
|
||||
}
|
||||
setOnClickListener {
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
screenManager.pop()
|
||||
}
|
||||
}.build())
|
||||
profiles.forEach {
|
||||
addItem(Row.Builder().apply {
|
||||
val name =
|
||||
it.name.ifEmpty { carContext.getString(R.string.unnamed_filter_profile) }
|
||||
setTitle(name)
|
||||
if (it.id == filterStatus) {
|
||||
setImage(checkIcon)
|
||||
} else {
|
||||
setImage(emptyIcon)
|
||||
}
|
||||
setOnClickListener {
|
||||
prefs.filterStatus = it.id
|
||||
screenManager.pop()
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
setNoItemsMessage(carContext.getString(R.string.filterprofiles_empty_state))
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
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.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.Lifecycle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.R
|
||||
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.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
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.filtersWithValue
|
||||
import net.vonforst.evmap.viewmodel.getFilterValues
|
||||
import net.vonforst.evmap.viewmodel.getFilters
|
||||
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.roundToInt
|
||||
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
*/
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
|
||||
Screen(ctx), LocationAwareScreen {
|
||||
private var updateCoroutine: Job? = null
|
||||
private var numUpdates = 0
|
||||
|
||||
/* Updating map contents is disabled - if the user uses Chargeprice from the charger
|
||||
detail screen, this already means 4 steps, after which the app would crash.
|
||||
follow https://issuetracker.google.com/issues/176694222 for updates how to solve this. */
|
||||
private val maxNumUpdates = 1
|
||||
|
||||
private var location: Location? = null
|
||||
private var lastChargerUpdateLocation: Location? = null
|
||||
private var lastDistanceUpdateTime: Instant? = null
|
||||
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 searchRadius = 5 // kilometers
|
||||
private val chargerUpdateThreshold = 2000 // meters
|
||||
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)
|
||||
} else 6
|
||||
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
private val filterStatus = MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
|
||||
?: FILTERS_DISABLED
|
||||
}
|
||||
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
|
||||
private val filters = api.getFilters(referenceData, carContext.stringProvider())
|
||||
private val filtersWithValue = filtersWithValue(filters, filterValues)
|
||||
|
||||
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
|
||||
init {
|
||||
filtersWithValue.observe(this) {
|
||||
loadChargers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
} ?: setLoading(true)
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
// only show the city if not all chargers are in the same city
|
||||
val showCity = chargerList.map { it.address.city }.distinct().size > 1
|
||||
chargerList.forEach { charger ->
|
||||
builder.addItem(formatCharger(charger, showCity))
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
}
|
||||
)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} ?: setLoading(true)
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (!favorites) {
|
||||
val filtersCount = filtersWithValue.value?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
|
||||
setActionStrip(
|
||||
ActionStrip.Builder()
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_filter
|
||||
)
|
||||
)
|
||||
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.pushForResult(FilterScreen(carContext)) {
|
||||
chargers = null
|
||||
numUpdates = 0
|
||||
filterStatus.value =
|
||||
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
|
||||
?: FILTERS_DISABLED
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.build())
|
||||
}
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
|
||||
val markerTint = if (charger.maxPower > 100) {
|
||||
R.color.charger_100kw_dark // slightly darker color for better contrast
|
||||
} else {
|
||||
getMarkerTint(charger)
|
||||
}
|
||||
val color = ContextCompat.getColor(carContext, markerTint)
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.createCustom(color, color))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
return Row.Builder().apply {
|
||||
// only show the city if not all chargers are in the same city (-> showCity == true)
|
||||
// and the city is not already contained in the charger name
|
||||
if (showCity && charger.address.city != null && charger.address.city !in charger.name) {
|
||||
setTitle(
|
||||
CarText.Builder("${charger.name} · ${charger.address.city}")
|
||||
.addVariant(charger.name)
|
||||
.build())
|
||||
} else {
|
||||
setTitle(charger.name)
|
||||
}
|
||||
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
location?.let {
|
||||
val distanceMeters = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
)
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(
|
||||
roundValueToDistance(
|
||||
distanceMeters,
|
||||
energyLevel?.distanceDisplayUnit?.value
|
||||
)
|
||||
),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// power
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${charger.maxPower.roundToInt()} kW")
|
||||
|
||||
// availability
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumBy { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
"$available/$total",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
addText(text)
|
||||
setMetadata(
|
||||
Metadata.Builder()
|
||||
.setPlace(place)
|
||||
.build()
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
if (location.latitude == this.location?.latitude
|
||||
&& location.longitude == this.location?.longitude
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
return
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
if (lastDistanceUpdateTime == null ||
|
||||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
|
||||
) {
|
||||
lastDistanceUpdateTime = now
|
||||
// update displayed distances
|
||||
invalidate()
|
||||
}
|
||||
|
||||
if (lastChargerUpdateLocation == null ||
|
||||
location.distanceTo(lastChargerUpdateLocation) > chargerUpdateThreshold
|
||||
) {
|
||||
lastChargerUpdateLocation = location
|
||||
// update displayed chargers
|
||||
loadChargers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargers() {
|
||||
val location = location ?: return
|
||||
val referenceData = referenceData.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
|
||||
numUpdates++
|
||||
println(numUpdates)
|
||||
if (numUpdates > maxNumUpdates) {
|
||||
/*CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
|
||||
.show()*/
|
||||
return
|
||||
}
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
try {
|
||||
// load chargers
|
||||
if (favorites) {
|
||||
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < maxRows) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius * 10,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove outdated availabilities
|
||||
availabilities = availabilities.filter {
|
||||
Duration.between(
|
||||
it.value.first,
|
||||
ZonedDateTime.now()
|
||||
) > availabilityUpdateThreshold
|
||||
}.toMutableMap()
|
||||
|
||||
// update availabilities
|
||||
chargers?.take(maxRows)?.map {
|
||||
lifecycleScope.async {
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
val date = ZonedDateTime.now()
|
||||
val availability = getAvailability(it).data
|
||||
if (availability != null) {
|
||||
availabilities[it.id] = date to availability
|
||||
}
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
|
||||
updateCoroutine = null
|
||||
lastDistanceUpdateTime = Instant.now()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
this.energyLevel = energyLevel
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
private fun setupListeners() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
"com.google.android.gms.permission.CAR_FUEL"
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
return
|
||||
|
||||
println("Setting up energy level listener")
|
||||
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
private fun removeListeners() {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.Manifest
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
/**
|
||||
* Welcome screen with selection between favorites and nearby chargers
|
||||
*/
|
||||
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
|
||||
private var location: Location? = null
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
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.bindLocationService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.app_name))
|
||||
if (!session.locationPermissionGranted()) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
}
|
||||
setItemList(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_closeby))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_address
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(
|
||||
MapScreen(
|
||||
carContext,
|
||||
session,
|
||||
favorites = false
|
||||
)
|
||||
)
|
||||
}
|
||||
.build())
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_favorites))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_fav
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = true))
|
||||
}
|
||||
.build())
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_vehicle_data))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(carContext, R.drawable.ic_car)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
session.mapScreen = null
|
||||
screenManager.push(VehicleDataScreen(carContext))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
setCurrentLocationEnabled(true)
|
||||
}
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
if (location.latitude == this.location?.latitude
|
||||
&& location.longitude == this.location?.longitude
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.location = location
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.text.style.StyleSpan
|
||||
import com.car2go.maps.google.adapter.AnyMapAdapter
|
||||
import com.car2go.maps.util.SphericalUtil
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.google.android.gms.common.api.CommonStatusCodes
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.gms.maps.model.LatLngBounds
|
||||
import com.google.android.gms.tasks.Tasks.await
|
||||
@@ -19,6 +20,7 @@ import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRe
|
||||
import com.google.android.libraries.places.api.net.PlacesStatusCodes
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import net.vonforst.evmap.R
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@@ -36,10 +38,10 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
): List<AutocompletePlace> {
|
||||
val request = FindAutocompletePredictionsRequest.builder().apply {
|
||||
if (location != null) {
|
||||
setLocationBias(calcLocationBias(location))
|
||||
setOrigin(LatLng(location.latitude, location.longitude))
|
||||
locationBias = calcLocationBias(location)
|
||||
origin = LatLng(location.latitude, location.longitude)
|
||||
}
|
||||
setSessionToken(token)
|
||||
sessionToken = token
|
||||
setQuery(query)
|
||||
}.build()
|
||||
try {
|
||||
@@ -58,6 +60,13 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
if (cause is ApiException) {
|
||||
if (cause.statusCode == PlacesStatusCodes.OVER_QUERY_LIMIT) {
|
||||
throw ApiUnavailableException()
|
||||
} else if (cause.statusCode in listOf(
|
||||
CommonStatusCodes.NETWORK_ERROR,
|
||||
CommonStatusCodes.TIMEOUT, CommonStatusCodes.RECONNECTION_TIMED_OUT,
|
||||
CommonStatusCodes.RECONNECTION_TIMED_OUT_DURING_UPDATE
|
||||
)
|
||||
) {
|
||||
throw IOException(cause)
|
||||
}
|
||||
}
|
||||
throw e
|
||||
@@ -83,10 +92,11 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAttributionString(): Int = R.string.places_powered_by_google
|
||||
override fun getAttributionString(): Int =
|
||||
com.google.android.libraries.places.R.string.places_powered_by_google
|
||||
|
||||
override fun getAttributionImage(dark: Boolean): Int =
|
||||
if (dark) R.drawable.places_powered_by_google_dark else R.drawable.places_powered_by_google_light
|
||||
if (dark) com.google.android.libraries.places.R.drawable.places_powered_by_google_dark else com.google.android.libraries.places.R.drawable.places_powered_by_google_light
|
||||
|
||||
private fun calcLocationBias(location: com.car2go.maps.model.LatLng): RectangularBounds {
|
||||
val radius = 100e3 // meters
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentOnboardingAndroidAutoBinding
|
||||
|
||||
class OnboardingViewPagerAdapter(fragment: Fragment) :
|
||||
FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount(): Int = 4
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> WelcomeFragment()
|
||||
1 -> IconsFragment()
|
||||
2 -> AndroidAutoFragment()
|
||||
3 -> DataSourceSelectFragment()
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidAutoFragment : OnboardingPageFragment() {
|
||||
private lateinit var binding: FragmentOnboardingAndroidAutoBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentOnboardingAndroidAutoBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.btnGetStarted.setOnClickListener {
|
||||
parent.goToNext()
|
||||
}
|
||||
binding.imgAndroidAuto.alpha = 0f
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val animators =
|
||||
listOf(
|
||||
ObjectAnimator.ofFloat(binding.imgAndroidAuto, "translationY", -20f, 0f).apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
},
|
||||
ObjectAnimator.ofFloat(binding.imgAndroidAuto, "alpha", 0f, 1f).apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
)
|
||||
AnimatorSet().apply {
|
||||
playTogether(animators)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.imgAndroidAuto.alpha = 0f
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.android.billingclient.api.*
|
||||
import com.android.billingclient.api.BillingClient.ProductType
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
|
||||
@@ -14,6 +15,12 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
.setListener(this)
|
||||
.enablePendingPurchases()
|
||||
.build()
|
||||
|
||||
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
|
||||
MutableLiveData<Resource<List<DonationItem>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
billingClient.startConnection(object : BillingClientStateListener {
|
||||
@@ -24,10 +31,15 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
loadProducts()
|
||||
|
||||
// consume pending purchases
|
||||
val purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
|
||||
purchases.purchasesList?.forEach {
|
||||
if (!it.isAcknowledged) {
|
||||
consumePurchase(it.purchaseToken, false)
|
||||
billingClient.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder()
|
||||
.setProductType(ProductType.INAPP)
|
||||
.build()
|
||||
) { _, purchasesList ->
|
||||
purchasesList.forEach {
|
||||
if (!it.isAcknowledged) {
|
||||
consumePurchase(it.purchaseToken, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,26 +48,26 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
|
||||
private fun loadProducts() {
|
||||
val params = SkuDetailsParams.newBuilder()
|
||||
.setType(BillingClient.SkuType.INAPP)
|
||||
.setSkusList(
|
||||
listOf(
|
||||
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
|
||||
) +
|
||||
if (BuildConfig.DEBUG) {
|
||||
listOf(
|
||||
"android.test.purchased", "android.test.canceled",
|
||||
"android.test.item_unavailable"
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val productIds = listOf(
|
||||
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
|
||||
) + if (BuildConfig.DEBUG) {
|
||||
listOf(
|
||||
"android.test.purchased", "android.test.canceled",
|
||||
"android.test.item_unavailable"
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productIds.map {
|
||||
QueryProductDetailsParams.Product.newBuilder().setProductType(ProductType.INAPP)
|
||||
.setProductId(it).build()
|
||||
})
|
||||
.build()
|
||||
billingClient.querySkuDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
|
||||
billingClient.queryProductDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
products.postValue(Resource.success(details
|
||||
.sortedBy { it.priceAmountMicros }
|
||||
.sortedBy { it.oneTimePurchaseOfferDetails!!.priceAmountMicros }
|
||||
.map { DonationItem(it) }
|
||||
))
|
||||
} else {
|
||||
@@ -64,12 +76,6 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
}
|
||||
|
||||
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
|
||||
MutableLiveData<Resource<List<DonationItem>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
}
|
||||
}
|
||||
|
||||
val purchaseSuccessful = SingleLiveEvent<Nothing>()
|
||||
val purchaseFailed = SingleLiveEvent<Nothing>()
|
||||
|
||||
@@ -97,7 +103,13 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
|
||||
fun startPurchase(it: DonationItem, activity: Activity) {
|
||||
val flowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(it.sku)
|
||||
.setProductDetailsParamsList(
|
||||
listOf(
|
||||
BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(it.product)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
val response = billingClient.launchBillingFlow(activity, flowParams)
|
||||
if (response.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
@@ -110,4 +122,4 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
}
|
||||
|
||||
data class DonationItem(val sku: SkuDetails) : Equatable
|
||||
data class DonationItem(val product: ProductDetails) : Equatable
|
||||
@@ -28,7 +28,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.sku.title}"
|
||||
android:text="@{item.product.title}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView21"
|
||||
@@ -41,7 +41,7 @@
|
||||
android:id="@+id/textView21"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.sku.price}"
|
||||
android:text="@{item.product.oneTimePurchaseOfferDetails.formattedPrice}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
||||
5
app/src/google/res/values-de/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
||||
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap (Mapbox) für die Kartendaten wechseln.</string>
|
||||
</resources>
|
||||
@@ -1,37 +0,0 @@
|
||||
<?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>
|
||||
<string name="auto_no_favorites_found">Keine Favoriten gefunden</string>
|
||||
<string name="open_in_app">In App öffnen</string>
|
||||
<string name="opened_on_phone">Auf dem Telefon geöffnet</string>
|
||||
<string name="auto_location_permission_needed">Um EVMap auf Android Auto zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
|
||||
<string name="auto_vehicle_data_permission_needed">Für diese Funktion benötigt EVMap Zugriff auf Daten deines Fahrzeugs.</string>
|
||||
<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_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>
|
||||
<string name="auto_vehicle_data">Fahrzeugdaten</string>
|
||||
<string name="auto_charging_level">Ladezustand</string>
|
||||
<string name="auto_no_data">Nicht verfügbar</string>
|
||||
<string name="auto_range">Reichweite</string>
|
||||
<string name="auto_speed">Geschwindigkeit</string>
|
||||
<string name="welcome_android_auto">Android Auto-Unterstützung</string>
|
||||
<string name="welcome_android_auto_detail">Auf unterstützen Autos kannst du EVMap auch mit Android Auto nutzen. Öffne dazu einfach die EVMap-App aus dem Menü von Android Auto.</string>
|
||||
<string name="sounds_cool">klingt cool</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap konnte das Fahrzeugmodell nicht erkennen.</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Keins der in der App ausgewählten Fahrzeuge passt zu diesem Fahrzeug (%1$s %2$s).</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%1$s %2$s).</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Ladebereich für Preisvergleich</string>
|
||||
</resources>
|
||||
7
app/src/google/res/values-fr/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?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="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap (Mapbox) pour les données cartographiques.</string>
|
||||
</resources>
|
||||
7
app/src/google/res/values-nb-rNO/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?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="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap (Mapbox) for kartdata.</string>
|
||||
</resources>
|
||||
7
app/src/google/res/values-nl/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
|
||||
\n
|
||||
\nGoogle houdt 15% in van elke donatie.</string>
|
||||
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap (Mapbox) voor de kaartgegevens.</string>
|
||||
</resources>
|
||||
7
app/src/google/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.
|
||||
\n
|
||||
\nA Google cobra 15% de cada doação.</string>
|
||||
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap (Mapbox) nas definições da app.</string>
|
||||
</resources>
|
||||
2
app/src/google/res/values-ro/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></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>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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>
|
||||
</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>
|
||||
5
app/src/google/res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?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.\n\nGoogle takes 15% off every donation.</string>
|
||||
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap (Mapbox) for the map data.</string>
|
||||
</resources>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="CarAppTheme">
|
||||
<item name="carColorPrimary">@color/colorPrimary</item>
|
||||
<item name="carColorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="carColorSecondary">@color/colorSecondary</item>
|
||||
<item name="carColorSecondaryDark">@color/colorSecondaryDark</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,47 +0,0 @@
|
||||
<?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>
|
||||
<string name="auto_no_favorites_found">No favorites found</string>
|
||||
<string name="open_in_app">Open in app</string>
|
||||
<string name="opened_on_phone">Opened on phone</string>
|
||||
<string name="auto_location_permission_needed">To run EVMap on Android Auto, you need to grant access to your location.</string>
|
||||
<string name="auto_vehicle_data_permission_needed">For this feature, EVMap needs access to your vehicle data.</string>
|
||||
<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_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>
|
||||
<string name="auto_vehicle_data">Vehicle data</string>
|
||||
<string name="auto_charging_level">Charging level</string>
|
||||
<string name="auto_no_data">Unavailable</string>
|
||||
<string name="auto_range">Range</string>
|
||||
<string name="auto_speed">Speed</string>
|
||||
<string name="welcome_android_auto">Android Auto support</string>
|
||||
<string name="welcome_android_auto_detail">You can also use EVMap from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.</string>
|
||||
<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="settings_android_auto_chargeprice_range">Charging range for price comparison</string>
|
||||
</resources>
|
||||
@@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<Preference
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
|
||||
android:title="@string/settings_android_auto"
|
||||
android:icon="@drawable/ic_android_auto" />
|
||||
<PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,10 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.vonforst.evmap">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -15,16 +20,27 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="google.navigation" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
|
||||
<package android:name="com.google.android.projection.gearhead" />
|
||||
<package android:name="com.google.android.apps.automotive.templates.host" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".EvMapApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:dataExtractionRules="@xml/backup_rules_api31"
|
||||
android:fullBackupOnly="true"
|
||||
android:backupAgent=".storage.BackupAgent"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
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"
|
||||
@@ -252,6 +268,17 @@
|
||||
android:host="www.goingelectric.de"
|
||||
android:pathPattern="/stromtankstellen/Ungarn/..*/..*/..*/"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="openchargemap.org"
|
||||
android:pathPattern="/site/poi/details/..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="net.vonforst.evmap" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -262,16 +289,58 @@
|
||||
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>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<!-- Remove WorkManagerInitializer as we implement getWorkManagerConfiguration in application class -->
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<!-- Configuration for Android Auto app -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.car.app.theme"
|
||||
android:resource="@style/CarAppTheme" />
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.car.app.minCarApiLevel"
|
||||
android:value="1" />
|
||||
|
||||
<service
|
||||
android:name=".auto.CarAppService"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="androidx.car.app.CarAppService"
|
||||
android:category="androidx.car.app.category.POI" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="net.vonforst.evmap" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,21 +1,34 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.stetho.Stetho
|
||||
import android.os.Build
|
||||
import androidx.work.*
|
||||
import net.vonforst.evmap.storage.CleanupCacheWorker
|
||||
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
|
||||
import org.acra.config.mailSender
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import java.time.Duration
|
||||
|
||||
class EvMapApplication : Application() {
|
||||
class EvMapApplication : Application(), Configuration.Provider {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
updateNightMode(PreferenceDataSource(this))
|
||||
Stetho.initializeWithDefaults(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
|
||||
}
|
||||
|
||||
init(applicationContext)
|
||||
addDebugInterceptors(applicationContext)
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
initAcra {
|
||||
@@ -39,5 +52,20 @@ class EvMapApplication : Application() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
|
||||
.setConstraints(Constraints.Builder().apply {
|
||||
setRequiresBatteryNotLow(true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
setRequiresDeviceIdle(true)
|
||||
}
|
||||
}.build()).build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.REPLACE, cleanupCacheRequest
|
||||
)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration {
|
||||
return Configuration.Builder().build()
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,16 @@ 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
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsClient
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen
|
||||
@@ -32,15 +33,14 @@ 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"
|
||||
const val EXTRA_FAVORITES = "favorites"
|
||||
const val EXTRA_DONATE = "donate"
|
||||
|
||||
class MapsActivity : AppCompatActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
@@ -54,17 +54,9 @@ 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()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_maps)
|
||||
|
||||
@@ -84,9 +76,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) { _, insets ->
|
||||
val header = navView.getHeaderView(0)
|
||||
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
insets
|
||||
}
|
||||
|
||||
@@ -98,7 +90,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// wait for splash screen animation to finish on first start
|
||||
splashScreen.setKeepVisibleCondition(object : SplashScreen.KeepOnScreenCondition {
|
||||
splashScreen.setKeepOnScreenCondition(object : SplashScreen.KeepOnScreenCondition {
|
||||
var startTime: Long? = null
|
||||
|
||||
override fun shouldKeepOnScreen(): Boolean {
|
||||
@@ -115,6 +107,10 @@ class MapsActivity : AppCompatActivity(),
|
||||
navGraph.setStartDestination(R.id.onboarding)
|
||||
navController.graph = navGraph
|
||||
return
|
||||
} else if (!prefs.privacyAccepted) {
|
||||
navGraph.setStartDestination(R.id.onboarding)
|
||||
navController.graph = navGraph
|
||||
return
|
||||
} else {
|
||||
navGraph.setStartDestination(R.id.map)
|
||||
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
|
||||
@@ -132,7 +128,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
|
||||
.createPendingIntent()
|
||||
} else if (query != null && query.isNotEmpty()) {
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
@@ -142,12 +138,69 @@ class MapsActivity : AppCompatActivity(),
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "goingelectric") {
|
||||
prefs.dataSource = "goingelectric"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_goingelectric)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "openchargemap.org") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "openchargemap") {
|
||||
prefs.dataSource = "openchargemap"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_openchargemap)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent.scheme == "net.vonforst.evmap") {
|
||||
intent.data?.let {
|
||||
if (it.host == "find_charger") {
|
||||
val lat = it.getQueryParameter("latitude")?.toDouble()
|
||||
val lon = it.getQueryParameter("longitude")?.toDouble()
|
||||
val name = it.getQueryParameter("name")
|
||||
if (lat != null && lon != null) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(
|
||||
MapFragmentArgs(
|
||||
latLng = LatLng(lat, lon),
|
||||
locationName = name
|
||||
).toBundle()
|
||||
)
|
||||
.createPendingIntent()
|
||||
} else if (name != null) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(locationName = name).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setDestination(R.id.map)
|
||||
@@ -163,8 +216,14 @@ class MapsActivity : AppCompatActivity(),
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.favs)
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_DONATE)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.donate)
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
deepLink?.send()
|
||||
@@ -178,7 +237,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
|
||||
intent.`package` = "com.google.android.apps.maps"
|
||||
if (prefs.navigateUseMaps && intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent);
|
||||
startActivity(intent)
|
||||
} else {
|
||||
// fallback: generic geo intent
|
||||
showLocation(charger)
|
||||
@@ -194,7 +253,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
})"
|
||||
)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent);
|
||||
startActivity(intent)
|
||||
} else {
|
||||
val cb = fragmentCallback ?: return
|
||||
Snackbar.make(
|
||||
@@ -206,6 +265,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
val pkg = CustomTabsClient.getPackageName(this, null)
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
@@ -213,6 +273,11 @@ class MapsActivity : AppCompatActivity(),
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
pkg?.let {
|
||||
// prefer to open URL in custom tab, even if native app
|
||||
// available (such as EVMap itself)
|
||||
intent.intent.setPackage(pkg)
|
||||
}
|
||||
try {
|
||||
intent.launchUrl(this, Uri.parse(url))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
@@ -227,7 +292,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
|
||||
fun shareUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
setType("text/plain")
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
}
|
||||
startActivity(intent)
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
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? {
|
||||
@@ -77,7 +75,7 @@ fun max(a: Int?, b: Int?): Int? {
|
||||
* otherwise the non-null value or null
|
||||
*/
|
||||
return if (a != null && b != null) {
|
||||
max(a, b)
|
||||
kotlin.math.max(a, b)
|
||||
} else {
|
||||
a ?: b
|
||||
}
|
||||
@@ -85,25 +83,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
|
||||
|
||||
@@ -112,4 +91,11 @@ const val meterPerFt = 0.3048
|
||||
|
||||
fun shouldUseImperialUnits(): Boolean {
|
||||
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
|
||||
}
|
||||
}
|
||||
|
||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
@@ -91,7 +90,7 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
class ChargepriceAdapter() :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
|
||||
val viewPool = RecyclerView.RecycledViewPool();
|
||||
val viewPool = RecyclerView.RecycledViewPool()
|
||||
var meta: ChargepriceChargepointMeta? = null
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -161,17 +160,18 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
val binding = holder.binding as ItemConnectorButtonBinding
|
||||
binding.enabled = enabledConnectors?.let { item.type in it } ?: true
|
||||
val root = binding.root as CheckableConstraintLayout
|
||||
root.setOnCheckedChangeListener { _, _ -> }
|
||||
root.isChecked = checkedItem == position
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
getCheckedItem()?.let { onCheckedItemChangedListener?.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked && item != checkedItem) {
|
||||
checkedItem = item
|
||||
root.post {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.joinToSpannedString
|
||||
import net.vonforst.evmap.model.ChargeCard
|
||||
@@ -10,6 +15,9 @@ import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.OpeningHoursDays
|
||||
import net.vonforst.evmap.plus
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import net.vonforst.evmap.utils.formatDMS
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
@@ -39,18 +47,25 @@ fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
teslaPricing: TeslaGraphQlApi.Pricing?,
|
||||
ctx: Context
|
||||
): List<DetailsAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
|
||||
return listOfNotNull(
|
||||
DetailsAdapter.Detail(
|
||||
if (teslaPricing != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_tesla,
|
||||
R.string.cost,
|
||||
formatTeslaPricing(teslaPricing, ctx),
|
||||
formatTeslaParkingFee(teslaPricing, ctx)
|
||||
) else null,
|
||||
if (loc.address != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_address,
|
||||
R.string.address,
|
||||
loc.address.toString(),
|
||||
loc.locationDescription,
|
||||
clickable = true
|
||||
),
|
||||
) else null,
|
||||
if (loc.operator != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_operator,
|
||||
R.string.operator,
|
||||
@@ -59,7 +74,8 @@ fun buildDetails(
|
||||
if (loc.network != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_network,
|
||||
R.string.network,
|
||||
loc.network
|
||||
loc.network,
|
||||
clickable = loc.networkUrl != null
|
||||
) else null,
|
||||
if (loc.faultReport != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_fault_report,
|
||||
@@ -91,7 +107,7 @@ fun buildDetails(
|
||||
R.drawable.ic_cost,
|
||||
R.string.cost,
|
||||
loc.cost.getStatusText(ctx),
|
||||
loc.cost.descriptionLong ?: loc.cost.descriptionShort
|
||||
loc.cost.getDetailText()
|
||||
)
|
||||
else null,
|
||||
if (loc.chargecards != null && loc.chargecards.isNotEmpty() || loc.barrierFree == true)
|
||||
@@ -123,6 +139,128 @@ fun buildDetails(
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
teslaPricing.memberRates?.activePricebook?.parking?.let { parkingFee ->
|
||||
ctx.getString(
|
||||
R.string.tesla_pricing_blocking_fee,
|
||||
formatTeslaPricingRate(parkingFee.rates, parkingFee.currencyCode, parkingFee.uom, ctx)
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
buildSpannedString {
|
||||
teslaPricing.memberRates?.let { memberRates ->
|
||||
append(
|
||||
ctx.getString(if (teslaPricing.userRates != null) R.string.tesla_pricing_members else R.string.tesla_pricing_owners),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(formatTeslaPricingRates(memberRates, ctx))
|
||||
}
|
||||
teslaPricing.userRates?.let { userRates ->
|
||||
append("\n\n")
|
||||
append(
|
||||
ctx.getString(R.string.tesla_pricing_others),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(formatTeslaPricingRates(userRates, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTeslaPricingRates(rates: TeslaGraphQlApi.Rates, ctx: Context) =
|
||||
buildSpannedString {
|
||||
val timeFmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
if (rates.activePricebook.charging.touRates.enabled) {
|
||||
// time-of-day-based rates
|
||||
val ratesByTime = rates.activePricebook.charging.touRates.activeRatesByTime
|
||||
val distinctRates =
|
||||
ratesByTime.map { it.rates }.distinct().sortedByDescending { it.max() }
|
||||
if (distinctRates.size == 2) {
|
||||
// special case: only list periods with higher price
|
||||
val highPriceTimes = ratesByTime.filter { it.rates == distinctRates[0] }
|
||||
append("\n")
|
||||
append(highPriceTimes.joinToString(", ") {
|
||||
timeFmt.format(it.startTime) + " - " + timeFmt.format(it.endTime)
|
||||
} + ": ", StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
distinctRates[0],
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
append("\n")
|
||||
append(
|
||||
ctx.getString(R.string.tesla_pricing_other_times),
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(" ")
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
distinctRates[1],
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// general case
|
||||
ratesByTime.forEach { rate ->
|
||||
append("\n")
|
||||
append(
|
||||
timeFmt.format(rate.startTime) + " - " + timeFmt.format(rate.endTime) + ": ",
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
rate.rates,
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fixed rates
|
||||
append(" ")
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
rates.activePricebook.charging.rates,
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTeslaPricingRate(
|
||||
rates: List<Double>,
|
||||
currencyCode: String,
|
||||
uom: String,
|
||||
ctx: Context
|
||||
): String {
|
||||
if (rates.isEmpty()) return ""
|
||||
val rate = rates.max()
|
||||
val value = ctx.getString(
|
||||
when (uom) {
|
||||
"kwh" -> R.string.charge_price_kwh_format
|
||||
"min" -> R.string.charge_price_minute_format
|
||||
else -> return ""
|
||||
}, rate, currency(currencyCode)
|
||||
)
|
||||
return if (rates.size > 1) {
|
||||
ctx.getString(R.string.pricing_up_to, value)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fun formatChargeCards(
|
||||
chargecards: List<ChargeCardId>,
|
||||
chargecardData: Map<Long, ChargeCard>?,
|
||||
|
||||
@@ -14,7 +14,7 @@ class FavoritesAdapter(val onDelete: (FavoritesViewModel.FavoritesListItem) -> U
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
override fun getItemId(position: Int): Long = getItem(position).fav.favorite.favoriteId
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun bind(
|
||||
|
||||
@@ -25,7 +25,7 @@ class FilterProfilesAdapter(
|
||||
super.bind(holder, item)
|
||||
|
||||
val binding = holder.binding as ItemFilterProfileBinding
|
||||
binding.handle.setOnTouchListener { v, event ->
|
||||
binding.handle.setOnTouchListener { _, event ->
|
||||
if (event?.action == MotionEvent.ACTION_DOWN) {
|
||||
dragHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
)
|
||||
}
|
||||
}
|
||||
is BooleanFilterValue -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
|
||||
@@ -37,27 +37,43 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val id = getItem(position).id
|
||||
val url = getItem(position).getUrl(height = holder.view.height)
|
||||
val item = getItem(position)
|
||||
|
||||
holder.view.load(
|
||||
url
|
||||
) {
|
||||
size(SizeResolver(OriginalSize))
|
||||
allowHardware(false)
|
||||
listener(
|
||||
onSuccess = { _, metadata ->
|
||||
memoryKeys[id] = metadata.memoryCacheKey
|
||||
if (holder.view.height == 0) {
|
||||
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
loadImage(item, holder)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
loadImage(item, holder)
|
||||
}
|
||||
|
||||
if (itemClickListener != null) {
|
||||
holder.view.setOnClickListener {
|
||||
itemClickListener.onItemClick(holder.view, position, memoryKeys[id])
|
||||
itemClickListener.onItemClick(holder.view, position, memoryKeys[item.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImage(
|
||||
item: ChargerPhoto,
|
||||
holder: ViewHolder
|
||||
) {
|
||||
val url = item.getUrl(height = holder.view.height)
|
||||
|
||||
holder.view.load(
|
||||
url
|
||||
) {
|
||||
listener(
|
||||
onSuccess = { _, metadata ->
|
||||
memoryKeys[item.id] = metadata.memoryCacheKey
|
||||
}
|
||||
)
|
||||
allowHardware(!BuildConfig.DEBUG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChargerPhotoDiffCallback : DiffUtil.ItemCallback<ChargerPhoto>() {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
|
||||
class SingleViewAdapter(val view: View) : Adapter<SingleViewAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun getItemCount() = 1
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,36 @@ 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 java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
interface ChargepointApi<out T : ReferenceData> {
|
||||
/**
|
||||
* Query for chargepoints within certain geographic bounds
|
||||
*/
|
||||
suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
useClustering: Boolean,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>>
|
||||
): Resource<ChargepointList>
|
||||
|
||||
/**
|
||||
* Query for chargepoints within a given radius in kilometers
|
||||
*/
|
||||
suspend fun getChargepointsRadius(
|
||||
referenceData: ReferenceData,
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
useClustering: Boolean,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>>
|
||||
): Resource<ChargepointList>
|
||||
|
||||
/**
|
||||
* Fetches detailed data for a specific charging site
|
||||
*/
|
||||
suspend fun getChargepointDetail(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
@@ -34,7 +47,17 @@ interface ChargepointApi<out T : ReferenceData> {
|
||||
|
||||
fun getFilters(referenceData: ReferenceData, sp: StringProvider): List<Filter<FilterValue>>
|
||||
|
||||
fun getName(): String
|
||||
fun convertFiltersToSQL(filters: FilterValues, referenceData: ReferenceData): FiltersSQLQuery
|
||||
|
||||
fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean
|
||||
|
||||
val name: String
|
||||
val id: String
|
||||
|
||||
/**
|
||||
* Duration we are limited to if there is a required API local cache time limit.
|
||||
*/
|
||||
val cacheLimit: Duration
|
||||
}
|
||||
|
||||
interface StringProvider {
|
||||
@@ -65,4 +88,16 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||
}
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
data class FiltersSQLQuery(
|
||||
val query: String,
|
||||
val requiresChargepointQuery: Boolean,
|
||||
val requiresChargeCardQuery: Boolean
|
||||
)
|
||||
|
||||
data class ChargepointList(val items: List<ChargepointListItem>, val isComplete: Boolean) {
|
||||
companion object {
|
||||
fun empty() = ChargepointList(emptyList(), true)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class RateLimitInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (request.url.host == "my.newmotion.com") {
|
||||
if (request.url.host == "ui-map.shellrecharge.com") {
|
||||
// limit requests sent to NewMotion to 3 per second
|
||||
rateLimiter.acquire(1)
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.RateLimitInterceptor
|
||||
import net.vonforst.evmap.api.await
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.cartesianProduct
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -21,6 +23,14 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
interface AvailabilityDetector {
|
||||
suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus
|
||||
|
||||
/**
|
||||
* Get a rough estimate whether this charger is supported by this provider.
|
||||
*
|
||||
* This might be done by checking supported countries, or even by matching the operator
|
||||
* for operator-specific availability detectors.
|
||||
*/
|
||||
fun isChargerSupported(charger: ChargeLocation): Boolean
|
||||
}
|
||||
|
||||
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
|
||||
@@ -63,19 +73,19 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
connectors: Map<Long, Pair<Double, String>>,
|
||||
chargepoints: List<Chargepoint>
|
||||
): Map<Chargepoint, Set<Long>> {
|
||||
var chargepoints = chargepoints
|
||||
var cpts = chargepoints
|
||||
|
||||
// iterate over each connector type
|
||||
val types = connectors.map { it.value.second }.distinct().toSet()
|
||||
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
|
||||
var geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
var geTypes = cpts.map { it.type }.distinct().toSet()
|
||||
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
|
||||
Chargepoint.SCHUKO
|
||||
)) {
|
||||
// If charger has household plugs and other plugs, try removing the household plugs
|
||||
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
|
||||
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
|
||||
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
|
||||
cpts = cpts.filter { it.type != Chargepoint.SCHUKO }
|
||||
}
|
||||
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
return types.flatMap { type ->
|
||||
@@ -85,14 +95,14 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
val powers = connsOfType.map { it.value.first }.distinct().sorted()
|
||||
// find corresponding powers in GE data
|
||||
val gePowers =
|
||||
chargepoints.filter { equivalentPlugTypes(it.type).any { it == type } }
|
||||
.map { it.power }.distinct().sorted()
|
||||
cpts.filter { equivalentPlugTypes(it.type).any { it == type } }
|
||||
.mapNotNull { it.power }.distinct().sorted()
|
||||
|
||||
// if the distinct number of powers is the same, try to match.
|
||||
if (powers.size == gePowers.size) {
|
||||
gePowers.zip(powers).map { (gePower, power) ->
|
||||
val chargepoint =
|
||||
chargepoints.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
|
||||
cpts.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
|
||||
val ids = connsOfType.filter { it.value.first == power }.keys
|
||||
if (chargepoint.count != ids.size) {
|
||||
throw AvailabilityDetectorException("chargepoints do not match")
|
||||
@@ -100,7 +110,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
chargepoint to ids
|
||||
}
|
||||
} else if (powers.size == 1 && gePowers.size == 2
|
||||
&& chargepoints.sumOf { it.count } == connsOfType.size
|
||||
&& cpts.sumOf { it.count } == connsOfType.size
|
||||
) {
|
||||
// special case: dual charger(s) with load balancing
|
||||
// GoingElectric shows 2 different powers, NewMotion just one
|
||||
@@ -108,7 +118,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
var i = 0
|
||||
gePowers.map { gePower ->
|
||||
val chargepoint =
|
||||
chargepoints.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
|
||||
cpts.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
|
||||
val ids = allIds.subList(i, i + chargepoint.count).toSet()
|
||||
i += chargepoint.count
|
||||
chargepoint to ids
|
||||
@@ -124,14 +134,17 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
|
||||
data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String
|
||||
val source: String,
|
||||
val evseIds: Map<Chargepoint, List<String>>? = null,
|
||||
val congestionHistogram: List<Double>? = null,
|
||||
val extraData: Any? = null // API-specific data
|
||||
) {
|
||||
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
|
||||
val statusFiltered = status.filterKeys {
|
||||
(connectors == null || connectors.map {
|
||||
equivalentPlugTypes(it)
|
||||
}.any { equivalent -> it.type in equivalent })
|
||||
&& (minPower == null || it.power > minPower)
|
||||
&& (minPower == null || (it.power != null && it.power >= minPower))
|
||||
}
|
||||
return this.copy(status = statusFiltered)
|
||||
}
|
||||
@@ -149,43 +162,41 @@ private val cookieManager = CookieManager().apply {
|
||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
}
|
||||
|
||||
private val okhttp = OkHttpClient.Builder()
|
||||
.addInterceptor(RateLimitInterceptor())
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
.build()
|
||||
val availabilityDetectors = listOf(
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
/*ChargecloudAvailabilityDetector(
|
||||
okhttp,
|
||||
"606a0da0dfdd338ee4134605653d4fd8"
|
||||
), // Maingau
|
||||
ChargecloudAvailabilityDetector(
|
||||
okhttp,
|
||||
"6336fe713f2eb7fa04b97ff6651b76f8"
|
||||
) // SW Kiel*/
|
||||
)
|
||||
class AvailabilityRepository(context: Context) {
|
||||
private val okhttp = OkHttpClient.Builder()
|
||||
.addInterceptor(RateLimitInterceptor())
|
||||
.addDebugInterceptors()
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
.build()
|
||||
private val availabilityDetectors = listOf(
|
||||
RheinenergieAvailabilityDetector(okhttp),
|
||||
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context)),
|
||||
EnBwAvailabilityDetector(okhttp),
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
)
|
||||
|
||||
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
if (!ad.isChargerSupported(charger)) continue
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
}
|
||||
}
|
||||
|
||||