mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 15:47:44 -05:00
Compare commits
121 Commits
openstreet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
897b439cca | ||
|
|
141b2c76b1 | ||
|
|
fc22b16111 | ||
|
|
f41ea230de | ||
|
|
ceb5081757 | ||
|
|
28bb8cef5f | ||
|
|
ba17cb989a | ||
|
|
d08aaa3325 | ||
|
|
0f24608d2a | ||
|
|
92e9539286 | ||
|
|
b373f49180 | ||
|
|
ec8728a253 | ||
|
|
9ca470cd46 | ||
|
|
38a1bf2da5 | ||
|
|
5c1dad82b1 | ||
|
|
5647820f3e | ||
|
|
092a3e50bc | ||
|
|
7b27fe2cac | ||
|
|
8991cb4e4a | ||
|
|
66d6aee97e | ||
|
|
3a5646a3ac | ||
|
|
14eadef10d | ||
|
|
cea0878267 | ||
|
|
2b4c0829a8 | ||
|
|
8e9d9d15c4 | ||
|
|
ca9a7df8b0 | ||
|
|
51aecd179c | ||
|
|
6781989266 | ||
|
|
872d3c5143 | ||
|
|
69622c6816 | ||
|
|
15fdac6348 | ||
|
|
6c206c7a25 | ||
|
|
8f49b1f238 | ||
|
|
31bd2b7dd4 | ||
|
|
5524d14562 | ||
|
|
5a360a7ee0 | ||
|
|
98d3c91686 | ||
|
|
12c1c6a5ec | ||
|
|
21e23efb50 | ||
|
|
f6f2b15f41 | ||
|
|
c3776758b3 | ||
|
|
6d9e34667c | ||
|
|
24b94a055e | ||
|
|
1d2a7e4af9 | ||
|
|
fa86c7c15a | ||
|
|
4cd9872d0f | ||
|
|
1e78ffce7e | ||
|
|
3eaa97ea4f | ||
|
|
adaf2f0c87 | ||
|
|
5802526d14 | ||
|
|
fe731f71e8 | ||
|
|
c22173b79e | ||
|
|
82a5730aed | ||
|
|
3386092bf8 | ||
|
|
1318126780 | ||
|
|
abf9165602 | ||
|
|
2c35df6360 | ||
|
|
4ed046df7a | ||
|
|
a20f25af17 | ||
|
|
b2a2114c88 | ||
|
|
c2896ade45 | ||
|
|
45983bce7f | ||
|
|
d0fffb1a97 | ||
|
|
4819a10d03 | ||
|
|
8a0f7e79f0 | ||
|
|
c727d9f1b8 | ||
|
|
e5d0ebbbb5 | ||
|
|
ee4b5e7319 | ||
|
|
fecde441f1 | ||
|
|
cb1543cb4a | ||
|
|
276daac607 | ||
|
|
f7d39a1ba5 | ||
|
|
fa09b9188e | ||
|
|
b31e55f130 | ||
|
|
c494b0d5e2 | ||
|
|
272b86ff88 | ||
|
|
32de28bc1c | ||
|
|
4cd6c44ba1 | ||
|
|
3265694c51 | ||
|
|
529be2cc34 | ||
|
|
00862b66a1 | ||
|
|
cabaa42772 | ||
|
|
1663607171 | ||
|
|
126c47bbc1 | ||
|
|
b93d01f96d | ||
|
|
7fb5df29e4 | ||
|
|
b878d37982 | ||
|
|
0f7aa44d8e | ||
|
|
d8e1c36993 | ||
|
|
03f613fa4b | ||
|
|
aba533e553 | ||
|
|
307af88f01 | ||
|
|
8478948d5f | ||
|
|
7e96c9e5a7 | ||
|
|
44bd2c6159 | ||
|
|
7d2a19b0a3 | ||
|
|
3414a7581c | ||
|
|
df47f7b4c1 | ||
|
|
a08e2ab7e9 | ||
|
|
c1351ce935 | ||
|
|
b4a1a8b546 | ||
|
|
3865e6c33d | ||
|
|
091b0f5ac3 | ||
|
|
1148200f37 | ||
|
|
1847e8b771 | ||
|
|
bbfe8e2bb2 | ||
|
|
983d368a78 | ||
|
|
4a6a34db3a | ||
|
|
35ddece698 | ||
|
|
36c6a4053d | ||
|
|
104913b3c4 | ||
|
|
5cc510fe22 | ||
|
|
4250eb2ba8 | ||
|
|
1db82db066 | ||
|
|
d6a8fbee7d | ||
|
|
23e2f0baad | ||
|
|
ea4fb37f30 | ||
|
|
094f38ac87 | ||
|
|
b84d13d42b | ||
|
|
845bd2e5ca | ||
|
|
0b68ddb939 |
47
.github/workflows/release.yml
vendored
47
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
- name: Decrypt keystore
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Extract version code
|
||||
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
||||
|
||||
- name: Build app release
|
||||
- name: Build app release & export licenses
|
||||
env:
|
||||
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
||||
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
||||
@@ -35,10 +35,14 @@ jobs:
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
|
||||
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
|
||||
NOBIL_API_KEY: ${{ secrets.NOBIL_API_KEY }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
||||
run: ./gradlew assembleRelease --no-daemon
|
||||
run: ./gradlew exportLibraryDefinitions assembleRelease --no-daemon
|
||||
|
||||
- name: Export licenses in Appning format
|
||||
run: python3 _ci/export_licenses_appning.py
|
||||
|
||||
- name: release
|
||||
uses: actions/create-release@v1
|
||||
@@ -88,3 +92,40 @@ jobs:
|
||||
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
||||
asset_name: app-foss-automotive-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
- name: upload Licenses
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/generated/aboutLibraries/aboutlibraries.json
|
||||
asset_name: aboutlibraries.json
|
||||
asset_content_type: application/json
|
||||
- name: upload Licenses Appning
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: licenses_fossAutomotiveRelease_appning.csv
|
||||
asset_name: licenses_fossAutomotiveRelease_appning.csv
|
||||
asset_content_type: text/csv
|
||||
- name: upload Licenses Appning
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: licenses_fossNormalRelease_appning.csv
|
||||
asset_name: licenses_fossNormalRelease_appning.csv
|
||||
asset_content_type: text/csv
|
||||
|
||||
- name: Trigger Website update
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/ev-map/ev-map.github.io/dispatches \
|
||||
-d "{\"event_type\": \"trigger-workflow\"}"
|
||||
|
||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
|
||||
@@ -75,8 +75,10 @@ jobs:
|
||||
run: |
|
||||
checksec --output=json --dir=lib > checksec_output.json
|
||||
jq --argjson exceptions '[
|
||||
"lib/arm64-v8a/libc++_shared.so",
|
||||
"lib/armeabi-v7a/libc++_shared.so",
|
||||
"lib/x86/libc++_shared.so"
|
||||
"lib/x86/libc++_shared.so",
|
||||
"lib/x86_64/libc++_shared.so"
|
||||
]' '
|
||||
to_entries
|
||||
| map(select(.value.fortify_source == "no" and (.key as $lib | $exceptions | index($lib) | not)))
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.kotlin
|
||||
/local.properties
|
||||
/.idea/*
|
||||
.DS_Store
|
||||
|
||||
36
Gemfile.lock
36
Gemfile.lock
@@ -5,23 +5,28 @@ GEM
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.354.0)
|
||||
aws-sdk-core (3.104.3)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.36.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.78.0)
|
||||
aws-sdk-core (~> 3, >= 3.104.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1196.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.3)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (4.0.1)
|
||||
claide (1.0.3)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
@@ -113,9 +118,10 @@ GEM
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.1)
|
||||
jmespath (1.6.2)
|
||||
json (2.3.1)
|
||||
jwt (2.2.1)
|
||||
logger (1.7.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
|
||||
@@ -20,7 +20,6 @@ Features
|
||||
- Search for places
|
||||
- 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 & Android Automotive OS integration
|
||||
- No ads, fully open source
|
||||
- Compatible with Android 5.0 and above
|
||||
@@ -90,9 +89,5 @@ information on the [Donate page](https://ev-map.app/donate/) on the EVMap websit
|
||||
Since May 2024, **JawgMaps** provides their OpenStreetMap vector map tiles service to EVMap for
|
||||
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
|
||||
|
||||
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="38"/></a><br>
|
||||
Since April 2021, **Chargeprice.app** provide their price comparison API at a greatly reduced
|
||||
price for EVMap. This data is used in EVMap's price comparison feature.
|
||||
|
||||
<a href="https://www.jetbrains.com/community/opensource/"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains logo" height="38"/></a><br>
|
||||
As part of its support program for Open-source projects, **JetBrains** supports the development of EVMap since December 2023 with a license of their software suite.
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<string name="jawg_key" translatable="false">ci</string>
|
||||
<string name="arcgis_key" translatable="false">ci</string>
|
||||
<string name="goingelectric_key" translatable="false">ci</string>
|
||||
<string name="chargeprice_key" translatable="false">ci</string>
|
||||
<string name="openchargemap_key" translatable="false">ci</string>
|
||||
<string name="nobil_key" translatable="false">ci</string>
|
||||
<string name="fronyx_key" translatable="false">ci</string>
|
||||
<string name="acra_credentials" translatable="false">ci:ci</string>
|
||||
<string name="evmap_key" translatable="false">ci</string>
|
||||
</resources>
|
||||
|
||||
@@ -4,13 +4,10 @@ import json
|
||||
build_types = ["fossNormalRelease", "fossAutomotiveRelease"]
|
||||
|
||||
for build_type in build_types:
|
||||
result = subprocess.run(["gradlew.bat", f"generateLibraryDefinitions{build_type.capitalize()}"],
|
||||
capture_output=True)
|
||||
|
||||
data = json.load(
|
||||
open(f"app/build/generated/aboutLibraries/{build_type}/res/raw/aboutlibraries.json"))
|
||||
|
||||
with open(f"licenses_{build_type}.csv", "w") as f:
|
||||
with open(f"licenses_{build_type}_appning.csv", "w") as f:
|
||||
f.write("component_name;license_title;license_url;public_repository;copyrights\n")
|
||||
for lib in data["libraries"]:
|
||||
license = data["licenses"][lib["licenses"][0]] if len(lib["licenses"]) > 0 else None
|
||||
231
_misc/taginfo.json
Normal file
231
_misc/taginfo.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"data_format": 1,
|
||||
"data_url": "https://raw.githubusercontent.com/ev-map/evmap/master/_misc/taginfo.json",
|
||||
"data_updated": "20250921T140000Z",
|
||||
"project": {
|
||||
"name": "EVMap",
|
||||
"description": "Find electric vehicle chargers comfortably using your Android phone.",
|
||||
"project_url": "https://ev-map.app/",
|
||||
"doc_url": "https://github.com/ev-map/evmap-osm",
|
||||
"icon_url": "https://avatars.githubusercontent.com/u/115927597?s=32",
|
||||
"contact_name": "Johan von Forstner",
|
||||
"contact_email": "evmap@vonforst.net"
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"key": "amenity",
|
||||
"value": "charging_station",
|
||||
"description": "Used to display charging stations."
|
||||
},
|
||||
{
|
||||
"key": "name"
|
||||
},
|
||||
{
|
||||
"key": "network"
|
||||
},
|
||||
{
|
||||
"key": "authentication:none",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "operator"
|
||||
},
|
||||
{
|
||||
"key": "description"
|
||||
},
|
||||
{
|
||||
"key": "website"
|
||||
},
|
||||
{
|
||||
"key": "addr:city"
|
||||
},
|
||||
{
|
||||
"key": "addr:country"
|
||||
},
|
||||
{
|
||||
"key": "addr:postcode"
|
||||
},
|
||||
{
|
||||
"key": "addr:street"
|
||||
},
|
||||
{
|
||||
"key": "addr:housenumber"
|
||||
},
|
||||
{
|
||||
"key": "addr:housename"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1_combo"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1_combo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_cable"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_cable:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_combo"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_combo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:chademo"
|
||||
},
|
||||
{
|
||||
"key": "socket:chademo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_standard"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_standard:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger_ccs"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger_ccs:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_blue"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_blue:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_16a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_16a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_32a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_32a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_63a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_63a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_125a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_125a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:schuko"
|
||||
},
|
||||
{
|
||||
"key": "socket:schuko:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t13"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t13:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t15"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t15:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t23"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t23:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t25"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t25:output"
|
||||
},
|
||||
{
|
||||
"key": "opening_hours",
|
||||
"value": "24/7"
|
||||
},
|
||||
{
|
||||
"key": "fee",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "fee",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "parking:fee",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "parking:fee",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "charge"
|
||||
},
|
||||
{
|
||||
"key": "charge:conditional"
|
||||
},
|
||||
{
|
||||
"key": "image"
|
||||
},
|
||||
{
|
||||
"key": "image:0"
|
||||
},
|
||||
{
|
||||
"key": "image:1"
|
||||
},
|
||||
{
|
||||
"key": "image:2"
|
||||
},
|
||||
{
|
||||
"key": "image:3"
|
||||
},
|
||||
{
|
||||
"key": "image:4"
|
||||
},
|
||||
{
|
||||
"key": "image:5"
|
||||
},
|
||||
{
|
||||
"key": "image:6"
|
||||
},
|
||||
{
|
||||
"key": "image:7"
|
||||
},
|
||||
{
|
||||
"key": "image:8"
|
||||
},
|
||||
{
|
||||
"key": "image:9"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import java.util.Base64
|
||||
|
||||
plugins {
|
||||
id("com.adarshr.test-logger") version "3.1.0"
|
||||
id("com.adarshr.test-logger") version "4.0.0"
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-parcelize")
|
||||
@@ -17,18 +17,18 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "net.vonforst.evmap"
|
||||
compileSdk = 35
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
compileSdk = 36
|
||||
minSdk = 23
|
||||
targetSdk = 36
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode = 230
|
||||
versionName = "1.9.6"
|
||||
versionCode = 268
|
||||
versionName = "2.0.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
val isRunningOnCI = System.getenv("CI") == "true"
|
||||
@@ -129,12 +129,34 @@ android {
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
applicationVariants.all {
|
||||
var evmapKey =
|
||||
System.getenv("EVMAP_API_KEY") ?: project.findProperty("EVMAP_API_KEY")?.toString()
|
||||
if (evmapKey == null && project.hasProperty("EVMAP_API_KEY_ENCRYPTED")) {
|
||||
evmapKey = decode(
|
||||
project.findProperty("EVMAP_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (evmapKey != null) {
|
||||
resValue("string", "evmap_key", evmapKey)
|
||||
}
|
||||
val goingelectricKey =
|
||||
System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY")
|
||||
?.toString()
|
||||
if (goingelectricKey != null) {
|
||||
resValue("string", "goingelectric_key", goingelectricKey)
|
||||
}
|
||||
var nobilKey =
|
||||
System.getenv("NOBIL_API_KEY") ?: project.findProperty("NOBIL_API_KEY")?.toString()
|
||||
if (nobilKey == null && project.hasProperty("NOBIL_API_KEY_ENCRYPTED")) {
|
||||
nobilKey = decode(
|
||||
project.findProperty("NOBIL_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (nobilKey != null) {
|
||||
resValue("string", "nobil_key", nobilKey)
|
||||
}
|
||||
var openchargemapKey =
|
||||
System.getenv("OPENCHARGEMAP_API_KEY") ?: project.findProperty("OPENCHARGEMAP_API_KEY")
|
||||
?.toString()
|
||||
@@ -186,18 +208,6 @@ android {
|
||||
if (arcgisKey != null) {
|
||||
resValue("string", "arcgis_key", jawgKey)
|
||||
}
|
||||
var chargepriceKey =
|
||||
System.getenv("CHARGEPRICE_API_KEY") ?: project.findProperty("CHARGEPRICE_API_KEY")
|
||||
?.toString()
|
||||
if (chargepriceKey == null && project.hasProperty("CHARGEPRICE_API_KEY_ENCRYPTED")) {
|
||||
chargepriceKey = decode(
|
||||
project.findProperty("CHARGEPRICE_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (chargepriceKey != null) {
|
||||
resValue("string", "chargeprice_key", chargepriceKey)
|
||||
}
|
||||
var fronyxKey =
|
||||
System.getenv("FRONYX_API_KEY") ?: project.findProperty("FRONYX_API_KEY")?.toString()
|
||||
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
|
||||
@@ -258,18 +268,21 @@ configurations {
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
allowedLicenses = arrayOf(
|
||||
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
||||
"asdkl", // Android SDK
|
||||
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
||||
"Google Maps Platform Terms of Service", // Google Maps SDK
|
||||
"provided without support or warranty", // org.json
|
||||
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
||||
"Bouncy Castle Licence", // bcprov
|
||||
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
||||
)
|
||||
excludeFields = arrayOf("generated")
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
license {
|
||||
allowedLicenses = setOf(
|
||||
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
||||
"asdkl", // Android SDK
|
||||
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
||||
"Google Maps Platform Terms of Service", // Google Maps SDK
|
||||
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
||||
"Bouncy Castle Licence", // bcprov
|
||||
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
||||
)
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
}
|
||||
export {
|
||||
excludeFields = setOf("generated")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -283,102 +296,93 @@ dependencies {
|
||||
val testGoogleImplementation by configurations
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.activity:activity-ktx:1.9.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.7.1")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("androidx.core:core-splashscreen:1.2.0")
|
||||
implementation("androidx.activity:activity-ktx:1.11.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("com.google.android.material:material:1.13.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||
implementation("androidx.browser:browser:1.9.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation("androidx.security:security-crypto:1.1.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.10.5")
|
||||
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
|
||||
implementation("com.squareup.retrofit2:retrofit:3.0.0")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
|
||||
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
|
||||
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
|
||||
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
||||
implementation("io.coil-kt:coil:2.6.0")
|
||||
implementation("io.coil-kt:coil:2.7.0")
|
||||
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
||||
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
||||
implementation("com.airbnb.android:lottie:4.1.0")
|
||||
implementation("com.airbnb.android:lottie:6.6.10")
|
||||
implementation("io.michaelrocks.bimap:bimap:1.1.0")
|
||||
implementation("com.google.guava:guava:29.0-android")
|
||||
implementation("com.github.pengrad:mapscaleview:1.6.0")
|
||||
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
||||
implementation("com.github.erfansn:locale-config-x:1.0.1")
|
||||
implementation("com.github.ev-map:locale-config-x:58b036abf4")
|
||||
|
||||
// Android Auto
|
||||
val carAppVersion = "1.7.0-rc01"
|
||||
val carAppVersion = "1.7.0"
|
||||
implementation("androidx.car.app:app:$carAppVersion")
|
||||
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
||||
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
||||
|
||||
// AnyMaps
|
||||
val anyMapsVersion = "1174ef9375"
|
||||
val anyMapsVersion = "65e06c4c9a"
|
||||
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:19.0.0")
|
||||
googleImplementation("com.google.android.gms:play-services-maps:19.2.0")
|
||||
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
|
||||
// duplicates classes from mapbox-sdk-services
|
||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||
}
|
||||
implementation("org.maplibre.gl:android-sdk:10.3.4") {
|
||||
implementation("org.maplibre.gl:android-sdk:10.3.5") {
|
||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||
}
|
||||
|
||||
// Google Places
|
||||
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0")
|
||||
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.8.0")
|
||||
|
||||
// navigation library
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||
|
||||
// viewmodel library
|
||||
val lifecycle_version = "2.8.1"
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
|
||||
val lifecycleVersion = "2.9.2"
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
||||
|
||||
// room library
|
||||
val room_version = "2.7.1"
|
||||
implementation("androidx.room:room-runtime:$room_version")
|
||||
ksp("androidx.room:room-compiler:$room_version")
|
||||
implementation("androidx.room:room-ktx:$room_version")
|
||||
implementation("com.github.anboralabs:spatia-room:0.3.0") {
|
||||
exclude("com.github.dalgarins", "android-spatialite")
|
||||
}
|
||||
// forked version with upgraded sqlite & libxml
|
||||
// https://github.com/dalgarins/android-spatialite/pull/10
|
||||
implementation("com.github.ev-map:android-spatialite:31495dcd81")
|
||||
val roomVersion = "2.7.2"
|
||||
implementation("androidx.room:room-runtime:$roomVersion")
|
||||
ksp("androidx.room:room-compiler:$roomVersion")
|
||||
implementation("androidx.room:room-ktx:$roomVersion")
|
||||
implementation("com.github.anboralabs:spatia-room:1.0.1")
|
||||
|
||||
// billing library
|
||||
val billing_version = "7.0.0"
|
||||
googleImplementation("com.android.billingclient:billing:$billing_version")
|
||||
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
|
||||
val billingVersion = "7.0.0"
|
||||
googleImplementation("com.android.billingclient:billing:$billingVersion")
|
||||
googleImplementation("com.android.billingclient:billing-ktx:$billingVersion")
|
||||
|
||||
// ACRA (crash reporting)
|
||||
val acraVersion = "5.11.1"
|
||||
val acraVersion = "5.12.0"
|
||||
implementation("ch.acra:acra-http:$acraVersion")
|
||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// debug tools
|
||||
debugImplementation("com.facebook.flipper:flipper:0.238.0")
|
||||
debugImplementation("com.facebook.soloader:soloader:0.10.5")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0")
|
||||
debugImplementation("com.jakewharton.timber:timber:5.0.1")
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
|
||||
|
||||
@@ -386,20 +390,18 @@ dependencies {
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||
//noinspection GradleDependency
|
||||
testImplementation("org.json:json:20080701")
|
||||
testImplementation("org.robolectric:robolectric:4.11.1")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
testImplementation("org.robolectric:robolectric:4.16")
|
||||
testImplementation("androidx.test:core:1.7.0")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
testImplementation("androidx.car.app:app-testing:$carAppVersion")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
||||
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
|
||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
}
|
||||
|
||||
fun decode(s: String, key: String): String {
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
{
|
||||
"fieldPath": "network",
|
||||
"columnName": "network",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
@@ -53,8 +52,7 @@
|
||||
{
|
||||
"fieldPath": "editUrl",
|
||||
"columnName": "editUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "verified",
|
||||
@@ -65,62 +63,52 @@
|
||||
{
|
||||
"fieldPath": "barrierFree",
|
||||
"columnName": "barrierFree",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "operator",
|
||||
"columnName": "operator",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "generalInformation",
|
||||
"columnName": "generalInformation",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amenities",
|
||||
"columnName": "amenities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locationDescription",
|
||||
"columnName": "locationDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photos",
|
||||
"columnName": "photos",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargecards",
|
||||
"columnName": "chargecards",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "license",
|
||||
"columnName": "license",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkUrl",
|
||||
"columnName": "networkUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerUrl",
|
||||
"columnName": "chargerUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
@@ -143,188 +131,157 @@
|
||||
{
|
||||
"fieldPath": "address.city",
|
||||
"columnName": "city",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.country",
|
||||
"columnName": "country",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.postcode",
|
||||
"columnName": "postcode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.street",
|
||||
"columnName": "street",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.created",
|
||||
"columnName": "fault_report_created",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.description",
|
||||
"columnName": "fault_report_description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.twentyfourSeven",
|
||||
"columnName": "twentyfourSeven",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.start",
|
||||
"columnName": "mostart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.end",
|
||||
"columnName": "moend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.start",
|
||||
"columnName": "tustart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.end",
|
||||
"columnName": "tuend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.start",
|
||||
"columnName": "westart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.end",
|
||||
"columnName": "weend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.start",
|
||||
"columnName": "thstart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.end",
|
||||
"columnName": "thend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.start",
|
||||
"columnName": "frstart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.end",
|
||||
"columnName": "frend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.start",
|
||||
"columnName": "sastart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.end",
|
||||
"columnName": "saend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.start",
|
||||
"columnName": "sustart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.end",
|
||||
"columnName": "suend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.start",
|
||||
"columnName": "hostart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.end",
|
||||
"columnName": "hoend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freecharging",
|
||||
"columnName": "freecharging",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freeparking",
|
||||
"columnName": "freeparking",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionShort",
|
||||
"columnName": "descriptionShort",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionLong",
|
||||
"columnName": "descriptionLong",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.country",
|
||||
"columnName": "chargepricecountry",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.network",
|
||||
"columnName": "chargepricenetwork",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.plugTypes",
|
||||
"columnName": "chargepriceplugTypes",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -333,9 +290,7 @@
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Favorite",
|
||||
@@ -642,8 +597,7 @@
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "RecentAutocompletePlace",
|
||||
@@ -688,8 +642,7 @@
|
||||
{
|
||||
"fieldPath": "viewport",
|
||||
"columnName": "viewport",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "types",
|
||||
@@ -704,9 +657,7 @@
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEPlug",
|
||||
@@ -724,9 +675,7 @@
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GENetwork",
|
||||
@@ -744,9 +693,7 @@
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEChargeCard",
|
||||
@@ -776,9 +723,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMConnectionType",
|
||||
@@ -799,20 +744,17 @@
|
||||
{
|
||||
"fieldPath": "formalName",
|
||||
"columnName": "formalName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "discontinued",
|
||||
"columnName": "discontinued",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "obsolete",
|
||||
"columnName": "obsolete",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -820,9 +762,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMCountry",
|
||||
@@ -843,8 +783,7 @@
|
||||
{
|
||||
"fieldPath": "continentCode",
|
||||
"columnName": "continentCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
@@ -858,9 +797,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMOperator",
|
||||
@@ -875,8 +812,7 @@
|
||||
{
|
||||
"fieldPath": "websiteUrl",
|
||||
"columnName": "websiteUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
@@ -887,20 +823,17 @@
|
||||
{
|
||||
"fieldPath": "contactEmail",
|
||||
"columnName": "contactEmail",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone1",
|
||||
"columnName": "contactTelephone1",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone2",
|
||||
"columnName": "contactTelephone2",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -908,9 +841,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OSMNetwork",
|
||||
@@ -928,9 +859,7 @@
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "SavedRegion",
|
||||
@@ -957,8 +886,7 @@
|
||||
{
|
||||
"fieldPath": "filters",
|
||||
"columnName": "filters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
@@ -969,8 +897,7 @@
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -990,11 +917,9 @@
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b2b3f39d450f4f7c8280ca850161bbb3')"
|
||||
|
||||
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/27.json
Normal file
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/27.json
Normal file
@@ -0,0 +1,938 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 27,
|
||||
"identityHash": "84f71cce385c444726ba336834ddf6b4",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ChargeLocation",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `accessibility` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `coordinatesProjected` BLOB NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coordinates",
|
||||
"columnName": "coordinates",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepoints",
|
||||
"columnName": "chargepoints",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "network",
|
||||
"columnName": "network",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSourceUrl",
|
||||
"columnName": "dataSourceUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "editUrl",
|
||||
"columnName": "editUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "verified",
|
||||
"columnName": "verified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "barrierFree",
|
||||
"columnName": "barrierFree",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "operator",
|
||||
"columnName": "operator",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "generalInformation",
|
||||
"columnName": "generalInformation",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amenities",
|
||||
"columnName": "amenities",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locationDescription",
|
||||
"columnName": "locationDescription",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photos",
|
||||
"columnName": "photos",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargecards",
|
||||
"columnName": "chargecards",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessibility",
|
||||
"columnName": "accessibility",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "license",
|
||||
"columnName": "license",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkUrl",
|
||||
"columnName": "networkUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerUrl",
|
||||
"columnName": "chargerUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
"columnName": "timeRetrieved",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coordinatesProjected",
|
||||
"columnName": "coordinatesProjected",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.city",
|
||||
"columnName": "city",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.country",
|
||||
"columnName": "country",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.postcode",
|
||||
"columnName": "postcode",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.street",
|
||||
"columnName": "street",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.created",
|
||||
"columnName": "fault_report_created",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.description",
|
||||
"columnName": "fault_report_description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.twentyfourSeven",
|
||||
"columnName": "twentyfourSeven",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.start",
|
||||
"columnName": "mostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.end",
|
||||
"columnName": "moend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.start",
|
||||
"columnName": "tustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.end",
|
||||
"columnName": "tuend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.start",
|
||||
"columnName": "westart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.end",
|
||||
"columnName": "weend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.start",
|
||||
"columnName": "thstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.end",
|
||||
"columnName": "thend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.start",
|
||||
"columnName": "frstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.end",
|
||||
"columnName": "frend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.start",
|
||||
"columnName": "sastart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.end",
|
||||
"columnName": "saend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.start",
|
||||
"columnName": "sustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.end",
|
||||
"columnName": "suend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.start",
|
||||
"columnName": "hostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.end",
|
||||
"columnName": "hoend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freecharging",
|
||||
"columnName": "freecharging",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freeparking",
|
||||
"columnName": "freeparking",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionShort",
|
||||
"columnName": "descriptionShort",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionLong",
|
||||
"columnName": "descriptionLong",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.country",
|
||||
"columnName": "chargepricecountry",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.network",
|
||||
"columnName": "chargepricenetwork",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.plugTypes",
|
||||
"columnName": "chargepriceplugTypes",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Favorite",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "favoriteId",
|
||||
"columnName": "favoriteId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerId",
|
||||
"columnName": "chargerId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerDataSource",
|
||||
"columnName": "chargerDataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"favoriteId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Favorite_chargerId_chargerDataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"chargerId",
|
||||
"chargerDataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "ChargeLocation",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"chargerId",
|
||||
"chargerDataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "BooleanFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BooleanFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "MultipleChoiceFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "values",
|
||||
"columnName": "values",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "all",
|
||||
"columnName": "all",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "SliderFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SliderFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "FilterProfile",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "order",
|
||||
"columnName": "order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"dataSource",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_FilterProfile_dataSource_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"dataSource",
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "RecentAutocompletePlace",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "primaryText",
|
||||
"columnName": "primaryText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "secondaryText",
|
||||
"columnName": "secondaryText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "latLng",
|
||||
"columnName": "latLng",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewport",
|
||||
"columnName": "viewport",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "types",
|
||||
"columnName": "types",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEPlug",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GENetwork",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEChargeCard",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMConnectionType",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "formalName",
|
||||
"columnName": "formalName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "discontinued",
|
||||
"columnName": "discontinued",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "obsolete",
|
||||
"columnName": "obsolete",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMCountry",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isoCode",
|
||||
"columnName": "isoCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "continentCode",
|
||||
"columnName": "continentCode",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMOperator",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "websiteUrl",
|
||||
"columnName": "websiteUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactEmail",
|
||||
"columnName": "contactEmail",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone1",
|
||||
"columnName": "contactTelephone1",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone2",
|
||||
"columnName": "contactTelephone2",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OSMNetwork",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "SavedRegion",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "region",
|
||||
"columnName": "region",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
"columnName": "timeRetrieved",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filters",
|
||||
"columnName": "filters",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SavedRegion_filters_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"filters",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84f71cce385c444726ba336834ddf6b4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json
Normal file
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json
Normal file
@@ -0,0 +1,938 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 28,
|
||||
"identityHash": "84f71cce385c444726ba336834ddf6b4",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ChargeLocation",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `accessibility` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `coordinatesProjected` BLOB NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coordinates",
|
||||
"columnName": "coordinates",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepoints",
|
||||
"columnName": "chargepoints",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "network",
|
||||
"columnName": "network",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSourceUrl",
|
||||
"columnName": "dataSourceUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "editUrl",
|
||||
"columnName": "editUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "verified",
|
||||
"columnName": "verified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "barrierFree",
|
||||
"columnName": "barrierFree",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "operator",
|
||||
"columnName": "operator",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "generalInformation",
|
||||
"columnName": "generalInformation",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amenities",
|
||||
"columnName": "amenities",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locationDescription",
|
||||
"columnName": "locationDescription",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photos",
|
||||
"columnName": "photos",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargecards",
|
||||
"columnName": "chargecards",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessibility",
|
||||
"columnName": "accessibility",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "license",
|
||||
"columnName": "license",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkUrl",
|
||||
"columnName": "networkUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerUrl",
|
||||
"columnName": "chargerUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
"columnName": "timeRetrieved",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coordinatesProjected",
|
||||
"columnName": "coordinatesProjected",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.city",
|
||||
"columnName": "city",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.country",
|
||||
"columnName": "country",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.postcode",
|
||||
"columnName": "postcode",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.street",
|
||||
"columnName": "street",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.created",
|
||||
"columnName": "fault_report_created",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.description",
|
||||
"columnName": "fault_report_description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.twentyfourSeven",
|
||||
"columnName": "twentyfourSeven",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.start",
|
||||
"columnName": "mostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.end",
|
||||
"columnName": "moend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.start",
|
||||
"columnName": "tustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.end",
|
||||
"columnName": "tuend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.start",
|
||||
"columnName": "westart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.end",
|
||||
"columnName": "weend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.start",
|
||||
"columnName": "thstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.end",
|
||||
"columnName": "thend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.start",
|
||||
"columnName": "frstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.end",
|
||||
"columnName": "frend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.start",
|
||||
"columnName": "sastart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.end",
|
||||
"columnName": "saend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.start",
|
||||
"columnName": "sustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.end",
|
||||
"columnName": "suend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.start",
|
||||
"columnName": "hostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.end",
|
||||
"columnName": "hoend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freecharging",
|
||||
"columnName": "freecharging",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freeparking",
|
||||
"columnName": "freeparking",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionShort",
|
||||
"columnName": "descriptionShort",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionLong",
|
||||
"columnName": "descriptionLong",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.country",
|
||||
"columnName": "chargepricecountry",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.network",
|
||||
"columnName": "chargepricenetwork",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.plugTypes",
|
||||
"columnName": "chargepriceplugTypes",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Favorite",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "favoriteId",
|
||||
"columnName": "favoriteId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerId",
|
||||
"columnName": "chargerId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerDataSource",
|
||||
"columnName": "chargerDataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"favoriteId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Favorite_chargerId_chargerDataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"chargerId",
|
||||
"chargerDataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "ChargeLocation",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"chargerId",
|
||||
"chargerDataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "BooleanFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BooleanFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "MultipleChoiceFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "values",
|
||||
"columnName": "values",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "all",
|
||||
"columnName": "all",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "SliderFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SliderFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "FilterProfile",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "order",
|
||||
"columnName": "order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"dataSource",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_FilterProfile_dataSource_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"dataSource",
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "RecentAutocompletePlace",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "primaryText",
|
||||
"columnName": "primaryText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "secondaryText",
|
||||
"columnName": "secondaryText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "latLng",
|
||||
"columnName": "latLng",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewport",
|
||||
"columnName": "viewport",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "types",
|
||||
"columnName": "types",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEPlug",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GENetwork",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEChargeCard",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMConnectionType",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "formalName",
|
||||
"columnName": "formalName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "discontinued",
|
||||
"columnName": "discontinued",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "obsolete",
|
||||
"columnName": "obsolete",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMCountry",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isoCode",
|
||||
"columnName": "isoCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "continentCode",
|
||||
"columnName": "continentCode",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMOperator",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "websiteUrl",
|
||||
"columnName": "websiteUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactEmail",
|
||||
"columnName": "contactEmail",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone1",
|
||||
"columnName": "contactTelephone1",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone2",
|
||||
"columnName": "contactTelephone2",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OSMNetwork",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "SavedRegion",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "region",
|
||||
"columnName": "region",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
"columnName": "timeRetrieved",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filters",
|
||||
"columnName": "filters",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SavedRegion_filters_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"filters",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84f71cce385c444726ba336834ddf6b4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ class ChargeLocationsDaoTest {
|
||||
"https://google.com",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
@@ -68,7 +69,7 @@ class ChargeLocationsDaoTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, null, null, null, null, null, null, Instant.now(), true
|
||||
null, null, null, null, null, null, null, null, Instant.now(), true
|
||||
)
|
||||
}
|
||||
runBlocking {
|
||||
|
||||
5
app/src/automotive/res/values-sv/strings.xml
Normal file
5
app/src/automotive/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Tillåt</string>
|
||||
<string name="auto_location_permission_needed">Du måste tillåta platsåtkomst för att använda EVMap i din bil.</string>
|
||||
</resources>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||
android:exported="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -2,44 +2,15 @@ package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
|
||||
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()
|
||||
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
||||
// Flipper does not work during unit tests - so check whether we are running tests first
|
||||
var isRunningTest = true
|
||||
try {
|
||||
Class.forName("org.junit.Test")
|
||||
} catch (e: ClassNotFoundException) {
|
||||
isRunningTest = false
|
||||
}
|
||||
|
||||
if (!isRunningTest) {
|
||||
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?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,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="app_name">EVMap (debug)</string>
|
||||
</resources>
|
||||
6
app/src/foss/res/values-sv/strings.xml
Normal file
6
app/src/foss/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Har du nytta av EVMap? Stöd utvecklingen genom att skicka en donation till utvecklaren.</string>
|
||||
<string name="donate_paypal">Donera med PayPal</string>
|
||||
<string name="data_sources_hint">Kartdata i appen tillhandahålls av OpenStreetMap.</string>
|
||||
</resources>
|
||||
5
app/src/google/res/values-sv/strings.xml
Normal file
5
app/src/google/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Har du nytta av EVMap? Stöd utvecklingen genom att skicka en donation till utvecklaren.\n\nGoogle tar 15% av alla donationer.</string>
|
||||
<string name="data_sources_hint">I inställningarna kan du välja mellan Google Maps och OpenStreetMap som kartleverantör.</string>
|
||||
</resources>
|
||||
@@ -18,6 +18,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.navigation.NavController
|
||||
@@ -55,6 +56,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.enableEdgeToEdge(window)
|
||||
|
||||
setContentView(R.layout.activity_maps)
|
||||
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
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
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import net.vonforst.evmap.BR
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
|
||||
import net.vonforst.evmap.databinding.ItemChargepriceBinding
|
||||
import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
|
||||
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.CheckableConstraintLayout
|
||||
import java.time.Instant
|
||||
|
||||
interface Equatable {
|
||||
@@ -106,141 +95,4 @@ class ConnectorDetailsAdapter : DataBindingAdapter<ConnectorDetailsAdapter.Conne
|
||||
Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.dialog_connector_details_item
|
||||
}
|
||||
|
||||
class ChargepriceAdapter :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
|
||||
val viewPool = RecyclerView.RecycledViewPool()
|
||||
var meta: ChargepriceChargepointMeta? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
var myTariffs: Set<String>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
var myTariffsAll: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<ChargePrice> {
|
||||
val holder = super.onCreateViewHolder(parent, viewType)
|
||||
val binding = holder.binding as ItemChargepriceBinding
|
||||
binding.rvTags.apply {
|
||||
adapter = ChargepriceTagsAdapter()
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
|
||||
recycleChildrenOnDetach = true
|
||||
}
|
||||
itemAnimator = null
|
||||
setRecycledViewPool(viewPool)
|
||||
}
|
||||
return holder
|
||||
}
|
||||
|
||||
override fun bind(holder: ViewHolder<ChargePrice>, item: ChargePrice) {
|
||||
super.bind(holder, item)
|
||||
(holder.binding as ItemChargepriceBinding).apply {
|
||||
this.meta = this@ChargepriceAdapter.meta
|
||||
this.myTariffs = this@ChargepriceAdapter.myTariffs
|
||||
this.myTariffsAll = this@ChargepriceAdapter.myTariffsAll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
private var checkedItem: Int? = 0
|
||||
|
||||
var enabledConnectors: List<String>? = null
|
||||
get() = field
|
||||
set(value) {
|
||||
field = value
|
||||
checkedItem?.let {
|
||||
if (value != null && getItem(it).type !in value) {
|
||||
checkedItem = currentList.indexOfFirst {
|
||||
it.type in value
|
||||
}.takeIf { it != -1 }
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem())
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector_button
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<Chargepoint>, position: Int) {
|
||||
val item = getItem(position)
|
||||
super.bind(holder, item)
|
||||
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 { _, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
getCheckedItem()?.let { onCheckedItemChangedListener?.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCheckedItem(): Chargepoint? = checkedItem?.let { getItem(it) }
|
||||
|
||||
fun setCheckedItem(item: Chargepoint?) {
|
||||
checkedItem = item?.let { currentList.indexOf(item) }.takeIf { it != -1 }
|
||||
}
|
||||
|
||||
var onCheckedItemChangedListener: ((Chargepoint?) -> Unit)? = null
|
||||
}
|
||||
|
||||
class ChargepriceTagsAdapter() :
|
||||
DataBindingAdapter<ChargepriceTag>() {
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag
|
||||
}
|
||||
|
||||
class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||
private var checkedItem: ChargepriceCar? = null
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_vehicle_chip
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<ChargepriceCar>, position: Int) {
|
||||
val item = getItem(position)
|
||||
super.bind(holder, item)
|
||||
val binding = holder.binding as ItemChargepriceVehicleChipBinding
|
||||
val root = binding.root as Chip
|
||||
root.isChecked = checkedItem == item
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked && item != checkedItem) {
|
||||
checkedItem = item
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCheckedItem(): ChargepriceCar? = checkedItem
|
||||
|
||||
fun setCheckedItem(item: ChargepriceCar?) {
|
||||
checkedItem = item
|
||||
}
|
||||
|
||||
var onCheckedItemChangedListener: ((ChargepriceCar?) -> Unit)? = null
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.nobil.NobilApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
|
||||
import net.vonforst.evmap.model.*
|
||||
@@ -94,6 +95,13 @@ fun Context.stringProvider() = object : StringProvider {
|
||||
|
||||
fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||
return when (type) {
|
||||
"nobil" -> {
|
||||
NobilApiWrapper(
|
||||
ctx.getString(
|
||||
R.string.nobil_key
|
||||
)
|
||||
)
|
||||
}
|
||||
"openchargemap" -> {
|
||||
OpenChargeMapApiWrapper(
|
||||
ctx.getString(
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
|
||||
class RateLimitInterceptor : Interceptor {
|
||||
private val rateLimiter = RateLimiter.create(3.0)
|
||||
private val rateLimiter = SimpleRateLimiter(3.0)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (request.url.host == "ui-map.shellrecharge.com") {
|
||||
// limit requests sent to NewMotion to 3 per second
|
||||
rateLimiter.acquire(1)
|
||||
rateLimiter.acquire()
|
||||
|
||||
var response: Response = chain.proceed(request)
|
||||
// 403 is how the NewMotion API indicates a rate limit error
|
||||
@@ -30,4 +32,27 @@ class RateLimitInterceptor : Interceptor {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SimpleRateLimiter(private val permitsPerSecond: Double) {
|
||||
private val interval: Duration = (1.0 / permitsPerSecond).seconds
|
||||
private var nextAvailable = TimeSource.Monotonic.markNow()
|
||||
|
||||
@Synchronized
|
||||
fun acquire() {
|
||||
val now = TimeSource.Monotonic.markNow()
|
||||
if (now < nextAvailable) {
|
||||
val waitTime = nextAvailable - now
|
||||
waitTime.sleep()
|
||||
nextAvailable += interval
|
||||
} else {
|
||||
nextAvailable = now + interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Duration.sleep() {
|
||||
if (this.isPositive()) {
|
||||
Thread.sleep(this.inWholeMilliseconds, (this.inWholeNanoseconds % 1_000_000).toInt())
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,14 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.experimental.ExperimentalTypeInference
|
||||
import kotlin.math.abs
|
||||
|
||||
operator fun <T> JSONArray.iterator(): Iterator<T> =
|
||||
(0 until length()).asSequence().map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
get(it) as T
|
||||
}.iterator()
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
suspend fun Call.await(): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
continuation.resume(response) {}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
})
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
cancel()
|
||||
} catch (ex: Throwable) {
|
||||
//Ignore cancel exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val plugNames = mapOf(
|
||||
Chargepoint.TYPE_1 to R.string.plug_type_1,
|
||||
Chargepoint.TYPE_2_UNKNOWN to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2_tethered,
|
||||
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_3A to R.string.plug_type_3a,
|
||||
Chargepoint.TYPE_3C to R.string.plug_type_3c,
|
||||
|
||||
@@ -175,6 +175,7 @@ class AvailabilityRepository(context: Context) {
|
||||
RheinenergieAvailabilityDetector(okhttp),
|
||||
teslaOwnerAvailabilityDetector,
|
||||
TeslaGuestAvailabilityDetector(okhttp),
|
||||
NobilAvailabilityDetector(okhttp, context),
|
||||
EnBwAvailabilityDetector(okhttp),
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
)
|
||||
|
||||
@@ -203,7 +203,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
"Typ 3A" -> Chargepoint.TYPE_3A
|
||||
"Typ 3C \"Scame\"" -> Chargepoint.TYPE_3C
|
||||
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"Typ 1" -> Chargepoint.TYPE_1
|
||||
"Typ 1 Steckdose" -> Chargepoint.TYPE_1
|
||||
"Steckdose(D)" -> Chargepoint.SCHUKO
|
||||
"CCS (Typ 1)" -> Chargepoint.CCS_TYPE_1 // US CCS, aka type1_combo
|
||||
"CCS (Typ 2)" -> Chargepoint.CCS_TYPE_2 // EU CCS, aka type2_combo
|
||||
@@ -266,6 +266,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
"Spanien",
|
||||
"Tschechien"
|
||||
) && charger.network != "Tesla Supercharger"
|
||||
"nobil" -> charger.network != "Tesla"
|
||||
"openchargemap" -> country in listOf(
|
||||
"DE",
|
||||
"AT",
|
||||
|
||||
@@ -61,8 +61,9 @@ interface NewMotionApi {
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int) {
|
||||
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int, val maxElectricPower: Double?) {
|
||||
fun getPower(): Double {
|
||||
maxElectricPower?.let { return it }
|
||||
val phases = when (powerType) {
|
||||
"AC1Phase" -> 1
|
||||
"AC3Phase" -> 3
|
||||
@@ -220,6 +221,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
// NewMotion is our fallback
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network != "Tesla Supercharger"
|
||||
"nobil" -> charger.network != "Tesla"
|
||||
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
|
||||
"openstreetmap" -> charger.operator !in listOf("Tesla, Inc.", "Tesla")
|
||||
else -> false
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Path
|
||||
import java.time.Instant
|
||||
|
||||
internal class InstantStringAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): Instant? = value?.let {
|
||||
Instant.parse(value)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Instant?): String? = value?.toString()
|
||||
}
|
||||
|
||||
interface NobilRealtimeApi {
|
||||
@GET("{nobilId}")
|
||||
suspend fun getAvailability(
|
||||
@Path("nobilId") nobilId: String,
|
||||
@Header("X-Api-Key") apiKey: String
|
||||
): List<NobilChargepointState>
|
||||
|
||||
companion object {
|
||||
fun create(client: OkHttpClient): NobilRealtimeApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl("https://api.ev-map.app/nobil/api/realtime/")
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(InstantStringAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(NobilRealtimeApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargepointState(
|
||||
val evseUid: String,
|
||||
val status: String,
|
||||
val timestamp: Instant
|
||||
)
|
||||
|
||||
class NobilAvailabilityDetector(client: OkHttpClient, context: Context) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
val api = NobilRealtimeApi.create(client)
|
||||
val apiKey = context.getString(R.string.evmap_key)
|
||||
|
||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||
val nobilId = when (location.address?.country) {
|
||||
"Norway" -> "NOR"
|
||||
"Sweden" -> "SWE"
|
||||
else -> throw AvailabilityDetectorException("nobil: unsupported country")
|
||||
} + "_%05d".format(location.id)
|
||||
|
||||
val availability = api.getAvailability(nobilId, apiKey)
|
||||
if (availability.isEmpty()) {
|
||||
throw AvailabilityDetectorException("nobil: no real-time data available")
|
||||
}
|
||||
return ChargeLocationStatus(
|
||||
location.chargepointsMerged.associateWith { cp ->
|
||||
cp.evseUIds!!.map { evseUId ->
|
||||
when (availability.find { it.evseUid == evseUId }?.status) {
|
||||
"AVAILABLE" -> ChargepointStatus.AVAILABLE
|
||||
"BLOCKED" -> ChargepointStatus.OCCUPIED
|
||||
"CHARGING" -> ChargepointStatus.CHARGING
|
||||
"INOPERATIVE" -> ChargepointStatus.FAULTED
|
||||
"OUTOFORDER" -> ChargepointStatus.FAULTED
|
||||
"PLANNED" -> ChargepointStatus.FAULTED
|
||||
"REMOVED" -> ChargepointStatus.FAULTED
|
||||
"RESERVED" -> ChargepointStatus.OCCUPIED
|
||||
"UNKNOWN" -> ChargepointStatus.UNKNOWN
|
||||
else -> ChargepointStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
},
|
||||
"Nobil",
|
||||
location.chargepointsMerged.associateWith { cp ->
|
||||
if (cp.evseIds != null) cp.evseIds.map { it ?: "??" } else listOf()
|
||||
},
|
||||
lastChange = location.chargepointsMerged.associateWith { cp ->
|
||||
cp.evseUIds!!.map { evseUId ->
|
||||
availability.find { it.evseUid == evseUId }?.timestamp
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"nobil" -> charger.chargepoints.any { it.evseUIds?.isNotEmpty() == true }
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,40 @@ class TeslaGuestAvailabilityDetector(
|
||||
}
|
||||
val details = detailsA.await()
|
||||
|
||||
if (location.dataSource == "nobil") {
|
||||
// TODO: Lots of copy & paste here. The main difference for nobil data
|
||||
// is that V2 chargers don't have duplicated connectors.
|
||||
var detailsSorted = details.chargerList
|
||||
.sortedBy { c -> c.labelLetter }
|
||||
.sortedBy { c -> c.labelNumber }
|
||||
|
||||
if (detailsSorted.size != location.chargepoints.size) {
|
||||
// TODO: Tesla data could also be missing for connectors
|
||||
throw AvailabilityDetectorException("charger has unknown connectors")
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
mutableMapOf<Chargepoint, List<TeslaChargingGuestGraphQlApi.ChargerDetail>>()
|
||||
var i = 0
|
||||
for (connector in location.chargepointsMerged) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
|
||||
|
||||
val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
labels = labelsMap,
|
||||
extraData = pricing
|
||||
)
|
||||
}
|
||||
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
@@ -166,6 +200,7 @@ class TeslaGuestAvailabilityDetector(
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"nobil" -> charger.network == "Tesla"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
||||
else -> false
|
||||
|
||||
@@ -67,7 +67,54 @@ class TeslaOwnerAvailabilityDetector(
|
||||
)
|
||||
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
if (location.dataSource == "nobil") {
|
||||
// TODO: Lots of copy & paste here. The main difference for nobil data
|
||||
// is that V2 chargers don't have duplicated connectors.s
|
||||
val chargerDetails = details.siteDynamic.chargerDetails
|
||||
val chargers = details.siteStatic.chargers.associateBy { it.id }
|
||||
var detailsSorted = chargerDetails
|
||||
.sortedBy { c -> c.charger.labelLetter ?: chargers[c.charger.id]?.labelLetter }
|
||||
.sortedBy { c -> c.charger.labelNumber ?: chargers[c.charger.id]?.labelNumber }
|
||||
if (detailsSorted.size != location.chargepoints.size) {
|
||||
// TODO: Code below suggests tesla data could also be missing for
|
||||
// connectors
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
|
||||
val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
|
||||
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
|
||||
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
|
||||
val data = cph.data.toMutableList()
|
||||
Collections.rotate(data, -index)
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
emptyMap<Chargepoint, List<TeslaChargingOwnershipGraphQlApi.ChargerDetail>>().toMutableMap()
|
||||
var i = 0
|
||||
for (connector in location.chargepointsMerged) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||
|
||||
val labelsMap = detailsMap.mapValues {
|
||||
it.value.map {
|
||||
it.charger.label?.value ?: chargers[it.charger.id]?.label?.value
|
||||
}
|
||||
}
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
labels = labelsMap,
|
||||
congestionHistogram = congestionHistogram,
|
||||
extraData = details.pricing
|
||||
)
|
||||
}
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
@@ -165,6 +212,7 @@ class TeslaOwnerAvailabilityDetector(
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"nobil" -> charger.network == "Tesla"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
||||
else -> false
|
||||
|
||||
@@ -115,7 +115,7 @@ interface TeslaChargingGuestGraphQlApi {
|
||||
val activeOutages: List<Outage>?,
|
||||
val chargerList: List<ChargerDetail>,
|
||||
val trtId: Long,
|
||||
val maxPowerKw: Int,
|
||||
val maxPowerKw: Int?,
|
||||
val name: String,
|
||||
val pricing: Pricing?,
|
||||
val publicStallCount: Int
|
||||
|
||||
@@ -100,7 +100,8 @@ interface TeslaAuthenticationApi {
|
||||
.appendQueryParameter("code_challenge_method", "S256")
|
||||
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("scope", "openid email offline_access")
|
||||
.appendQueryParameter("scope", "openid email offline_access phone")
|
||||
.appendQueryParameter("is_in_app", "true")
|
||||
.appendQueryParameter("state", "123").build()
|
||||
|
||||
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
|
||||
|
||||
@@ -1,99 +1,16 @@
|
||||
package net.vonforst.evmap.api.chargeprice
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import jsonapi.Document
|
||||
import jsonapi.JsonApiFactory
|
||||
import jsonapi.retrofit.DocumentConverterFactory
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
interface ChargepriceApi {
|
||||
@POST("charge_prices")
|
||||
suspend fun getChargePrices(
|
||||
@Body @jsonapi.retrofit.Document request: ChargepriceRequest,
|
||||
@Header("Accept-Language") language: String
|
||||
): Document<List<ChargePrice>>
|
||||
|
||||
@GET("vehicles")
|
||||
@jsonapi.retrofit.Document
|
||||
suspend fun getVehicles(): List<ChargepriceCar>
|
||||
|
||||
@GET("tariffs")
|
||||
@jsonapi.retrofit.Document
|
||||
suspend fun getTariffs(): List<ChargepriceTariff>
|
||||
|
||||
@POST("user_feedback")
|
||||
suspend fun userFeedback(@Body @jsonapi.retrofit.Document feedback: ChargepriceUserFeedback)
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 1L * 1024 * 1024 // 1MB
|
||||
val supportedLanguages = setOf("de", "en", "fr", "nl")
|
||||
|
||||
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
|
||||
private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
|
||||
|
||||
private val jsonApiAdapterFactory = JsonApiFactory.Builder()
|
||||
.addType(ChargepriceRequest::class.java)
|
||||
.addType(ChargepriceTariff::class.java)
|
||||
.addType(ChargepriceBrand::class.java)
|
||||
.addType(ChargePrice::class.java)
|
||||
.addType(ChargepriceCar::class.java)
|
||||
.build()
|
||||
val moshi = Moshi.Builder()
|
||||
.add(jsonApiAdapterFactory)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ChargepriceUserFeedback::class.java, "type")
|
||||
.withSubtype(ChargepriceMissingPriceFeedback::class.java, "missing_price")
|
||||
.withSubtype(ChargepriceWrongPriceFeedback::class.java, "wrong_price")
|
||||
.withSubtype(ChargepriceMissingVehicleFeedback::class.java, "missing_vehicle")
|
||||
)
|
||||
.build()
|
||||
|
||||
fun create(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.chargeprice.app/v1/",
|
||||
context: Context? = null
|
||||
): ChargepriceApi {
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
val original = chain.request()
|
||||
val new = original.newBuilder()
|
||||
.header("API-Key", apikey)
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
chain.proceed(new)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseurl)
|
||||
.addConverterFactory(DocumentConverterFactory.create())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(ChargepriceApi::class.java)
|
||||
}
|
||||
|
||||
|
||||
fun getChargepriceLanguage(): String {
|
||||
val locale = Locale.getDefault().language
|
||||
return if (supportedLanguages.contains(locale)) {
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
package net.vonforst.evmap.api.chargeprice
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Patterns
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import jsonapi.*
|
||||
import kotlinx.parcelize.Parceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.WriteWith
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
|
||||
@Resource("charge_price_request")
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceRequest(
|
||||
@Json(name = "data_adapter")
|
||||
val dataAdapter: String,
|
||||
val station: ChargepriceStation,
|
||||
val options: ChargepriceOptions,
|
||||
@ToMany("tariffs")
|
||||
val tariffs: List<ChargepriceTariff>? = null,
|
||||
@ToOne("vehicle")
|
||||
val vehicle: ChargepriceCar? = null,
|
||||
@RelationshipsObject var relationships: Relationships? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceStation(
|
||||
val longitude: Double,
|
||||
val latitude: Double,
|
||||
val country: String?,
|
||||
val network: String?,
|
||||
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepoint>
|
||||
) {
|
||||
companion object {
|
||||
fun fromEvmap(
|
||||
charger: ChargeLocation,
|
||||
compatibleConnectors: List<String>,
|
||||
): ChargepriceStation {
|
||||
if (charger.chargepriceData == null) throw IllegalArgumentException()
|
||||
|
||||
val plugTypes =
|
||||
charger.chargepriceData.plugTypes ?: charger.chargepoints.map { it.type }
|
||||
return ChargepriceStation(
|
||||
charger.coordinates.lng,
|
||||
charger.coordinates.lat,
|
||||
charger.chargepriceData.country,
|
||||
charger.chargepriceData.network,
|
||||
charger.chargepoints.zip(plugTypes)
|
||||
.filter { equivalentPlugTypes(it.first.type).any { it in compatibleConnectors } }
|
||||
.map { ChargepriceChargepoint(it.first.power ?: 0.0, it.second) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceChargepoint(
|
||||
val power: Double,
|
||||
val plug: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceOptions(
|
||||
@Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null,
|
||||
val energy: Double? = null,
|
||||
val duration: Int? = null,
|
||||
@Json(name = "battery_range") val batteryRange: List<Double>? = null,
|
||||
@Json(name = "car_ac_phases") val carAcPhases: Int? = null,
|
||||
val currency: String? = null,
|
||||
@Json(name = "start_time") val startTime: Int? = null,
|
||||
@Json(name = "allow_unbalanced_load") val allowUnbalancedLoad: Boolean? = null,
|
||||
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null,
|
||||
@Json(name = "show_price_unavailable") val showPriceUnavailable: Boolean? = null,
|
||||
@Json(name = "show_all_brand_restricted_tariffs") val showAllBrandRestrictedTariffs: Boolean? = null
|
||||
)
|
||||
|
||||
@Resource("tariff")
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceTariff(
|
||||
@Id val id_: String?,
|
||||
val provider: String,
|
||||
val name: String,
|
||||
@Json(name = "direct_payment")
|
||||
val directPayment: Boolean = false,
|
||||
@Json(name = "provider_customer_tariff")
|
||||
val providerCustomerTariff: Boolean = false,
|
||||
@Json(name = "supported_countries")
|
||||
val supportedCountries: Set<String>,
|
||||
@Json(name = "charge_card_id")
|
||||
val chargeCardId: String?, // GE charge card ID
|
||||
) : Parcelable {
|
||||
val id: String
|
||||
get() = id_!!
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource("car")
|
||||
@Parcelize
|
||||
data class ChargepriceCar(
|
||||
@Id val id_: String?,
|
||||
val name: String,
|
||||
val brand: String,
|
||||
|
||||
@Json(name = "dc_charge_ports")
|
||||
val dcChargePorts: List<String>,
|
||||
|
||||
@Json(name = "usable_battery_size")
|
||||
val usableBatterySize: Float,
|
||||
|
||||
@Json(name = "ac_max_power")
|
||||
val acMaxPower: Float,
|
||||
|
||||
@Json(name = "dc_max_power")
|
||||
val dcMaxPower: Float?
|
||||
) : Equatable, Parcelable {
|
||||
fun formatSpecs(): String = buildString {
|
||||
append("%.0f kWh".format(usableBatterySize))
|
||||
append(" | ")
|
||||
append("AC %.0f kW".format(acMaxPower))
|
||||
dcMaxPower?.let {
|
||||
append(" | ")
|
||||
append("DC %.0f kW".format(it))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val acConnectors = listOf(
|
||||
Chargepoint.CEE_BLAU,
|
||||
Chargepoint.CEE_ROT,
|
||||
Chargepoint.SCHUKO,
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG
|
||||
)
|
||||
private val plugMapping = mapOf(
|
||||
"ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"tesla_suc" to Chargepoint.SUPERCHARGER,
|
||||
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"chademo" to Chargepoint.CHADEMO
|
||||
)
|
||||
}
|
||||
|
||||
val id: String
|
||||
get() = id_!!
|
||||
|
||||
val compatibleEvmapConnectors: List<String>
|
||||
get() = dcChargePorts.mapNotNull {
|
||||
plugMapping[it]
|
||||
}.plus(acConnectors)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource("brand")
|
||||
@Parcelize
|
||||
data class ChargepriceBrand(
|
||||
@Id val id: String?
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource("charge_price")
|
||||
@Parcelize
|
||||
data class ChargePrice(
|
||||
val provider: String,
|
||||
@Json(name = "tariff_name")
|
||||
val tariffName: String,
|
||||
val url: String,
|
||||
@Json(name = "monthly_min_sales")
|
||||
val monthlyMinSales: Double = 0.0,
|
||||
@Json(name = "total_monthly_fee")
|
||||
val totalMonthlyFee: Double = 0.0,
|
||||
@Json(name = "flat_rate")
|
||||
val flatRate: Boolean = false,
|
||||
|
||||
@Json(name = "direct_payment")
|
||||
val directPayment: Boolean = false,
|
||||
|
||||
@Json(name = "provider_customer_tariff")
|
||||
val providerCustomerTariff: Boolean = false,
|
||||
val currency: String,
|
||||
|
||||
@Json(name = "start_time")
|
||||
val startTime: Int = 0,
|
||||
val tags: List<ChargepriceTag>,
|
||||
|
||||
@Json(name = "charge_point_prices")
|
||||
val chargepointPrices: List<ChargepointPrice>,
|
||||
|
||||
@Json(name = "branding")
|
||||
val branding: ChargepriceBranding? = null,
|
||||
|
||||
@RelationshipsObject
|
||||
val relationships: @WriteWith<RelationshipsParceler>() Relationships? = null,
|
||||
) : Equatable, Cloneable, Parcelable {
|
||||
val tariffId: String?
|
||||
get() = (relationships?.get("tariff") as? Relationship.ToOne)?.data?.id
|
||||
|
||||
fun formatMonthlyFees(ctx: Context): String {
|
||||
return listOfNotNull(
|
||||
if (totalMonthlyFee > 0) {
|
||||
ctx.getString(R.string.chargeprice_base_fee, totalMonthlyFee, currency(currency))
|
||||
} else null,
|
||||
if (monthlyMinSales > 0) {
|
||||
ctx.getString(R.string.chargeprice_min_spend, monthlyMinSales, currency(currency))
|
||||
} else null
|
||||
).joinToString(", ")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parceler implementation for the Relationships object.
|
||||
* Note that this ignores certain fields that we don't need (links, meta, etc.)
|
||||
*/
|
||||
internal object RelationshipsParceler : Parceler<Relationships?> {
|
||||
override fun create(parcel: Parcel): Relationships? {
|
||||
if (parcel.readInt() == 0) return null
|
||||
|
||||
val nMembers = parcel.readInt()
|
||||
val members = (0 until nMembers).associate { _ ->
|
||||
val key = parcel.readString()!!
|
||||
val value = if (parcel.readInt() == 0) {
|
||||
val type = parcel.readString()
|
||||
val id = parcel.readString()
|
||||
val ri = if (type != null && id != null) {
|
||||
ResourceIdentifier(type, id)
|
||||
} else null
|
||||
Relationship.ToOne(ri)
|
||||
} else {
|
||||
val size = parcel.readInt()
|
||||
val ris = (0 until size).map { _ ->
|
||||
val type = parcel.readString()!!
|
||||
val id = parcel.readString()!!
|
||||
ResourceIdentifier(type, id)
|
||||
}
|
||||
Relationship.ToMany(ris)
|
||||
}
|
||||
key to value
|
||||
}
|
||||
|
||||
return Relationships(members)
|
||||
}
|
||||
|
||||
override fun Relationships?.write(parcel: Parcel, flags: Int) {
|
||||
if (this == null) {
|
||||
parcel.writeInt(0)
|
||||
return
|
||||
} else {
|
||||
parcel.writeInt(1)
|
||||
}
|
||||
|
||||
parcel.writeInt(members.size)
|
||||
for (member in this.members) {
|
||||
parcel.writeString(member.key)
|
||||
when (val value = member.value) {
|
||||
is Relationship.ToOne -> {
|
||||
parcel.writeInt(0)
|
||||
parcel.writeString(value.data?.type)
|
||||
parcel.writeString(value.data?.id)
|
||||
}
|
||||
is Relationship.ToMany -> {
|
||||
parcel.writeInt(1)
|
||||
parcel.writeInt(value.data.size)
|
||||
for (ri in value.data) {
|
||||
parcel.writeString(ri.type)
|
||||
parcel.writeString(ri.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepointPrice(
|
||||
val power: Double,
|
||||
val plug: String,
|
||||
val price: Double?,
|
||||
@Json(name = "price_distribution") val priceDistribution: PriceDistribution,
|
||||
@Json(name = "blocking_fee_start") val blockingFeeStart: Int?,
|
||||
@Json(name = "no_price_reason") var noPriceReason: String?
|
||||
) : Parcelable {
|
||||
fun formatDistribution(ctx: Context): String {
|
||||
fun percent(value: Double): String {
|
||||
return ctx.getString(R.string.percent_format, value * 100) + "\u00a0"
|
||||
}
|
||||
|
||||
fun time(value: Int): String {
|
||||
val h = floor(value.toDouble() / 60).toInt()
|
||||
val min = ceil(value.toDouble() % 60).toInt()
|
||||
return if (h == 0 && min > 0) "${min}min";
|
||||
// be slightly sloppy (3:01 is shown as 3h) to save space
|
||||
else if (h > 0 && (min == 0 || min == 1)) "${h}h";
|
||||
else "%d:%02dh".format(h, min)
|
||||
}
|
||||
|
||||
// based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js
|
||||
with(priceDistribution) {
|
||||
return listOfNotNull(
|
||||
if (session != null && session > 0.0) {
|
||||
(if (session < 1) percent(session) else "") + ctx.getString(R.string.chargeprice_session_fee)
|
||||
} else null,
|
||||
if (kwh != null && kwh > 0.0 && !isOnlyKwh) {
|
||||
(if (kwh < 1) percent(kwh) else "") + ctx.getString(R.string.chargeprice_per_kwh)
|
||||
} else null,
|
||||
if (minute != null && minute > 0.0) {
|
||||
(if (minute < 1) percent(minute) else "") + ctx.getString(R.string.chargeprice_per_minute) +
|
||||
if (blockingFeeStart != null) {
|
||||
" (${
|
||||
ctx.getString(
|
||||
R.string.chargeprice_blocking_fee,
|
||||
time(blockingFeeStart)
|
||||
)
|
||||
})"
|
||||
} else ""
|
||||
} else null,
|
||||
if ((minute == null || minute == 0.0) && blockingFeeStart != null) {
|
||||
ctx.getString(R.string.chargeprice_blocking_fee, time(blockingFeeStart))
|
||||
} else null
|
||||
).joinToString(" +\u00a0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepriceBranding(
|
||||
@Json(name = "background_color") val backgroundColor: String,
|
||||
@Json(name = "text_color") val textColor: String,
|
||||
@Json(name = "logo_url") val logoUrl: String
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) :
|
||||
Parcelable {
|
||||
val isOnlyKwh
|
||||
get() = kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable,
|
||||
Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceMeta(
|
||||
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
|
||||
)
|
||||
|
||||
enum class ChargepriceInclude {
|
||||
@Json(name = "filter")
|
||||
FILTER,
|
||||
@Json(name = "always")
|
||||
ALWAYS,
|
||||
@Json(name = "exclusive")
|
||||
EXCLUSIVE
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepriceRequestTariffMeta(
|
||||
val include: ChargepriceInclude
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceChargepointMeta(
|
||||
val power: Double,
|
||||
val plug: String,
|
||||
val energy: Double,
|
||||
val duration: Double
|
||||
)
|
||||
|
||||
@Resource("user_feedback")
|
||||
sealed class ChargepriceUserFeedback(
|
||||
val notes: String,
|
||||
val email: String,
|
||||
val context: String,
|
||||
val language: String
|
||||
) {
|
||||
init {
|
||||
if (email.isBlank() || email.length > 100 || !Patterns.EMAIL_ADDRESS.matcher(email)
|
||||
.matches()
|
||||
) {
|
||||
throw IllegalArgumentException("invalid email")
|
||||
}
|
||||
if (!ChargepriceApi.supportedLanguages.contains(language)) {
|
||||
throw IllegalArgumentException("invalid language")
|
||||
}
|
||||
if (context.length > 500) throw IllegalArgumentException("invalid context")
|
||||
if (notes.length > 1000) throw IllegalArgumentException("invalid notes")
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource(type = "missing_price")
|
||||
class ChargepriceMissingPriceFeedback(
|
||||
val tariff: String,
|
||||
val cpo: String,
|
||||
val price: String,
|
||||
@Json(name = "poi_link") val poiLink: String,
|
||||
notes: String,
|
||||
email: String,
|
||||
context: String,
|
||||
language: String
|
||||
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||
init {
|
||||
if (tariff.isBlank() || tariff.length > 100) throw IllegalArgumentException("invalid tariff")
|
||||
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
|
||||
if (price.isBlank() || price.length > 100) throw IllegalArgumentException("invalid price")
|
||||
if (poiLink.isBlank() || poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource(type = "wrong_price")
|
||||
class ChargepriceWrongPriceFeedback(
|
||||
val tariff: String,
|
||||
val cpo: String,
|
||||
@Json(name = "displayed_price") val displayedPrice: String,
|
||||
@Json(name = "actual_price") val actualPrice: String,
|
||||
@Json(name = "poi_link") val poiLink: String,
|
||||
notes: String,
|
||||
email: String,
|
||||
context: String,
|
||||
language: String,
|
||||
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||
init {
|
||||
if (tariff.length > 100) throw IllegalArgumentException("invalid tariff")
|
||||
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
|
||||
if (displayedPrice.length > 100) throw IllegalArgumentException("invalid displayedPrice")
|
||||
if (actualPrice.length > 100) throw IllegalArgumentException("invalid actualPrice")
|
||||
if (poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource(type = "missing_vehicle")
|
||||
class ChargepriceMissingVehicleFeedback(
|
||||
val brand: String,
|
||||
val model: String,
|
||||
notes: String,
|
||||
email: String,
|
||||
context: String,
|
||||
language: String,
|
||||
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||
init {
|
||||
if (brand.length > 100) throw IllegalArgumentException("invalid brand")
|
||||
if (model.length > 100) throw IllegalArgumentException("invalid model")
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ data class GEChargeLocation(
|
||||
address.convert(),
|
||||
chargepoints.map { it.convert() },
|
||||
network,
|
||||
"https://www.goingelectric.de/",
|
||||
"https:${url}",
|
||||
"https:${url}edit/",
|
||||
faultReport?.convert(),
|
||||
@@ -88,6 +89,7 @@ data class GEChargeLocation(
|
||||
locationDescription,
|
||||
photos?.map { it.convert(apikey) },
|
||||
chargecards?.map { it.convert() },
|
||||
null,
|
||||
openinghours?.convert(),
|
||||
cost?.convert(),
|
||||
null,
|
||||
|
||||
128
app/src/main/java/net/vonforst/evmap/api/nobil/NobilAdapters.kt
Normal file
128
app/src/main/java/net/vonforst/evmap/api/nobil/NobilAdapters.kt
Normal file
@@ -0,0 +1,128 @@
|
||||
package net.vonforst.evmap.api.nobil
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.squareup.moshi.rawType
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.Type
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
internal class CoordinateAdapter {
|
||||
@FromJson
|
||||
fun fromJson(position: String): Coordinate {
|
||||
val pattern = """\((\d+(\.\d+)?), *(-?\d+(\.\d+)?)\)"""
|
||||
val match = Regex(pattern).matchEntire(position)
|
||||
?: throw JsonDataException("Unexpected coordinate format: '$position'")
|
||||
|
||||
val latitude : String = match.groups[1]?.value ?: "0.0"
|
||||
val longitude : String = match.groups[3]?.value ?: "0.0"
|
||||
return Coordinate(latitude.toDouble(), longitude.toDouble())
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Coordinate): String = "(" + value.lat + ", " + value.lng + ")"
|
||||
}
|
||||
|
||||
internal class LocalDateTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): LocalDateTime? = value?.let {
|
||||
LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: LocalDateTime?): String? = value?.toString()
|
||||
}
|
||||
|
||||
internal class NobilConverterFactory(val moshi: Moshi) : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<ResponseBody, *>? {
|
||||
val stringAdapter = moshi.adapter(String::class.java)
|
||||
|
||||
if (type.rawType == NobilNumChargepointsResponseData::class.java) {
|
||||
// {"Provider":"NOBIL.no",
|
||||
// "Rights":"Creative Commons Attribution 4.0 International License",
|
||||
// "apiver":"3",
|
||||
// "chargerstations": [{"count":8748}]
|
||||
// }
|
||||
return Converter<ResponseBody, NobilNumChargepointsResponseData> { body ->
|
||||
val reader = JsonReader.of(body.source())
|
||||
reader.beginObject()
|
||||
|
||||
var error: String? = null
|
||||
var provider: String? = null
|
||||
var rights: String? = null
|
||||
var apiver: String? = null
|
||||
var count: Int? = null
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"error" -> error = stringAdapter.fromJson(reader)!!
|
||||
"Provider" -> provider = stringAdapter.fromJson(reader)!!
|
||||
"Rights" -> rights = stringAdapter.fromJson(reader)!!
|
||||
"apiver" -> apiver = stringAdapter.fromJson(reader)!!
|
||||
"chargerstations" -> {
|
||||
reader.beginArray()
|
||||
val intAdapter = moshi.adapter(Int::class.java)
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"count" -> count = intAdapter.fromJson(reader)!!
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
reader.endArray()
|
||||
reader.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
NobilNumChargepointsResponseData(error, provider, rights, apiver, count)
|
||||
}
|
||||
}
|
||||
|
||||
if (type.rawType == NobilDynamicResponseData::class.java) {
|
||||
val nobilChargerStationAdapter = moshi.adapter(NobilChargerStation::class.java)
|
||||
return Converter<ResponseBody, NobilDynamicResponseData> { body ->
|
||||
val reader = JsonReader.of(body.source())
|
||||
reader.beginObject()
|
||||
|
||||
var error: String? = null
|
||||
var provider: String? = null
|
||||
var rights: String? = null
|
||||
var apiver: String? = null
|
||||
var doc: Sequence<NobilChargerStation>? = null
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"error" -> error = stringAdapter.fromJson(reader)!!
|
||||
"Provider" -> provider = stringAdapter.fromJson(reader)!!
|
||||
"Rights" -> rights = stringAdapter.fromJson(reader)!!
|
||||
"apiver" -> apiver = stringAdapter.fromJson(reader)!!
|
||||
"chargerstations" -> {
|
||||
doc = sequence {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
yield(nobilChargerStationAdapter.fromJson(reader)!!)
|
||||
}
|
||||
reader.endArray()
|
||||
reader.close()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
NobilDynamicResponseData(error, provider, rights, apiver, doc)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
354
app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt
Normal file
354
app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt
Normal file
@@ -0,0 +1,354 @@
|
||||
package net.vonforst.evmap.api.nobil
|
||||
|
||||
import android.content.Context
|
||||
import android.database.DatabaseUtils
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.ChargepointList
|
||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
||||
import net.vonforst.evmap.api.FullDownloadResult
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.mapPower
|
||||
import net.vonforst.evmap.api.mapPowerInverse
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.powerSteps
|
||||
import net.vonforst.evmap.model.BooleanFilter
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.Filter
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterValues
|
||||
import net.vonforst.evmap.model.MultipleChoiceFilter
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.model.SliderFilter
|
||||
import net.vonforst.evmap.model.getBooleanValue
|
||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.model.getSliderValue
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
|
||||
private const val maxResults = 2000
|
||||
|
||||
interface NobilApi {
|
||||
@GET("datadump.php")
|
||||
suspend fun getAllChargingStations(
|
||||
@Query("apikey") apikey: String,
|
||||
@Query("format") dataFormat: String = "json"
|
||||
): Response<NobilDynamicResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getNumChargepoints(
|
||||
@Body request: NobilNumChargepointsRequest
|
||||
): Response<NobilNumChargepointsResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getChargepoints(
|
||||
@Body request: NobilRectangleSearchRequest
|
||||
): Response<NobilResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getChargepointsRadius(
|
||||
@Body request: NobilRadiusSearchRequest
|
||||
): Response<NobilResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getChargepointDetail(
|
||||
@Body request: NobilDetailSearchRequest
|
||||
): Response<NobilResponseData>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024 // 10MB
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(LocalDateTimeAdapter())
|
||||
.add(CoordinateAdapter())
|
||||
.build()
|
||||
|
||||
fun create(
|
||||
baseurl: String,
|
||||
context: Context?
|
||||
): NobilApi {
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
if (BuildConfig.DEBUG) {
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseurl)
|
||||
.addConverterFactory(NobilConverterFactory(moshi))
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(NobilApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NobilApiWrapper(
|
||||
val apikey: String,
|
||||
baseurl: String = "https://nobil.no/api/server/",
|
||||
context: Context? = null
|
||||
) : ChargepointApi<NobilReferenceData> {
|
||||
override val name = "Nobil"
|
||||
override val id = "nobil"
|
||||
override val supportsOnlineQueries = false // Online queries are supported, but can't be used together with full downloads
|
||||
override val supportsFullDownload = true
|
||||
override val cacheLimit = Duration.ofDays(300L)
|
||||
val api = NobilApi.create(baseurl, context)
|
||||
|
||||
override suspend fun fullDownload(): FullDownloadResult<NobilReferenceData> {
|
||||
var numTotalChargepoints = 0
|
||||
arrayOf("NOR", "SWE").forEach { countryCode ->
|
||||
val request = NobilNumChargepointsRequest(apikey, countryCode)
|
||||
val response = api.getNumChargepoints(request)
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException(response.message())
|
||||
}
|
||||
val numChargepoints = response.body()!!.count
|
||||
?: throw JsonDataException("Failed to get chargepoint count for '$countryCode'")
|
||||
numTotalChargepoints += numChargepoints
|
||||
}
|
||||
|
||||
val response = api.getAllChargingStations(apikey)
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException(response.message())
|
||||
} else {
|
||||
val data = response.body()!!
|
||||
return NobilFullDownloadResult(data, numTotalChargepoints)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
useClustering: Boolean,
|
||||
filters: FilterValues?,
|
||||
): Resource<ChargepointList> {
|
||||
try {
|
||||
val northeast = "(" + bounds.northeast.latitude + ", " + bounds.northeast.longitude + ")"
|
||||
val southwest = "(" + bounds.southwest.latitude + ", " + bounds.southwest.longitude + ")"
|
||||
val request = NobilRectangleSearchRequest(apikey, northeast, southwest, maxResults)
|
||||
val response = api.getChargepoints(request)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
val data = response.body()!!
|
||||
if (data.chargerStations == null) {
|
||||
return Resource.success(ChargepointList.empty())
|
||||
}
|
||||
val result = postprocessResult(
|
||||
data,
|
||||
filters
|
||||
)
|
||||
return Resource.success(ChargepointList(result, data.chargerStations.size < maxResults))
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChargepointsRadius(
|
||||
referenceData: ReferenceData,
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
useClustering: Boolean,
|
||||
filters: FilterValues?
|
||||
): Resource<ChargepointList> {
|
||||
try {
|
||||
val request = NobilRadiusSearchRequest(apikey, location.latitude, location.longitude, radius * 1000.0, maxResults)
|
||||
val response = api.getChargepointsRadius(request)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
val data = response.body()!!
|
||||
if (data.chargerStations == null) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
val result = postprocessResult(
|
||||
data,
|
||||
filters
|
||||
)
|
||||
return Resource.success(ChargepointList(result, data.chargerStations.size < maxResults))
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postprocessResult(
|
||||
data: NobilResponseData,
|
||||
filters: FilterValues?
|
||||
): List<ChargepointListItem> {
|
||||
if (data.rights == null ) throw JsonDataException("Rights field is missing in received data")
|
||||
|
||||
return data.chargerStations!!.mapNotNull { it.convert(data.rights, filters) }.distinct()
|
||||
}
|
||||
|
||||
override suspend fun getChargepointDetail(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
// TODO: Nobil ids are "SWE_1234", not Long
|
||||
return Resource.error("getChargepointDetail is not implemented", null)
|
||||
}
|
||||
|
||||
override suspend fun getReferenceData(): Resource<NobilReferenceData> {
|
||||
return Resource.success(NobilReferenceData(0))
|
||||
}
|
||||
|
||||
override fun getFilters(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val connectors = listOf(
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG,
|
||||
Chargepoint.CCS_UNKNOWN,
|
||||
Chargepoint.CHADEMO,
|
||||
Chargepoint.SUPERCHARGER
|
||||
)
|
||||
val connectorsMap = connectors.associateWith { connector ->
|
||||
nameForPlugType(sp, connector)
|
||||
}
|
||||
val accessibilityMap = mapOf(
|
||||
"Public" to sp.getString(R.string.accessibility_public),
|
||||
"Visitors" to sp.getString(R.string.accessibility_visitors),
|
||||
"Employees" to sp.getString(R.string.accessibility_employees),
|
||||
"By appointment" to sp.getString(R.string.accessibility_by_appointment),
|
||||
"Residents" to sp.getString(R.string.accessibility_residents)
|
||||
)
|
||||
return listOf(
|
||||
BooleanFilter(sp.getString(R.string.filter_free_parking), "freeparking"),
|
||||
BooleanFilter(sp.getString(R.string.filter_open_247), "open_247"),
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_connectors), "connectors",
|
||||
connectorsMap, manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_accessibility), "accessibilities",
|
||||
accessibilityMap, manyChoices = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertFiltersToSQL(
|
||||
filters: FilterValues,
|
||||
referenceData: ReferenceData
|
||||
): FiltersSQLQuery {
|
||||
if (filters.isEmpty()) return FiltersSQLQuery("",
|
||||
requiresChargepointQuery = false,
|
||||
requiresChargeCardQuery = false
|
||||
)
|
||||
|
||||
var requiresChargepointQuery = false
|
||||
val result = StringBuilder()
|
||||
|
||||
if (filters.getBooleanValue("freeparking") == true) {
|
||||
result.append(" AND freeparking IS 1")
|
||||
}
|
||||
|
||||
if (filters.getBooleanValue("open_247") == true) {
|
||||
result.append(" AND twentyfourSeven IS 1")
|
||||
}
|
||||
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
if (minPower != null && minPower > 0) {
|
||||
result.append(" AND json_extract(cp.value, '$.power') >= $minPower")
|
||||
requiresChargepointQuery = true
|
||||
}
|
||||
|
||||
val connectors = filters.getMultipleChoiceValue("connectors")
|
||||
if (connectors != null && !connectors.all) {
|
||||
val connectorsList = connectors.values.joinToString(",") {
|
||||
DatabaseUtils.sqlEscapeString(it)
|
||||
}
|
||||
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
|
||||
requiresChargepointQuery = true
|
||||
}
|
||||
|
||||
val minConnectors = filters.getSliderValue("min_connectors")
|
||||
if (minConnectors != null && minConnectors > 1) {
|
||||
result.append(" GROUP BY ChargeLocation.id HAVING SUM(json_extract(cp.value, '$.count')) >= $minConnectors")
|
||||
requiresChargepointQuery = true
|
||||
}
|
||||
|
||||
val accessibilities = filters.getMultipleChoiceValue("accessibilities")
|
||||
if (accessibilities != null && !accessibilities.all) {
|
||||
val accessibilitiesList = accessibilities.values.joinToString(",") {
|
||||
DatabaseUtils.sqlEscapeString(it)
|
||||
}
|
||||
result.append(" AND accessibility IN (${accessibilitiesList})")
|
||||
}
|
||||
|
||||
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false)
|
||||
}
|
||||
|
||||
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class NobilFullDownloadResult(private val data: NobilDynamicResponseData,
|
||||
private val numTotalChargepoints: Int) : FullDownloadResult<NobilReferenceData> {
|
||||
private var downloadProgress = 0f
|
||||
private var refData: NobilReferenceData? = null
|
||||
|
||||
override val chargers: Sequence<ChargeLocation>
|
||||
get() {
|
||||
if (data.rights == null) throw JsonDataException("Rights field is missing in received data")
|
||||
return sequence {
|
||||
data.chargerStations?.forEachIndexed { i, it ->
|
||||
downloadProgress = i.toFloat() / numTotalChargepoints
|
||||
val charger = it.convert(data.rights, null)
|
||||
charger?.let { yield(charger) }
|
||||
}
|
||||
refData = NobilReferenceData(0)
|
||||
}
|
||||
}
|
||||
override val progress: Float
|
||||
get() = downloadProgress
|
||||
override val referenceData: NobilReferenceData
|
||||
get() = refData ?: throw UnsupportedOperationException("referenceData is only available once download is complete")
|
||||
}
|
||||
346
app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt
Normal file
346
app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt
Normal file
@@ -0,0 +1,346 @@
|
||||
package net.vonforst.evmap.api.nobil
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.max
|
||||
import net.vonforst.evmap.model.Address
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import net.vonforst.evmap.model.Cost
|
||||
import net.vonforst.evmap.model.FilterValues
|
||||
import net.vonforst.evmap.model.OpeningHours
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.model.getBooleanValue
|
||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.model.getSliderValue
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class NobilReferenceData(
|
||||
val dummy: Int
|
||||
) : ReferenceData()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilNumChargepointsRequest(
|
||||
val apikey: String,
|
||||
val countrycode: String,
|
||||
val action: String = "search",
|
||||
val type: String = "stats_GetSumChargerstations",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3"
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilRectangleSearchRequest(
|
||||
val apikey: String,
|
||||
val northeast: String,
|
||||
val southwest: String,
|
||||
val limit: Int,
|
||||
val action: String = "search",
|
||||
val type: String = "rectangle",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3",
|
||||
// val existingids: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilRadiusSearchRequest(
|
||||
val apikey: String,
|
||||
val lat: Double,
|
||||
val long: Double,
|
||||
val distance: Double, // meters
|
||||
val limit: Int,
|
||||
val action: String = "search",
|
||||
val type: String = "near",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3",
|
||||
// val existingids: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilDetailSearchRequest(
|
||||
val apikey: String,
|
||||
val id: String,
|
||||
val action: String = "search",
|
||||
val type: String = "id",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3",
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilResponseData(
|
||||
@Json(name = "error") val error: String?,
|
||||
@Json(name = "Provider") val provider: String?,
|
||||
@Json(name = "Rights") val rights: String?,
|
||||
@Json(name = "apiver") val apiver: String?,
|
||||
@Json(name = "chargerstations") val chargerStations: List<NobilChargerStation>?
|
||||
)
|
||||
|
||||
data class NobilNumChargepointsResponseData(
|
||||
val error: String?,
|
||||
val provider: String?,
|
||||
val rights: String?,
|
||||
val apiver: String?,
|
||||
val count: Int?
|
||||
)
|
||||
|
||||
data class NobilDynamicResponseData(
|
||||
val error: String?,
|
||||
val provider: String?,
|
||||
val rights: String?,
|
||||
val apiver: String?,
|
||||
val chargerStations: Sequence<NobilChargerStation>?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStation(
|
||||
@Json(name = "csmd") val chargerStationData: NobilChargerStationData,
|
||||
@Json(name = "attr") val chargerStationAttributes: NobilChargerStationAttributes
|
||||
) {
|
||||
fun convert(dataLicense: String,
|
||||
filters: FilterValues?) : ChargeLocation? {
|
||||
val chargepoints = chargerStationAttributes.conn
|
||||
.mapNotNull { createChargepointFromNobilConnection(it.value) }
|
||||
if (chargepoints.isEmpty()) return null
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val connectors = filters?.getMultipleChoiceValue("connectors")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
if (chargepoints
|
||||
.filter { it.power != null && it.power >= (minPower ?: 0) }
|
||||
.filter { if (connectors != null && !connectors.all) it.type in connectors.values else true }
|
||||
.size < (minConnectors ?: 0)) return null
|
||||
|
||||
val chargeLocation = ChargeLocation(
|
||||
chargerStationData.id,
|
||||
"nobil",
|
||||
HtmlCompat.fromHtml(chargerStationData.name, HtmlCompat.FROM_HTML_MODE_COMPACT)
|
||||
.toString(),
|
||||
chargerStationData.position,
|
||||
Address(
|
||||
chargerStationData.city,
|
||||
when (chargerStationData.landCode) {
|
||||
"NOR" -> "Norway"
|
||||
"SWE" -> "Sweden"
|
||||
else -> ""
|
||||
},
|
||||
chargerStationData.zipCode,
|
||||
listOfNotNull(
|
||||
chargerStationData.street,
|
||||
chargerStationData.houseNumber
|
||||
).joinToString(" ")
|
||||
),
|
||||
chargepoints,
|
||||
if (chargerStationData.operator != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.operator,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
"https://nobil.no/",
|
||||
null,
|
||||
when (chargerStationData.landCode) {
|
||||
"SWE" -> "https://www.energimyndigheten.se/klimat/transporter/laddinfrastruktur/registrera-din-laddstation/elbilsagare/"
|
||||
else -> "mailto:post@nobil.no?subject=" + Uri.encode("Regarding charging station " + chargerStationData.internationalId)
|
||||
},
|
||||
null,
|
||||
chargerStationData.ocpiId != null ||
|
||||
chargerStationData.updated.isAfter(LocalDateTime.now().minusMonths(6)),
|
||||
null,
|
||||
if (chargerStationData.ownedBy != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.ownedBy,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
if (chargerStationData.userComment != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.userComment,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
null,
|
||||
if (chargerStationData.description != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.description,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
if (Regex("""\d+\.\w+""").matchEntire(chargerStationData.image) != null) listOf(
|
||||
NobilChargerPhotoAdapter(chargerStationData.image)
|
||||
) else null,
|
||||
null,
|
||||
// 2: Availability
|
||||
chargerStationAttributes.st["2"]?.attrTrans,
|
||||
// 24: Open 24h
|
||||
if (chargerStationAttributes.st["24"]?.attrTrans == "Yes") OpeningHours(
|
||||
twentyfourSeven = true,
|
||||
null,
|
||||
null
|
||||
) else null,
|
||||
Cost(
|
||||
// 7: Parking fee
|
||||
freeparking = when (chargerStationAttributes.st["7"]?.attrTrans) {
|
||||
"Yes" -> false
|
||||
"No" -> true
|
||||
else -> null
|
||||
},
|
||||
descriptionLong = chargerStationAttributes.conn.mapNotNull {
|
||||
// 19: Payment method
|
||||
when (it.value["19"]?.attrValId) {
|
||||
"1" -> listOf("Mobile phone") // TODO: Translate
|
||||
"2" -> listOf("Bank card")
|
||||
"10" -> listOf("Other")
|
||||
"20" -> listOf("Mobile phone", "Charging card")
|
||||
"21" -> listOf("Bank card", "Charging card")
|
||||
"25" -> listOf("Bank card", "Charging card", "Mobile phone")
|
||||
else -> null
|
||||
}
|
||||
}.flatten().sorted().toSet().ifEmpty { null }
|
||||
?.joinToString(prefix = "Accepted payment methods: ")
|
||||
),
|
||||
dataLicense,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Instant.now(),
|
||||
true
|
||||
)
|
||||
|
||||
val accessibilities = filters?.getMultipleChoiceValue("accessibilities")
|
||||
if (accessibilities != null && !accessibilities.all) {
|
||||
if (!accessibilities.values.contains(chargeLocation.accessibility)) return null
|
||||
}
|
||||
|
||||
val freeparking = filters?.getBooleanValue("freeparking")
|
||||
if (freeparking == true && chargeLocation.cost?.freeparking != true) return null
|
||||
|
||||
val open247 = filters?.getBooleanValue("open_247")
|
||||
if (open247 == true && chargeLocation.openinghours?.twentyfourSeven != true) return null
|
||||
|
||||
return chargeLocation
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createChargepointFromNobilConnection(attribs: Map<String, NobilChargerStationGenericAttribute>): Chargepoint? {
|
||||
// https://nobil.no/admin/attributes.php
|
||||
|
||||
val isFixedCable = attribs["25"]?.attrTrans == "Yes"
|
||||
val connectionType = when (attribs["4"]?.attrValId) {
|
||||
"0" -> "" // Unspecified
|
||||
"30" -> Chargepoint.CHADEMO // CHAdeMO
|
||||
"31" -> Chargepoint.TYPE_1 // Type 1
|
||||
"32" -> if (isFixedCable) Chargepoint.TYPE_2_PLUG else Chargepoint.TYPE_2_SOCKET // Type 2
|
||||
"39" -> Chargepoint.CCS_UNKNOWN // CCS/Combo
|
||||
"40" -> Chargepoint.SUPERCHARGER // Tesla Connector Model
|
||||
"70" -> return null // Hydrogen
|
||||
"82" -> return null // Biogas
|
||||
"87" -> "" // MCS
|
||||
|
||||
// These are deprecated and not used
|
||||
"50" -> "" // Type 2 + Schuko
|
||||
"60" -> "" // Type1/Type2
|
||||
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val connectionPower = when (attribs["5"]?.attrValId) {
|
||||
"7" -> 3.6 // 3,6 kW - 230V 1-phase max 16A
|
||||
"8" -> 7.4 // 7,4 kW - 230V 1-phase max 32A
|
||||
"10" -> 11.0 // 11 kW - 400V 3-phase max 16A
|
||||
"11" -> 22.0 // 22 kW - 400V 3-phase max 32A
|
||||
"12" -> 43.0 // 43 kW - 400V 3-phase max 63A
|
||||
"13" -> 50.0 // 50 kW - 500VDC max 100A
|
||||
"16" -> 11.0 // 230V 3-phase max 16A'
|
||||
"17" -> 22.0 // 230V 3-phase max 32A
|
||||
"18" -> 43.0 // 230V 3-phase max 63A
|
||||
"19" -> 20.0 // 20 kW - 500VDC max 50A
|
||||
"22" -> 135.0 // 135 kW - 480VDC max 270A
|
||||
"23" -> 100.0 // 100 kW - 500VDC max 200A
|
||||
"24" -> 150.0 // 150 kW DC
|
||||
"25" -> 350.0 // 350 kW DC
|
||||
"26" -> null // 350 bar
|
||||
"27" -> null // 700 bar
|
||||
"29" -> 75.0 // 75 kW DC
|
||||
"30" -> 225.0 // 225 kW DC
|
||||
"31" -> 250.0 // 250 kW DC
|
||||
"32" -> 200.0 // 200 kW DC
|
||||
"33" -> 300.0 // 300 kW DC
|
||||
"34" -> null // CBG
|
||||
"35" -> null // LBG
|
||||
"36" -> 400.0 // 400 kW DC
|
||||
"37" -> 30.0 // 30 kW DC
|
||||
"38" -> 62.5 // 62,5 kW DC
|
||||
"39" -> 500.0 // 500 kW DC
|
||||
"41" -> 175.0 // 175 kW DC
|
||||
"42" -> 180.0 // 180 kW DC
|
||||
"43" -> 600.0 // 600 kW DC
|
||||
"44" -> 700.0 // 700 kW DC
|
||||
"45" -> 800.0 // 800 kW DC
|
||||
else -> null
|
||||
}
|
||||
|
||||
val connectionVoltage = if (attribs["12"]?.attrVal is String) attribs["12"]?.attrVal.toString().toDoubleOrNull() else null
|
||||
val connectionCurrent = if (attribs["31"]?.attrVal is String) attribs["31"]?.attrVal.toString().toDoubleOrNull() else null
|
||||
val evseUId = if (attribs["27"]?.attrVal is String) listOf(attribs["27"]?.attrVal.toString()) else null
|
||||
val evseId = if (attribs["28"]?.attrVal is String) listOf(attribs["28"]?.attrVal.toString()) else null
|
||||
|
||||
return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId, evseUId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStationData(
|
||||
@Json(name = "id") val id: Long,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "ocpidb_mapping_stasjon_id") val ocpiId: String?,
|
||||
@Json(name = "Street") val street: String?,
|
||||
@Json(name = "House_number") val houseNumber: String,
|
||||
@Json(name = "Zipcode") val zipCode: String?,
|
||||
@Json(name = "City") val city: String?,
|
||||
@Json(name = "Municipality_ID") val municipalityId: String,
|
||||
@Json(name = "Municipality") val municipality: String,
|
||||
@Json(name = "County_ID") val countyId: String,
|
||||
@Json(name = "County") val county: String,
|
||||
@Json(name = "Description_of_location") val description: String?,
|
||||
@Json(name = "Owned_by") val ownedBy: String?,
|
||||
@Json(name = "Operator") val operator: String?,
|
||||
@Json(name = "Number_charging_points") val numChargePoints: Int,
|
||||
@Json(name = "Position") val position: Coordinate,
|
||||
@Json(name = "Image") val image: String,
|
||||
@Json(name = "Available_charging_points") val availableChargePoints: Int,
|
||||
@Json(name = "User_comment") val userComment: String?,
|
||||
@Json(name = "Contact_info") val contactInfo: String?,
|
||||
@Json(name = "Created") val created: LocalDateTime,
|
||||
@Json(name = "Updated") val updated: LocalDateTime,
|
||||
@Json(name = "Station_status") val stationStatus: Int,
|
||||
@Json(name = "Land_code") val landCode: String,
|
||||
@Json(name = "International_id") val internationalId: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStationAttributes(
|
||||
@Json(name = "st") val st: Map<String, NobilChargerStationGenericAttribute>,
|
||||
@Json(name = "conn") val conn: Map<String, Map<String, NobilChargerStationGenericAttribute>>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStationGenericAttribute(
|
||||
@Json(name = "attrtypeid") val attrTypeId: String,
|
||||
@Json(name = "attrname") val attrName: String,
|
||||
@Json(name = "attrvalid") val attrValId: String,
|
||||
@Json(name = "trans") val attrTrans: String,
|
||||
@Json(name = "attrval") val attrVal: Any
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
class NobilChargerPhotoAdapter(override val id: String) :
|
||||
ChargerPhoto(id) {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
|
||||
val maxSize = size ?: max(height, width)
|
||||
return "https://www.nobil.no/img/ladestasjonbilder/" +
|
||||
when (maxSize) {
|
||||
in 0..50 -> "tn_$id"
|
||||
else -> id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ data class OCMChargepoint(
|
||||
addressInfo.toAddress(refData),
|
||||
connections.map { it.convert(refData) },
|
||||
operatorInfo?.title ?: refData.operators.find { it.id == operatorId }?.title,
|
||||
"https://openchargemap.org/",
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
convertFaultReport(),
|
||||
@@ -76,6 +77,7 @@ data class OCMChargepoint(
|
||||
mediaItems?.mapNotNull { it.convert() },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
cost?.takeIf { it.isNotBlank() }.let { Cost(descriptionShort = it) },
|
||||
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
|
||||
ChargepriceData(
|
||||
|
||||
@@ -3,7 +3,13 @@ package net.vonforst.evmap.api.openstreetmap
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.model.Address
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import net.vonforst.evmap.model.Cost
|
||||
import net.vonforst.evmap.model.OpeningHours
|
||||
import okhttp3.internal.immutableListOf
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
@@ -98,6 +104,7 @@ data class OSMChargingStation(
|
||||
getAddress(),
|
||||
getChargepoints(),
|
||||
tags["network"],
|
||||
"https://www.openstreetmap.org/",
|
||||
"https://www.openstreetmap.org/node/$id",
|
||||
"https://www.openstreetmap.org/edit?node=$id",
|
||||
null,
|
||||
@@ -109,6 +116,7 @@ data class OSMChargingStation(
|
||||
null,
|
||||
getPhotos(),
|
||||
null,
|
||||
null,
|
||||
getOpeningHours(),
|
||||
getCost(),
|
||||
"© OpenStreetMap contributors",
|
||||
@@ -237,10 +245,24 @@ data class OSMChargingStation(
|
||||
if (rawOutput == null) {
|
||||
return null
|
||||
}
|
||||
val pattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
|
||||
val matchResult = pattern.matchEntire(rawOutput) ?: return null
|
||||
val numberString = matchResult.groupValues[1].replace(',', '.')
|
||||
return numberString.toDoubleOrNull()
|
||||
val kwPattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
|
||||
kwPattern.matchEntire(rawOutput)?.let { matchResult ->
|
||||
val numberString = matchResult.groupValues[1].replace(',', '.')
|
||||
return numberString.toDoubleOrNull()
|
||||
}
|
||||
|
||||
val numberPattern = Regex("([0-9.,]+)")
|
||||
numberPattern.matchEntire(rawOutput)?.let { matchResult ->
|
||||
// just a number is mapped without unit
|
||||
val numberString = matchResult.groupValues[1].replace(',', '.')
|
||||
val number = numberString.toDoubleOrNull()
|
||||
return number?.let {
|
||||
// assume kW if the number is < 1000, otherwise assume W and convert to kW
|
||||
if (number < 1000) number else number / 1000
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ class CarAppService : androidx.car.app.CarAppService() {
|
||||
@ExperimentalCarApi
|
||||
class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver {
|
||||
private val TAG = "EVMapSession"
|
||||
lateinit var intent: Intent
|
||||
var mapScreen: LocationAwareScreen? = null
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -132,7 +133,8 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
val mapScreen = if (supportsNewMapScreen(carContext)) {
|
||||
this.intent = intent
|
||||
val mapScreen = if (supportsNewMapScreen(carContext) && prefs.androidAutoNewMapScreenEnabled) {
|
||||
MapScreen(carContext, this)
|
||||
} else {
|
||||
LegacyMapScreen(carContext, this)
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.Model
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.ActionStrip
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.SectionedItemList
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
|
||||
import jsonapi.Meta
|
||||
import jsonapi.Relationship
|
||||
import jsonapi.Relationships
|
||||
import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import net.vonforst.evmap.ui.time
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ExperimentalCarApi
|
||||
class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger: ChargeLocation) :
|
||||
Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
ChargepriceApi.create(
|
||||
carContext.getString(R.string.chargeprice_key),
|
||||
carContext.getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
private var prices: List<ChargePrice>? = null
|
||||
private var meta: ChargepriceChargepointMeta? = null
|
||||
private var chargepoint: Chargepoint? = null
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private var errorMessage: String? = null
|
||||
private val batteryRange = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (prices == null) loadData()
|
||||
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
R.string.chargeprice_battery_range,
|
||||
batteryRange[0],
|
||||
batteryRange[1]
|
||||
) + " · " + carContext.getString(R.string.powered_by_chargeprice)
|
||||
)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (prices == null && errorMessage == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
val header = meta?.let { meta ->
|
||||
chargepoint?.let { chargepoint ->
|
||||
"${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
chargepoint.type
|
||||
)
|
||||
} ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
|
||||
carContext.getString(
|
||||
R.string.chargeprice_stats,
|
||||
meta.energy,
|
||||
time(meta.duration.roundToInt()),
|
||||
meta.energy / meta.duration * 60
|
||||
)
|
||||
}"
|
||||
}
|
||||
}
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
val myTariffsAll = prefs.chargepriceMyTariffsAll
|
||||
|
||||
val prices = prices?.take(maxRows)
|
||||
if (prices != null && prices.isNotEmpty() && !myTariffsAll && myTariffs != null) {
|
||||
val (myPrices, otherPrices) = prices.partition { price -> price.tariffId in myTariffs }
|
||||
val myPricesList = buildPricesList(myPrices)
|
||||
val otherPricesList = buildPricesList(otherPrices)
|
||||
if (myPricesList.items.isNotEmpty() && otherPricesList.items.isNotEmpty()) {
|
||||
addSectionedList(
|
||||
SectionedItemList.create(
|
||||
myPricesList,
|
||||
(header?.let { it + "\n" } ?: "") +
|
||||
carContext.getString(R.string.chargeprice_header_my_tariffs)
|
||||
)
|
||||
)
|
||||
addSectionedList(
|
||||
SectionedItemList.create(
|
||||
otherPricesList,
|
||||
carContext.getString(R.string.chargeprice_header_other_tariffs)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val list =
|
||||
if (myPricesList.items.isNotEmpty()) myPricesList else otherPricesList
|
||||
if (header != null) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val list = buildPricesList(prices)
|
||||
if (header != null && list.items.isNotEmpty()) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
Action.Builder().setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
).setOnClickListener {
|
||||
openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
|
||||
}.build()
|
||||
).build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildPricesList(prices: List<ChargePrice>?): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
setNoItemsMessage(
|
||||
errorMessage
|
||||
?: carContext.getString(R.string.chargeprice_no_tariffs_found)
|
||||
)
|
||||
prices?.forEach { price ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(formatProvider(price))
|
||||
addText(formatPrice(price))
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatProvider(price: ChargePrice): String {
|
||||
if (!price.tariffName.startsWith(price.provider)) {
|
||||
return price.provider + " " + price.tariffName
|
||||
} else {
|
||||
return price.tariffName
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPrice(price: ChargePrice): String {
|
||||
val amount = price.chargepointPrices.first().price
|
||||
?: return "${carContext.getString(R.string.chargeprice_price_not_available)} (${price.chargepointPrices.first().noPriceReason})"
|
||||
val totalPrice = carContext.getString(
|
||||
R.string.charge_price_format,
|
||||
amount,
|
||||
currency(price.currency)
|
||||
)
|
||||
val kwhPrice = if (amount > 0f) {
|
||||
carContext.getString(
|
||||
if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
|
||||
R.string.charge_price_kwh_format
|
||||
} else {
|
||||
R.string.charge_price_average_format
|
||||
},
|
||||
amount / meta!!.energy,
|
||||
currency(price.currency)
|
||||
)
|
||||
} else null
|
||||
val monthlyFees = if (price.totalMonthlyFee > 0 || price.monthlyMinSales > 0) {
|
||||
price.formatMonthlyFees(carContext)
|
||||
} else null
|
||||
var text = totalPrice
|
||||
if (kwhPrice != null && monthlyFees != null) {
|
||||
text += " ($kwhPrice, $monthlyFees)"
|
||||
} else if (kwhPrice != null) {
|
||||
text += " ($kwhPrice)"
|
||||
} else if (monthlyFees != null) {
|
||||
text += " ($monthlyFees)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
val hardwareMan =
|
||||
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
hardwareMan.carInfo.fetchModel(exec) { model ->
|
||||
loadPrices(model)
|
||||
}
|
||||
} else {
|
||||
loadPrices(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPrices(model: Model?) {
|
||||
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
|
||||
val manufacturer = getVehicleBrand(model?.manufacturer?.value)
|
||||
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val car = determineVehicle(manufacturer, modelName)
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
|
||||
|
||||
if (cpStation.chargePoints.isEmpty()) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val result = api.getChargePrices(
|
||||
ChargepriceRequest(
|
||||
dataAdapter = dataAdapter,
|
||||
station = cpStation,
|
||||
vehicle = car,
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange.map { it.toDouble() },
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad,
|
||||
showPriceUnavailable = true
|
||||
),
|
||||
relationships = if (!prefs.chargepriceMyTariffsAll) {
|
||||
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
|
||||
Relationships(
|
||||
"tariffs" to Relationship.ToMany(
|
||||
myTariffs.map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
id = it
|
||||
)
|
||||
},
|
||||
meta = Meta.from(
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
|
||||
ChargepriceApi.moshi
|
||||
)
|
||||
)
|
||||
)
|
||||
} else null
|
||||
), ChargepriceApi.getChargepriceLanguage()
|
||||
)
|
||||
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
|
||||
// choose the highest power chargepoint
|
||||
// (we have already filtered so that only compatible ones are included)
|
||||
val chargepoint = cpStation.chargePoints.maxByOrNull { it.power }
|
||||
|
||||
val index = cpStation.chargePoints.indexOf(chargepoint)
|
||||
this@ChargepriceScreen.chargepoint =
|
||||
charger.chargepoints.filter { equivalentPlugTypes(it.type).any { it in car.compatibleEvmapConnectors } }[index]
|
||||
|
||||
if (chargepoint == null) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val metaMapped =
|
||||
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||
meta = metaMapped.chargePoints.maxByOrNull { it.power }
|
||||
|
||||
prices = result.data!!.mapNotNull { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == chargepoint.plug && it.power == chargepoint.power
|
||||
}
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.copy(
|
||||
chargepointPrices = filteredPrices
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
}
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.chargeprice_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.chargeprice_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} catch (e: NoVehicleSelectedException) {
|
||||
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
|
||||
invalidate()
|
||||
} catch (e: VehicleUnknownException) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_unknown,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
invalidate()
|
||||
} catch (e: VehicleAmbiguousException) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_ambiguous,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
invalidate()
|
||||
} catch (e: VehicleUnavailableException) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NoVehicleSelectedException : Exception()
|
||||
private class VehicleUnknownException : Exception()
|
||||
private class VehicleAmbiguousException : Exception()
|
||||
private class VehicleUnavailableException : Exception()
|
||||
|
||||
private suspend fun determineVehicle(
|
||||
manufacturer: String?,
|
||||
modelName: String?
|
||||
): ChargepriceCar {
|
||||
var vehicles = api.getVehicles().filter {
|
||||
it.id in prefs.chargepriceMyVehicles
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
throw NoVehicleSelectedException()
|
||||
} else if (vehicles.size > 1) {
|
||||
if (manufacturer != null) {
|
||||
vehicles = vehicles.filter {
|
||||
it.brand.lowercase() == getVehicleBrand(manufacturer)?.lowercase()
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
throw VehicleUnknownException()
|
||||
} else if (vehicles.size > 1) {
|
||||
if (modelName != null) {
|
||||
vehicles = vehicles.filter {
|
||||
it.name.lowercase().startsWith(modelName.lowercase())
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
throw VehicleUnknownException()
|
||||
} else if (vehicles.size > 1) {
|
||||
throw VehicleAmbiguousException()
|
||||
}
|
||||
} else {
|
||||
throw VehicleAmbiguousException()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw VehicleUnavailableException()
|
||||
}
|
||||
}
|
||||
return vehicles[0]
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -142,32 +141,26 @@ class ChargerDetailScreen(
|
||||
if (ChargepriceApi.isChargerSupported(charger)) {
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.auto_prices))
|
||||
.setOnClickListener {
|
||||
if (prefs.chargepriceNativeIntegration) {
|
||||
if (!prefs.chargepriceRemoval2025DialogShown) {
|
||||
screenManager.push(
|
||||
ChargepriceScreen(
|
||||
TextDialogScreen(
|
||||
carContext,
|
||||
session,
|
||||
charger
|
||||
R.string.chargeprice_removal_2025_dialog_title,
|
||||
R.string.chargeprice_removal_2025_dialog_detail
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
session.cas.startActivity(intent)
|
||||
prefs.chargepriceRemoval2025DialogShown = true
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
session.cas.startActivity(intent)
|
||||
}
|
||||
.build())
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.MarkerManager
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.utils.headingDiff
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.await
|
||||
@@ -70,12 +69,12 @@ import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
private const val DEFAULT_ZOOM_MYLOCATION = 14f
|
||||
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites.
|
||||
*
|
||||
@@ -146,6 +145,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
private var map: AnyMap? = null
|
||||
private var markerManager: MarkerManager? = null
|
||||
private var myLocationEnabled = false
|
||||
private var compassEnabled = false
|
||||
private var myLocationNeedsUpdate = false
|
||||
|
||||
private val formatter = ChargerListFormatter(ctx, this, session.cas)
|
||||
@@ -241,11 +241,17 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
.addAction(Action.PAN)
|
||||
.addAction(
|
||||
Action.Builder().setIcon(
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_location))
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
if (compassEnabled) R.drawable.ic_compass else R.drawable.ic_location
|
||||
)
|
||||
)
|
||||
.setTint(if (myLocationEnabled) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
).setOnClickListener {
|
||||
enableLocation(true)
|
||||
enableLocation(true, myLocationEnabled && !compassEnabled)
|
||||
invalidate()
|
||||
}.build()
|
||||
)
|
||||
.addAction(
|
||||
@@ -385,8 +391,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
|
||||
val map = map ?: return
|
||||
if (myLocationEnabled) {
|
||||
val bearing = if (compassEnabled) getBearing(location) else 0f
|
||||
if (oldLoc == null) {
|
||||
mapSurfaceCallback.animateCamera(map.cameraUpdateFactory.newLatLngZoom(latLng, 13f))
|
||||
mapSurfaceCallback.animateCamera(
|
||||
map.cameraUpdateFactory.newLatLngZoomBearing(
|
||||
latLng,
|
||||
DEFAULT_ZOOM_MYLOCATION,
|
||||
bearing
|
||||
)
|
||||
)
|
||||
} else if (latLng != oldLoc && distanceBetween(
|
||||
latLng.latitude,
|
||||
latLng.longitude,
|
||||
@@ -395,7 +408,11 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
) > 1
|
||||
) {
|
||||
// only update map if location changed by more than 1 meter
|
||||
val camUpdate = map.cameraUpdateFactory.newLatLng(latLng)
|
||||
val camUpdate = map.cameraUpdateFactory.newLatLngZoomBearing(
|
||||
latLng,
|
||||
map.cameraPosition.zoom,
|
||||
bearing
|
||||
)
|
||||
mapSurfaceCallback.animateCamera(camUpdate)
|
||||
}
|
||||
}
|
||||
@@ -545,6 +562,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
availabilities.clear()
|
||||
location = null
|
||||
myLocationEnabled = false
|
||||
compassEnabled = false
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
@@ -556,6 +574,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
prefs.currentMapZoom = it.cameraPosition.zoom
|
||||
}
|
||||
prefs.currentMapMyLocationEnabled = myLocationEnabled
|
||||
prefs.androidAutoCompassEnabled = compassEnabled
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
@@ -625,9 +644,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
onClusterClick = {
|
||||
val newZoom = map.cameraPosition.zoom + 2
|
||||
mapSurfaceCallback.animateCamera(
|
||||
map.cameraUpdateFactory.newLatLngZoom(
|
||||
map.cameraUpdateFactory.newLatLngZoomBearing(
|
||||
LatLng(it.coordinates.lat, it.coordinates.lng),
|
||||
newZoom
|
||||
newZoom,
|
||||
if (compassEnabled) location?.let { getBearing(it) } ?: 0f else 0f
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -657,6 +677,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
prefs.placeSearchResultAndroidAuto?.let { place ->
|
||||
// move to the location of the search result
|
||||
myLocationEnabled = false
|
||||
compassEnabled = false
|
||||
markerManager?.searchResult = place
|
||||
if (place.viewport != null) {
|
||||
map.moveCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
|
||||
@@ -664,7 +685,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
map.moveCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
||||
}
|
||||
} ?: if (prefs.currentMapMyLocationEnabled) {
|
||||
enableLocation(false)
|
||||
enableLocation(false, prefs.androidAutoCompassEnabled)
|
||||
} else {
|
||||
// use position saved in preferences, fall back to default (Europe)
|
||||
val cameraUpdate =
|
||||
@@ -692,14 +713,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
loadChargers()
|
||||
}
|
||||
|
||||
private fun enableLocation(animated: Boolean) {
|
||||
private fun enableLocation(animated: Boolean, withCompass: Boolean) {
|
||||
myLocationEnabled = true
|
||||
compassEnabled = withCompass
|
||||
myLocationNeedsUpdate = true
|
||||
if (location != null) {
|
||||
location?.let { location ->
|
||||
val map = map ?: return
|
||||
val update = map.cameraUpdateFactory.newLatLngZoom(
|
||||
val update = map.cameraUpdateFactory.newLatLngZoomBearing(
|
||||
LatLng.fromLocation(location),
|
||||
13f
|
||||
DEFAULT_ZOOM_MYLOCATION,
|
||||
if (withCompass) getBearing(location) else 0f
|
||||
)
|
||||
if (animated) {
|
||||
mapSurfaceCallback.animateCamera(update)
|
||||
@@ -708,4 +731,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBearing(location: Location): Float =
|
||||
heading?.orientations?.value?.get(0) ?: location.bearing
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.auto
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Presentation
|
||||
import android.content.Context
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
@@ -39,10 +40,6 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
SurfaceCallback, OnMapReadyCallback {
|
||||
private val VIRTUAL_DISPLAY_NAME = "evmap_map"
|
||||
private val VELOCITY_THRESHOLD_IGNORE_FLING = 1000
|
||||
private val STATUSBAR_OFFSET_SYSTEMS = listOf(
|
||||
"VolvoCars/ihu_emulator_volvo_car/ihu_emulator:11",
|
||||
"Google/sdk_gcar_x86_64/generic_64bitonly_x86_64:11"
|
||||
)
|
||||
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
@@ -173,14 +170,23 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
||||
flingAnimator?.cancel()
|
||||
val map = map ?: return
|
||||
if (scaleFactor == 2f) return
|
||||
|
||||
val offsetX = (focusX - mapView.width / 2) * (scaleFactor - 1f)
|
||||
val offsetY = (offsetY(focusY) - mapView.height / 2) * (scaleFactor - 1f)
|
||||
val (x, y) = offsetScreen(focusX, focusY)
|
||||
val offsetX = (x - mapView.width / 2) * (scaleFactor - 1f)
|
||||
val offsetY = (y - mapView.height / 2) * (scaleFactor - 1f)
|
||||
|
||||
Log.i("MapSurfaceCallback", "focus: $focusX, $focusY, scaleFactor: $scaleFactor")
|
||||
map.moveCamera(map.cameraUpdateFactory.zoomBy(scaleFactor - 1))
|
||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(offsetX, offsetY))
|
||||
if (scaleFactor == 2f) {
|
||||
map.animateCamera(
|
||||
map.cameraUpdateFactory.zoomBy(
|
||||
scaleFactor - 1,
|
||||
Point(focusX.roundToInt(), focusY.roundToInt())
|
||||
)
|
||||
)
|
||||
} else {
|
||||
map.moveCamera(map.cameraUpdateFactory.zoomBy(scaleFactor - 1))
|
||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(offsetX, offsetY))
|
||||
}
|
||||
dispatchCameraMoveStarted()
|
||||
}
|
||||
|
||||
@@ -218,13 +224,13 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
flingAnimator?.cancel()
|
||||
val downTime: Long = SystemClock.uptimeMillis()
|
||||
val eventTime: Long = downTime + 100
|
||||
val yOffset = offsetY(y)
|
||||
val (xOffset, yOffset) = offsetScreen(x, y)
|
||||
|
||||
val downEvent = MotionEvent.obtain(
|
||||
downTime,
|
||||
downTime,
|
||||
MotionEvent.ACTION_DOWN,
|
||||
x,
|
||||
xOffset,
|
||||
yOffset,
|
||||
0
|
||||
)
|
||||
@@ -234,7 +240,7 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
downTime,
|
||||
eventTime,
|
||||
MotionEvent.ACTION_UP,
|
||||
x,
|
||||
xOffset,
|
||||
yOffset,
|
||||
0
|
||||
)
|
||||
@@ -242,14 +248,24 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
upEvent.recycle()
|
||||
}
|
||||
|
||||
private fun offsetY(y: Float): Float {
|
||||
if (!STATUSBAR_OFFSET_SYSTEMS.any { Build.FINGERPRINT.startsWith(it) }) return y
|
||||
private fun offsetScreen(x: Float, y: Float): Pair<Float, Float> {
|
||||
if (BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
return x to y
|
||||
}
|
||||
|
||||
// In some emulators, touch locations are offset by the status bar height
|
||||
// On AAOS, touch locations don't seem to take into account system bar insets
|
||||
// related: https://issuetracker.google.com/issues/256905247
|
||||
val resId = ctx.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
val offset = resId.takeIf { it > 0 }?.let { ctx.resources.getDimensionPixelSize(it) } ?: 0
|
||||
return y + offset
|
||||
val yOffset = resId.takeIf { it > 0 }?.let { ctx.resources.getDimensionPixelSize(it) } ?: 0
|
||||
|
||||
val xOffset = if (Build.MODEL == "AIVI2 R FULL DOM" && width > height) {
|
||||
// Renault 5 left system bar
|
||||
120
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
return x + xOffset to y + yOffset
|
||||
}
|
||||
|
||||
private fun createMap(ctx: Context): MapContainerView {
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.add
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||
|
||||
class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
|
||||
companion object {
|
||||
private val resultRegistry: MutableMap<String, MutableSharedFlow<String>> = mutableMapOf()
|
||||
|
||||
fun registerForResult(url: String): Flow<String> {
|
||||
val flow = MutableSharedFlow<String>(replay = 1)
|
||||
resultRegistry[url] = flow
|
||||
return flow
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
@@ -22,10 +29,14 @@ class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
|
||||
}
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
finish()
|
||||
val url = intent.getStringExtra(OAuthLoginFragment.EXTRA_URL)!!
|
||||
supportFragmentManager.setFragmentResultListener(url, this) { _, result ->
|
||||
val resultUrl = result.getString(OAuthLoginFragment.EXTRA_URL) ?: return@setFragmentResultListener
|
||||
resultRegistry[url]?.let { flow ->
|
||||
flow.tryEmit(resultUrl)
|
||||
resultRegistry.remove(url)
|
||||
}
|
||||
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
@@ -17,8 +15,6 @@ import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.GridItem
|
||||
import androidx.car.app.model.GridTemplate
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
@@ -27,13 +23,12 @@ import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.SectionedItemList
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.Toggle
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.EXTRA_DONATE
|
||||
@@ -42,11 +37,6 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.currencyDisplayName
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
||||
import net.vonforst.evmap.getPackageInfoCompat
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
@@ -55,12 +45,15 @@ import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@ExperimentalCarApi
|
||||
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), DefaultLifecycleObserver {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
val newMapScreenEnabledPrevious = prefs.androidAutoNewMapScreenEnabled
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
@@ -84,23 +77,6 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
screenManager.push(DataSettingsScreen(carContext, session))
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_chargeprice))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).setTint(
|
||||
CarColor.DEFAULT
|
||||
).build()
|
||||
)
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargepriceSettingsScreen(carContext))
|
||||
}
|
||||
}.build())
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
@@ -116,7 +92,26 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
}
|
||||
.build()
|
||||
)
|
||||
if (carContext.carAppApiLevel < 7 || !carContext.isAppDrivenRefreshSupported) {
|
||||
if (supportsNewMapScreen(carContext)) {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_use_new_map_screen))
|
||||
.setToggle(Toggle.Builder {
|
||||
prefs.androidAutoNewMapScreenEnabled = it
|
||||
invalidate()
|
||||
}.setChecked(prefs.androidAutoNewMapScreenEnabled).build())
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_developer
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (!supportsNewMapScreen(carContext) || !prefs.androidAutoNewMapScreenEnabled) {
|
||||
// this option is only supported in LegacyMapScreen
|
||||
addItem(
|
||||
Row.Builder()
|
||||
@@ -148,14 +143,23 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
screenManager.push(AboutScreen(carContext, session))
|
||||
}
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
if (newMapScreenEnabledPrevious != prefs.androidAutoNewMapScreenEnabled) {
|
||||
val newMapScreen = session.onCreateScreen(session.intent)
|
||||
val oldMapScreen = screenManager.screenStack.last()
|
||||
screenManager.push(newMapScreen)
|
||||
screenManager.remove(oldMapScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCarApi
|
||||
@@ -340,23 +344,18 @@ class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ct
|
||||
val args = OAuthLoginFragmentArgs(
|
||||
uri.toString(),
|
||||
TeslaAuthenticationApi.resultUrlPrefix,
|
||||
"#000000"
|
||||
"#FFFFFF"
|
||||
).toBundle()
|
||||
val intent = Intent(carContext, OAuthLoginActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtras(args)
|
||||
|
||||
LocalBroadcastManager.getInstance(carContext)
|
||||
.registerReceiver(object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
val url = IntentCompat.getParcelableExtra(
|
||||
intent,
|
||||
OAuthLoginFragment.EXTRA_URL,
|
||||
Uri::class.java
|
||||
)
|
||||
teslaGetAccessToken(url!!, codeVerifier)
|
||||
}
|
||||
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
|
||||
val resultFlow = OAuthLoginActivity.registerForResult(uri.toString())
|
||||
lifecycleScope.launch {
|
||||
resultFlow.collect { resultUrl ->
|
||||
teslaGetAccessToken(resultUrl.toUri(), codeVerifier)
|
||||
}
|
||||
}
|
||||
|
||||
session.cas.startActivity(intent)
|
||||
|
||||
@@ -451,6 +450,7 @@ class ChooseDataSourceScreen(
|
||||
val descriptions = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> listOf(
|
||||
carContext.getString(R.string.data_source_goingelectric_desc),
|
||||
carContext.getString(R.string.data_source_nobil_desc),
|
||||
carContext.getString(R.string.data_source_openchargemap_desc),
|
||||
carContext.getString(R.string.data_source_openstreetmap_desc)
|
||||
)
|
||||
@@ -509,341 +509,6 @@ class ChooseDataSourceScreen(
|
||||
}
|
||||
}
|
||||
|
||||
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_chargeprice))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_native_integration))
|
||||
addText(carContext.getString(if (prefs.chargepriceNativeIntegration) R.string.pref_chargeprice_native_integration_on else R.string.pref_chargeprice_native_integration_off))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceNativeIntegration = it
|
||||
invalidate()
|
||||
}.setChecked(prefs.chargepriceNativeIntegration).build())
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_my_vehicle))
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectVehiclesScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_my_tariffs))
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectTariffsScreen(carContext))
|
||||
}
|
||||
addText(
|
||||
if (prefs.chargepriceMyTariffsAll) {
|
||||
carContext.getString(R.string.chargeprice_all_tariffs_selected)
|
||||
} else {
|
||||
val n = prefs.chargepriceMyTariffs?.size ?: 0
|
||||
carContext.resources
|
||||
.getQuantityString(
|
||||
R.plurals.chargeprice_some_tariffs_selected,
|
||||
n,
|
||||
n
|
||||
) + "\n" + carContext.resources.getQuantityString(
|
||||
R.plurals.pref_my_tariffs_summary,
|
||||
n
|
||||
)
|
||||
}
|
||||
)
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
|
||||
setBrowsable(true)
|
||||
|
||||
val range = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.chargeprice_battery_range,
|
||||
range[0],
|
||||
range[1]
|
||||
)
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectChargingRangeScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_currency))
|
||||
|
||||
val values =
|
||||
carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
|
||||
val names = values.map(::currencyDisplayName)
|
||||
val index = values.indexOf(prefs.chargepriceCurrency)
|
||||
addText(if (index >= 0) names[index] else "")
|
||||
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectCurrencyScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_no_base_fee))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceNoBaseFee = it
|
||||
}.setChecked(prefs.chargepriceNoBaseFee).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
if (maxRows > 6) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs))
|
||||
addText(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs_summary))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceShowProviderCustomerTariffs = it
|
||||
}.setChecked(prefs.chargepriceShowProviderCustomerTariffs).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load))
|
||||
addText(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load_summary))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceAllowUnbalancedLoad = it
|
||||
}.setChecked(prefs.chargepriceAllowUnbalancedLoad).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
}
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceCar>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
private var api = ChargepriceApi.create(
|
||||
carContext.getString(R.string.chargeprice_key),
|
||||
carContext.getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
override val isMultiSelect = true
|
||||
override val shouldShowSelectAll = false
|
||||
|
||||
override fun isSelected(it: ChargepriceCar): Boolean {
|
||||
return prefs.chargepriceMyVehicles.contains(it.id)
|
||||
}
|
||||
|
||||
override fun toggleSelected(item: ChargepriceCar) {
|
||||
if (isSelected(item)) {
|
||||
prefs.chargepriceMyVehicles = prefs.chargepriceMyVehicles.minus(item.id)
|
||||
} else {
|
||||
prefs.chargepriceMyVehicles = prefs.chargepriceMyVehicles.plus(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLabel(it: ChargepriceCar) = "${it.brand} ${it.name}"
|
||||
|
||||
override fun getDetails(it: ChargepriceCar) = it.formatSpecs()
|
||||
|
||||
override suspend fun loadData(): List<ChargepriceCar> {
|
||||
return api.getVehicles()
|
||||
}
|
||||
}
|
||||
|
||||
class SelectTariffsScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceTariff>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
private var api = ChargepriceApi.create(
|
||||
carContext.getString(R.string.chargeprice_key),
|
||||
carContext.getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
override val isMultiSelect = true
|
||||
override val shouldShowSelectAll = true
|
||||
|
||||
override fun isSelected(it: ChargepriceTariff): Boolean {
|
||||
return prefs.chargepriceMyTariffsAll or (prefs.chargepriceMyTariffs?.contains(it.id)
|
||||
?: false)
|
||||
}
|
||||
|
||||
override fun toggleSelected(item: ChargepriceTariff) {
|
||||
val tariffs = prefs.chargepriceMyTariffs ?: if (prefs.chargepriceMyTariffsAll) {
|
||||
fullList!!.map { it.id }.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
if (isSelected(item)) {
|
||||
prefs.chargepriceMyTariffs = tariffs.minus(item.id)
|
||||
prefs.chargepriceMyTariffsAll = false
|
||||
} else {
|
||||
prefs.chargepriceMyTariffs = tariffs.plus(item.id)
|
||||
if (prefs.chargepriceMyTariffs == fullList!!.map { it.id }.toSet()) {
|
||||
prefs.chargepriceMyTariffsAll = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectAll() {
|
||||
prefs.chargepriceMyTariffsAll = true
|
||||
super.selectAll()
|
||||
}
|
||||
|
||||
override fun selectNone() {
|
||||
prefs.chargepriceMyTariffsAll = false
|
||||
prefs.chargepriceMyTariffs = emptySet()
|
||||
super.selectNone()
|
||||
}
|
||||
|
||||
override fun getLabel(it: ChargepriceTariff): String {
|
||||
return if (!it.name.lowercase().startsWith(it.provider.lowercase())) {
|
||||
"${it.provider} ${it.name}"
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadData(): List<ChargepriceTariff> {
|
||||
return api.getTariffs()
|
||||
}
|
||||
}
|
||||
|
||||
class SelectCurrencyScreen(ctx: CarContext) : MultiSelectSearchScreen<Pair<String, String>>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
override val isMultiSelect = false
|
||||
override val shouldShowSelectAll = false
|
||||
|
||||
override fun isSelected(it: Pair<String, String>): Boolean =
|
||||
prefs.chargepriceCurrency == it.second
|
||||
|
||||
override fun toggleSelected(item: Pair<String, String>) {
|
||||
prefs.chargepriceCurrency = item.second
|
||||
}
|
||||
|
||||
override fun getLabel(it: Pair<String, String>): String = it.first
|
||||
|
||||
override suspend fun loadData(): List<Pair<String, String>> {
|
||||
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
|
||||
val names = values.map(::currencyDisplayName)
|
||||
return names.zip(values)
|
||||
}
|
||||
}
|
||||
|
||||
class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
private val maxItems = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)
|
||||
} else 6
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(
|
||||
ItemList.Builder().apply {
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.chargeprice_battery_range_from))
|
||||
setText(
|
||||
carContext.getString(
|
||||
R.string.percent_format,
|
||||
prefs.chargepriceBatteryRangeAndroidAuto[0]
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_add
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[0] = min(this[1] - 5, this[0] + 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.chargeprice_battery_range_to))
|
||||
setText(
|
||||
carContext.getString(
|
||||
R.string.percent_format,
|
||||
prefs.chargepriceBatteryRangeAndroidAuto[1]
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_add
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[1] = min(100f, this[1] + 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
|
||||
val nSpacers = when {
|
||||
maxItems % 3 == 0 -> 1
|
||||
maxItems == 100 -> 0 // AA has increased the limit to 100 and changed the way items are laid out
|
||||
maxItems % 4 == 0 -> 2
|
||||
else -> 0
|
||||
}
|
||||
|
||||
for (i in 0..nSpacers) {
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(" ")
|
||||
setImage(emptyCarIcon)
|
||||
}.build())
|
||||
}
|
||||
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(" ")
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_remove
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[0] = max(0f, this[0] - 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(" ")
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_remove
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[1] = max(this[0] + 5, this[1] - 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
}.build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCarApi
|
||||
class AboutScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.LongMessageTemplate
|
||||
import androidx.car.app.model.Template
|
||||
|
||||
class TextDialogScreen(
|
||||
ctx: CarContext,
|
||||
@StringRes val title: Int,
|
||||
@StringRes val message: Int
|
||||
) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return LongMessageTemplate.Builder(carContext.getString(message)).apply {
|
||||
setTitle(carContext.getString(title))
|
||||
setHeaderAction(Action.BACK)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.ChargepriceAdapter
|
||||
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
|
||||
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.SingleViewAdapter
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
|
||||
import net.vonforst.evmap.databinding.FragmentChargepriceHeaderBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.navigation.safeNavigate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
|
||||
import java.text.NumberFormat
|
||||
|
||||
class ChargepriceFragment : Fragment() {
|
||||
private lateinit var binding: FragmentChargepriceBinding
|
||||
private lateinit var headerBinding: FragmentChargepriceHeaderBinding
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
|
||||
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
|
||||
savedStateViewModelFactory { state ->
|
||||
ChargepriceViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url),
|
||||
state
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedElementEnterTransition = MaterialContainerTransform()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
prefs.chargepriceCounter += 1
|
||||
if ((prefs.chargepriceCounter).mod(30) == 0) {
|
||||
showDonationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
vm.reloadPrefs()
|
||||
}
|
||||
|
||||
private fun showDonationDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.chargeprice_donation_dialog_title)
|
||||
.setMessage(R.string.chargeprice_donation_dialog_detail)
|
||||
.setNegativeButton(R.string.ok) { di, _ ->
|
||||
di.cancel()
|
||||
}
|
||||
.setPositiveButton(R.string.donate) { di, _ ->
|
||||
di.dismiss()
|
||||
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToDonateFragment())
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_chargeprice, container, false
|
||||
)
|
||||
headerBinding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_chargeprice_header, container, false
|
||||
)
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.vm = vm
|
||||
headerBinding.lifecycleOwner = viewLifecycleOwner
|
||||
headerBinding.vm = vm
|
||||
|
||||
binding.toolbar.inflateMenu(R.menu.chargeprice)
|
||||
binding.toolbar.setTitle(R.string.chargeprice_title)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
|
||||
val charger = fragmentArgs.charger
|
||||
vm.charger.value = charger
|
||||
if (vm.chargepoint.value == null) {
|
||||
vm.chargepoint.value = charger.chargepointsMerged[0]
|
||||
}
|
||||
|
||||
val vehicleAdapter = CheckableChargepriceCarAdapter()
|
||||
headerBinding.vehicleSelection.adapter = vehicleAdapter
|
||||
val vehicleObserver: Observer<ChargepriceCar?> = Observer {
|
||||
vehicleAdapter.setCheckedItem(it)
|
||||
}
|
||||
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
|
||||
vehicleAdapter.onCheckedItemChangedListener = {
|
||||
vm.vehicle.removeObserver(vehicleObserver)
|
||||
vm.vehicle.value = it
|
||||
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
|
||||
}
|
||||
|
||||
val chargepriceAdapter = ChargepriceAdapter().apply {
|
||||
onClickListener = {
|
||||
(requireActivity() as MapsActivity).openUrl(it.url, binding.root)
|
||||
}
|
||||
}
|
||||
val joinedAdapter = ConcatAdapter(
|
||||
SingleViewAdapter(headerBinding.root),
|
||||
chargepriceAdapter
|
||||
)
|
||||
binding.chargePricesList.apply {
|
||||
adapter = joinedAdapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
context, LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
}
|
||||
vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.meta = it?.data
|
||||
}
|
||||
vm.myTariffs.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.myTariffs = it
|
||||
}
|
||||
vm.myTariffsAll.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.myTariffsAll = it
|
||||
}
|
||||
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.submitList(it?.data ?: emptyList())
|
||||
}
|
||||
|
||||
val connectorsAdapter = CheckableConnectorAdapter()
|
||||
|
||||
val observer: Observer<Chargepoint?> = Observer {
|
||||
connectorsAdapter.setCheckedItem(it)
|
||||
}
|
||||
vm.chargepoint.observe(viewLifecycleOwner, observer)
|
||||
connectorsAdapter.onCheckedItemChangedListener = {
|
||||
vm.chargepoint.removeObserver(observer)
|
||||
vm.chargepoint.value = it
|
||||
vm.chargepoint.observe(viewLifecycleOwner, observer)
|
||||
}
|
||||
|
||||
vm.vehicleCompatibleConnectors.observe(viewLifecycleOwner) { plugs ->
|
||||
connectorsAdapter.enabledConnectors =
|
||||
plugs?.flatMap { plug -> equivalentPlugTypes(plug) }
|
||||
}
|
||||
|
||||
headerBinding.connectorsList.apply {
|
||||
adapter = connectorsAdapter
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
}
|
||||
|
||||
binding.imgChargepriceLogo.setOnClickListener {
|
||||
(requireActivity() as MapsActivity).openUrl(
|
||||
ChargepriceApi.getPoiUrl(charger),
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
|
||||
binding.btnSettings.setOnClickListener {
|
||||
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToChargepriceSettingsFragment())
|
||||
}
|
||||
|
||||
headerBinding.batteryRange.setLabelFormatter { value: Float ->
|
||||
val fmt = NumberFormat.getNumberInstance()
|
||||
fmt.maximumFractionDigits = 0
|
||||
fmt.format(value.toDouble()) + "%"
|
||||
}
|
||||
headerBinding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
|
||||
when (motionEvent.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> vm.batteryRangeSliderDragging.value = true
|
||||
MotionEvent.ACTION_UP -> vm.batteryRangeSliderDragging.value = false
|
||||
}
|
||||
false
|
||||
}
|
||||
headerBinding.tvChargeFromTo.setOnClickListener {
|
||||
it.postDelayed({
|
||||
vm.resetBatteryRangeToDefault()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
binding.toolbar.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_help -> {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
getString(R.string.chargeprice_faq_link),
|
||||
binding.root
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) { res ->
|
||||
when (res?.status) {
|
||||
Status.ERROR -> {
|
||||
if (vm.vehicle.value == null) return@observe
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
connectionErrorSnackbar = Snackbar
|
||||
.make(
|
||||
view,
|
||||
R.string.chargeprice_connection_error,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.setAction(R.string.retry) {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
vm.loadPrices()
|
||||
}
|
||||
connectionErrorSnackbar!!.show()
|
||||
}
|
||||
Status.SUCCESS, null -> {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
|
||||
if (prefs.dataSourceSet) {
|
||||
when (prefs.dataSource) {
|
||||
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
||||
"nobil" -> binding.rgDataSource.rbNobil.isChecked = true
|
||||
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
||||
"openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true
|
||||
}
|
||||
@@ -64,6 +65,8 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
|
||||
binding.btnOK.setOnClickListener {
|
||||
val result = if (binding.rgDataSource.rbGoingElectric.isChecked) {
|
||||
"goingelectric"
|
||||
} else if (binding.rgDataSource.rbNobil.isChecked) {
|
||||
"nobil"
|
||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||
"openchargemap"
|
||||
} else if (binding.rgDataSource.rbOpenStreetMap.isChecked) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.content.Intent
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
||||
@@ -22,5 +25,10 @@ abstract class DonateFragmentBase : Fragment() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(referrals.root) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -68,6 +71,13 @@ class FavoritesFragment : Fragment() {
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.vm = vm
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.favsList
|
||||
) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -49,6 +52,13 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
binding.vm = vm
|
||||
vm.filterProfile.observe(viewLifecycleOwner) {}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.filtersList
|
||||
) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -60,6 +63,13 @@ class FilterProfilesFragment : Fragment() {
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.vm = vm
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.filterProfilesList
|
||||
) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ package net.vonforst.evmap.fragment
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.KeyListener
|
||||
@@ -44,7 +47,6 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
@@ -117,7 +119,6 @@ import net.vonforst.evmap.viewmodel.Status
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@@ -137,7 +138,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
private var mapTopPadding: Int = 0
|
||||
private var mapBottomPadding: Int = 0
|
||||
private var popupMenu: PopupMenu? = null
|
||||
private var insetBottom: Int = 0
|
||||
private lateinit var favToggle: MenuItem
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
@@ -215,27 +218,27 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
|
||||
binding.detailAppBar.toolbar.popupTheme =
|
||||
com.google.android.material.R.style.ThemeOverlay_AppCompat_DayNight
|
||||
com.google.android.material.R.style.Theme_Material3_DayNight
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { _, insets ->
|
||||
ViewCompat.onApplyWindowInsets(binding.root, insets)
|
||||
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
val density = resources.displayMetrics.density
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.detailAppBar.toolbar) { v, insets ->
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = systemWindowInsetTop
|
||||
}
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.fabLayers) { v, insets ->
|
||||
// margin of layers button: status bar height + toolbar height + margin
|
||||
val density = resources.displayMetrics.density
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
||||
val margin =
|
||||
if (binding.toolbarContainer.layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
||||
} else {
|
||||
systemWindowInsetTop + (12 * density).toInt()
|
||||
}
|
||||
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -244,11 +247,37 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
|
||||
// set map padding so that compass is not obstructed by toolbar
|
||||
mapTopPadding = systemWindowInsetTop + (48 * density).toInt() + (16 * density).toInt()
|
||||
mapBottomPadding = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||
// if we actually use map.setPadding here, MapLibre will re-trigger onApplyWindowInsets
|
||||
// and cause an infinite loop. So we rely on onMapReady being called later than
|
||||
// onApplyWindowInsets.
|
||||
|
||||
insets
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.fabLocate) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin =
|
||||
systemBars + resources.getDimensionPixelSize(com.mahc.custombottomsheetbehavior.R.dimen.fab_margin)
|
||||
}
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.navBarScrim) { v, insets ->
|
||||
insetBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||
v.layoutParams.height = insetBottom
|
||||
updatePeekHeight()
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.galleryContainer) { v, insets ->
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
||||
val newHeight =
|
||||
resources.getDimensionPixelSize(R.dimen.gallery_height_with_margin) + systemWindowInsetTop
|
||||
v.layoutParams.height = newHeight
|
||||
bottomSheetBehavior.anchorPoint = newHeight
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
exitTransition = TransitionInflater.from(requireContext())
|
||||
@@ -262,6 +291,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun updatePeekHeight() {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom + insetBottom
|
||||
}
|
||||
|
||||
private fun getMapProvider(provider: String) = when (provider) {
|
||||
"mapbox" -> MapFactory.MAPLIBRE
|
||||
"google" -> MapFactory.GOOGLE
|
||||
@@ -291,7 +324,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
|
||||
binding.detailView.topPart.doOnNextLayout {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
|
||||
updatePeekHeight()
|
||||
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
|
||||
}
|
||||
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
|
||||
@@ -409,24 +442,33 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
binding.detailView.sourceButton.setOnClickListener {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.openUrl(charger.url, binding.root, true)
|
||||
(activity as? MapsActivity)?.openUrl(charger.url ?: charger.dataSourceUrl, binding.root, true)
|
||||
}
|
||||
}
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
if (prefs.chargepriceNativeIntegration) {
|
||||
val extras =
|
||||
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
|
||||
findNavController().safeNavigate(
|
||||
MapFragmentDirections.actionMapToChargepriceFragment(charger),
|
||||
extras
|
||||
)
|
||||
} else {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
ChargepriceApi.getPoiUrl(charger),
|
||||
binding.root
|
||||
)
|
||||
|
||||
if (prefs.chargepriceCounter > 0 && !prefs.chargepriceRemoval2025DialogShown) {
|
||||
// user has been using the native Chargeprice integration before
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.chargeprice_removal_2025_dialog_title)
|
||||
.setMessage(R.string.chargeprice_removal_2025_dialog_detail)
|
||||
.setPositiveButton(R.string.ok) { di, _ ->
|
||||
di.cancel()
|
||||
prefs.chargepriceRemoval2025DialogShown = true
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
ChargepriceApi.getPoiUrl(charger),
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
.show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
ChargepriceApi.getPoiUrl(charger),
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
binding.detailView.btnChargerWebsite.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
@@ -470,7 +512,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
R.id.menu_share -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
if (charger != null && charger.url != null) {
|
||||
(activity as? MapsActivity)?.shareUrl(charger.url)
|
||||
}
|
||||
true
|
||||
@@ -478,7 +520,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
R.id.menu_edit -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger?.editUrl != null) {
|
||||
(activity as? MapsActivity)?.openUrl(charger.editUrl, binding.root, true)
|
||||
val uri = Uri.parse(charger.editUrl)
|
||||
if (uri.getScheme() == "mailto") {
|
||||
val intent = Intent(Intent.ACTION_SENDTO, uri)
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.no_email_app_found,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
else {
|
||||
(activity as? MapsActivity)?.openUrl(charger.editUrl, binding.root, true)
|
||||
}
|
||||
|
||||
if (vm.apiId.value == "goingelectric") {
|
||||
// instructions specific to GoingElectric
|
||||
Toast.makeText(
|
||||
@@ -616,16 +674,24 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
if (bottomSheetBehavior.state == STATE_HIDDEN) {
|
||||
map?.setPadding(0, mapTopPadding, 0, 0)
|
||||
map?.setPadding(0, mapTopPadding, 0, mapBottomPadding)
|
||||
} else {
|
||||
val height = binding.root.height - bottomSheet.top
|
||||
map?.setPadding(
|
||||
0,
|
||||
mapTopPadding,
|
||||
0,
|
||||
min(bottomSheetBehavior.peekHeight, height)
|
||||
mapBottomPadding + min(bottomSheetBehavior.peekHeight, height)
|
||||
)
|
||||
}
|
||||
println(slideOffset)
|
||||
if (bottomSheetBehavior.state != STATE_HIDDEN) {
|
||||
binding.navBarScrim.visibility = View.VISIBLE
|
||||
binding.navBarScrim.translationY =
|
||||
(if (slideOffset < 0f) -slideOffset else 2 * slideOffset) * binding.navBarScrim.height
|
||||
} else {
|
||||
binding.navBarScrim.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
@@ -659,6 +725,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
removeSearchFocus()
|
||||
binding.fabDirections.show()
|
||||
detailAppBarBehavior.setToolbarTitle(it.name)
|
||||
updateShareItemVisibility()
|
||||
updateFavoriteToggle()
|
||||
markerManager?.highlighedCharger = it
|
||||
markerManager?.animateBounce(it)
|
||||
@@ -769,6 +836,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateShareItemVisibility() {
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
val shareItem = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_share)
|
||||
shareItem.isVisible = charger.url != null
|
||||
}
|
||||
|
||||
private fun setupAdapters() {
|
||||
var viewer: StfalconImageViewer<ChargerPhoto>? = null
|
||||
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
|
||||
@@ -832,11 +905,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
(activity as? MapsActivity)?.showLocation(charger, binding.root)
|
||||
}
|
||||
R.drawable.ic_fault_report -> {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
charger.url,
|
||||
binding.root,
|
||||
true
|
||||
)
|
||||
if (charger.url != null) {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
charger.url,
|
||||
binding.root,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
R.drawable.ic_payment -> {
|
||||
@@ -1056,7 +1131,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
map.setTrafficEnabled(vm.mapTrafficEnabled.value ?: false)
|
||||
|
||||
// set padding so that compass is not obstructed by toolbar
|
||||
map.setPadding(0, mapTopPadding, 0, 0)
|
||||
map.setPadding(0, mapTopPadding, 0, mapBottomPadding)
|
||||
|
||||
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
map.setMapStyle(
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.URLSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -13,6 +16,7 @@ import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@@ -27,6 +31,8 @@ import net.vonforst.evmap.databinding.FragmentOnboardingWelcomeBinding
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.navigation.safeNavigate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.CustomUrlSpan
|
||||
import net.vonforst.evmap.ui.replaceUrlSpansWithCustom
|
||||
import net.vonforst.evmap.waitForLayout
|
||||
|
||||
class OnboardingFragment : Fragment() {
|
||||
@@ -220,6 +226,8 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
binding.rgDataSource.textView28,
|
||||
binding.rgDataSource.rbOpenStreetMap,
|
||||
binding.rgDataSource.textView29,
|
||||
binding.rgDataSource.rbNobil,
|
||||
binding.rgDataSource.textView30,
|
||||
binding.dataSourceHint,
|
||||
binding.cbAcceptPrivacy
|
||||
)
|
||||
@@ -235,19 +243,21 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.cbAcceptPrivacy.text =
|
||||
val text =
|
||||
HtmlCompat.fromHtml(
|
||||
getString(
|
||||
R.string.accept_privacy,
|
||||
getString(R.string.privacy_link)
|
||||
), HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
).replaceUrlSpansWithCustom()
|
||||
binding.cbAcceptPrivacy.text = text
|
||||
binding.cbAcceptPrivacy.linksClickable = true
|
||||
binding.cbAcceptPrivacy.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
binding.btnGetStarted.visibility = View.INVISIBLE
|
||||
|
||||
for (rb in listOf(
|
||||
binding.rgDataSource.rbGoingElectric,
|
||||
binding.rgDataSource.rbNobil,
|
||||
binding.rgDataSource.rbOpenChargeMap,
|
||||
binding.rgDataSource.rbOpenStreetMap
|
||||
)) {
|
||||
@@ -263,6 +273,7 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
if (prefs.dataSourceSet) {
|
||||
when (prefs.dataSource) {
|
||||
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
||||
"nobil" -> binding.rgDataSource.rbNobil.isChecked = true
|
||||
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
||||
"openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true
|
||||
}
|
||||
@@ -281,6 +292,8 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
|
||||
val result = if (binding.rgDataSource.rbGoingElectric.isChecked) {
|
||||
"goingelectric"
|
||||
} else if (binding.rgDataSource.rbNobil.isChecked) {
|
||||
"nobil"
|
||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||
"openchargemap"
|
||||
} else if (binding.rgDataSource.rbOpenStreetMap.isChecked) {
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
package net.vonforst.evmap.fragment.oauth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class OAuthLoginFragment : Fragment() {
|
||||
companion object {
|
||||
val ACTION_OAUTH_RESULT = "oauth_result"
|
||||
val EXTRA_URL = "url"
|
||||
}
|
||||
|
||||
@@ -72,11 +71,11 @@ class OAuthLoginFragment : Fragment() {
|
||||
}
|
||||
|
||||
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
|
||||
val uri = Uri.parse(args.url)
|
||||
val uri = args.url.toUri()
|
||||
|
||||
webView = view.findViewById(R.id.webView)
|
||||
|
||||
args.color?.let { webView.setBackgroundColor(Color.parseColor(it)) }
|
||||
args.color?.let { webView.setBackgroundColor(it.toColorInt()) }
|
||||
val progress = view.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
|
||||
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
@@ -89,13 +88,8 @@ class OAuthLoginFragment : Fragment() {
|
||||
|
||||
if (url.toString().startsWith(args.resultUrlPrefix)) {
|
||||
val result = Bundle()
|
||||
result.putString("url", url.toString())
|
||||
result.putString(EXTRA_URL, url.toString())
|
||||
setFragmentResult(args.url, result)
|
||||
context?.let {
|
||||
LocalBroadcastManager.getInstance(it).sendBroadcast(
|
||||
Intent(ACTION_OAUTH_RESULT).putExtra(EXTRA_URL, url)
|
||||
)
|
||||
}
|
||||
navController?.popBackStack()
|
||||
}
|
||||
|
||||
@@ -104,6 +98,9 @@ class OAuthLoginFragment : Fragment() {
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w("WebViewClient", url)
|
||||
}
|
||||
progress.show()
|
||||
}
|
||||
|
||||
@@ -112,6 +109,24 @@ class OAuthLoginFragment : Fragment() {
|
||||
progress.hide()
|
||||
webView.background = null
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError
|
||||
) {
|
||||
super.onReceivedError(view, request, error)
|
||||
Log.w("WebViewClient", error.toString())
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
errorResponse: WebResourceResponse
|
||||
) {
|
||||
super.onReceivedHttpError(view, request, errorResponse)
|
||||
Log.w("WebViewClient", "HTTP Error ${errorResponse.statusCode}")
|
||||
}
|
||||
}
|
||||
webView.settings.javaScriptEnabled = true
|
||||
webView.loadUrl(args.url)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.Preference
|
||||
@@ -30,6 +35,19 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
exitTransition = MaterialFadeThrough()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(listView) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.RangeSliderPreference
|
||||
import java.text.NumberFormat
|
||||
|
||||
class AndroidAutoSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
|
||||
private lateinit var rangePreference: RangeSliderPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
rangePreference = findPreference("chargeprice_battery_range_android_auto")!!
|
||||
rangePreference.labelFormatter = { value: Float ->
|
||||
val fmt = NumberFormat.getNumberInstance()
|
||||
fmt.maximumFractionDigits = 0
|
||||
fmt.format(value.toDouble()) + "%"
|
||||
}
|
||||
updateRangePreferenceSummary()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_android_auto, rootKey)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
"chargeprice_battery_range_android_auto_min", "chargeprice_battery_range_android_auto_max" -> {
|
||||
updateRangePreferenceSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRangePreferenceSummary() {
|
||||
val range = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
rangePreference.summary = getString(R.string.chargeprice_battery_range, range[0], range[1])
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,13 @@ package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
@@ -35,6 +40,19 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(listView) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.currencyDisplayName
|
||||
import net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
|
||||
class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
|
||||
private val vm: SettingsViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
SettingsViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
private lateinit var myVehiclePreference: MultiSelectDialogPreference
|
||||
private lateinit var myTariffsPreference: MultiSelectDialogPreference
|
||||
private lateinit var nativeIntegrationPreference: CheckBoxPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
nativeIntegrationPreference = findPreference("chargeprice_native_integration")!!
|
||||
|
||||
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
|
||||
myVehiclePreference.isEnabled = false
|
||||
vm.vehicles.observe(viewLifecycleOwner) { res ->
|
||||
res.data?.let { cars ->
|
||||
val sortedCars = cars.sortedBy { it.brand }
|
||||
myVehiclePreference.entryValues = sortedCars.map { it.id }.toTypedArray()
|
||||
myVehiclePreference.entries = sortedCars.map {
|
||||
SpannableStringBuilder().apply {
|
||||
appendLine("${it.brand} ${it.name}")
|
||||
append(
|
||||
it.formatSpecs(),
|
||||
RelativeSizeSpan(0.86f),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}.toTypedArray()
|
||||
myVehiclePreference.isEnabled = nativeIntegrationPreference.isChecked
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
}
|
||||
|
||||
myTariffsPreference = findPreference("chargeprice_my_tariffs")!!
|
||||
myTariffsPreference.isEnabled = false
|
||||
vm.tariffs.observe(viewLifecycleOwner) { res ->
|
||||
res.data?.let { tariffs ->
|
||||
myTariffsPreference.entryValues = tariffs.map { it.id }.toTypedArray()
|
||||
myTariffsPreference.entries = tariffs.map {
|
||||
if (!it.name.lowercase().startsWith(it.provider.lowercase())) {
|
||||
"${it.provider} ${it.name}"
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
}.toTypedArray()
|
||||
myTariffsPreference.isEnabled = nativeIntegrationPreference.isChecked
|
||||
updateMyTariffsSummary()
|
||||
}
|
||||
}
|
||||
updateNativeIntegrationState()
|
||||
|
||||
val currencyPreference = findPreference<ListPreference>("chargeprice_currency")!!
|
||||
currencyPreference.entries = currencyPreference.entryValues.map {
|
||||
currencyDisplayName(it.toString()).replaceFirstChar { it.uppercase() }
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
private fun updateNativeIntegrationState() {
|
||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||
val pref = preferenceScreen.getPreference(i)
|
||||
if (pref == nativeIntegrationPreference) {
|
||||
continue
|
||||
} else if (pref == myTariffsPreference) {
|
||||
pref.isEnabled =
|
||||
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
|
||||
} else if (pref == myVehiclePreference) {
|
||||
pref.isEnabled =
|
||||
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
|
||||
} else {
|
||||
pref.isEnabled = nativeIntegrationPreference.isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyTariffsSummary() {
|
||||
myTariffsPreference.summary =
|
||||
if (prefs.chargepriceMyTariffsAll) {
|
||||
getString(R.string.chargeprice_all_tariffs_selected)
|
||||
} else {
|
||||
val n = prefs.chargepriceMyTariffs?.size ?: 0
|
||||
requireContext().resources
|
||||
.getQuantityString(
|
||||
R.plurals.chargeprice_some_tariffs_selected,
|
||||
n,
|
||||
n
|
||||
) + "\n" + requireContext().resources
|
||||
.getQuantityString(R.plurals.pref_my_tariffs_summary, n)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyVehiclesSummary() {
|
||||
vm.vehicles.value?.data?.let { cars ->
|
||||
val vehicles = cars.filter { it.id in prefs.chargepriceMyVehicles }
|
||||
val summary = vehicles.joinToString(", ") {
|
||||
"${it.brand} ${it.name}"
|
||||
}
|
||||
myVehiclePreference.summary = summary
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_chargeprice, rootKey)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
"chargeprice_my_vehicle" -> {
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
|
||||
"chargeprice_my_tariffs" -> {
|
||||
updateMyTariffsSummary()
|
||||
}
|
||||
|
||||
"chargeprice_native_integration" -> {
|
||||
updateNativeIntegrationState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -17,6 +17,7 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
@@ -31,8 +32,6 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
viewModelFactory {
|
||||
SettingsViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -146,7 +145,7 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
val args = OAuthLoginFragmentArgs(
|
||||
uri.toString(),
|
||||
TeslaAuthenticationApi.resultUrlPrefix,
|
||||
"#000000"
|
||||
"#FFFFFF"
|
||||
).toBundle()
|
||||
|
||||
setFragmentResultListener(uri.toString()) { _, result ->
|
||||
@@ -159,7 +158,7 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) {
|
||||
teslaAccountPreference.summary = getString(R.string.logging_in)
|
||||
|
||||
val url = Uri.parse(result.getString("url"))
|
||||
val url = result.getString(OAuthLoginFragment.EXTRA_URL)!!.toUri()
|
||||
val code = url.getQueryParameter("code") ?: return
|
||||
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
|
||||
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
|
||||
|
||||
@@ -37,6 +37,7 @@ sealed class ChargepointListItem
|
||||
* @param address The charge location address
|
||||
* @param chargepoints List of chargepoints at this location
|
||||
* @param network The charging network (Mobility Service Provider, MSP)
|
||||
* @param dataSourceUrl A link to the data source website
|
||||
* @param url A link to this charging site
|
||||
* @param editUrl A link to a website where this charging site can be edited
|
||||
* @param faultReport Set this if the charging site is reported to be out of service
|
||||
@@ -49,6 +50,7 @@ sealed class ChargepointListItem
|
||||
* @param locationDescription Directions on how to find the charger (e.g. "In the parking garage on level 5")
|
||||
* @param photos List of photos of this charging site
|
||||
* @param chargecards List of charge cards accepted here
|
||||
* @param accessibility Specifies who may use this charge location
|
||||
* @param openinghours List of times when this charging site can be accessed / used
|
||||
* @param cost The cost for charging and/or parking
|
||||
* @param license How the data about this chargepoint is licensed
|
||||
@@ -67,7 +69,8 @@ data class ChargeLocation(
|
||||
@Embedded val address: Address?,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
val network: String?,
|
||||
val url: String, // URL of this charger at the data source
|
||||
val dataSourceUrl: String, // URL to the data source
|
||||
val url: String?, // URL of this charger at the data source
|
||||
val editUrl: String?, // URL to edit this charger at the data source
|
||||
@Embedded(prefix = "fault_report_") val faultReport: FaultReport?,
|
||||
val verified: Boolean,
|
||||
@@ -79,6 +82,7 @@ data class ChargeLocation(
|
||||
val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
val chargecards: List<ChargeCardId>?,
|
||||
val accessibility: String?,
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?,
|
||||
val license: String?,
|
||||
@@ -135,9 +139,13 @@ data class ChargeLocation(
|
||||
val filtered = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
val count = filtered.sumOf { it.count }
|
||||
val mergedEvseIds = filtered.map { if (it.evseIds == null) List(it.count) {null} else it.evseIds }.flatten()
|
||||
val mergedEvseUIds = filtered.map { if (it.evseUIds == null) List(it.count) {null} else it.evseUIds }.flatten()
|
||||
Chargepoint(variant.type, variant.power, count,
|
||||
filtered.map { it.current }.distinct().singleOrNull(),
|
||||
filtered.map { it.voltage }.distinct().singleOrNull()
|
||||
filtered.map { it.voltage }.distinct().singleOrNull(),
|
||||
if (mergedEvseIds.all { it == null }) null else mergedEvseIds,
|
||||
if (mergedEvseUIds.all { it == null }) null else mergedEvseUIds
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -417,7 +425,11 @@ data class Chargepoint(
|
||||
// Max voltage in V (or null if unknown).
|
||||
// note that for DC chargers: current * voltage may be larger than power
|
||||
// (each of the three can be separately limited)
|
||||
val voltage: Double? = null
|
||||
val voltage: Double? = null,
|
||||
// Electric Vehicle Supply Equipment Ids for this Chargepoint's plugs/sockets
|
||||
val evseIds: List<String?>? = null,
|
||||
// Electric Vehicle Supply Equipment Unique Ids for this Chargepoint's plugs/sockets
|
||||
val evseUIds: List<String?>? = null
|
||||
) : Equatable, Parcelable {
|
||||
fun hasKnownPower(): Boolean = power != null
|
||||
fun hasKnownVoltageAndCurrent(): Boolean = voltage != null && current != null
|
||||
|
||||
@@ -25,4 +25,4 @@ data class Favorite(
|
||||
data class FavoriteWithDetail(
|
||||
@Embedded val favorite: Favorite,
|
||||
@Embedded val charger: ChargeLocation
|
||||
)
|
||||
)
|
||||
@@ -35,7 +35,9 @@ class CustomNavigator(
|
||||
val prefs = PreferenceDataSource(context)
|
||||
val url = when (prefs.dataSource) {
|
||||
"goingelectric" -> "https://www.goingelectric.de/stromtankstellen/new/"
|
||||
"nobil" -> "http://nobil.no/api/chargerregistration/chargerregistration.php?action=register"
|
||||
"openchargemap" -> "https://openchargemap.org/site/poi/add"
|
||||
"openstreetmap" -> "https://www.openstreetmap.org/edit"
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
launchCustomTab(url)
|
||||
|
||||
@@ -16,14 +16,15 @@ import java.time.Instant
|
||||
* successful.
|
||||
*/
|
||||
class CacheLiveData<T>(
|
||||
cache: LiveData<T>,
|
||||
cache: LiveData<Resource<T>>,
|
||||
api: LiveData<Resource<T>>,
|
||||
skipApi: LiveData<Boolean>? = null
|
||||
) :
|
||||
MediatorLiveData<Resource<T>>() {
|
||||
private var cacheResult: T? = null
|
||||
private var cacheResult: Resource<T>? = null
|
||||
private var apiResult: Resource<T>? = null
|
||||
private var skipApiResult: Boolean = false
|
||||
private val apiLiveData = api
|
||||
|
||||
init {
|
||||
updateValue()
|
||||
@@ -64,9 +65,21 @@ class CacheLiveData<T>(
|
||||
Log.d("CacheLiveData", "cache has finished loading before API")
|
||||
// cache has finished loading before API
|
||||
if (skipApiResult) {
|
||||
value = Resource.success(cache)
|
||||
value = when (cache.status) {
|
||||
Status.SUCCESS -> cache
|
||||
Status.ERROR -> {
|
||||
Log.d("CacheLiveData", "Cache returned an error, querying API")
|
||||
addSource(apiLiveData) {
|
||||
apiResult = it
|
||||
updateValue()
|
||||
}
|
||||
Resource.loading(null)
|
||||
}
|
||||
|
||||
Status.LOADING -> cache
|
||||
}
|
||||
} else {
|
||||
value = Resource.loading(cache)
|
||||
value = Resource.loading(cache.data)
|
||||
}
|
||||
} else if (cache == null && api != null) {
|
||||
Log.d("CacheLiveData", "API has finished loading before cache")
|
||||
@@ -81,7 +94,7 @@ class CacheLiveData<T>(
|
||||
// Both cache and API have finished loading
|
||||
value = when (api.status) {
|
||||
Status.SUCCESS -> api
|
||||
Status.ERROR -> Resource.error(api.message, cache)
|
||||
Status.ERROR -> Resource.error(api.message, cache.data)
|
||||
Status.LOADING -> api // should not occur
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import net.vonforst.evmap.api.FiltersSQLQuery
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.nobil.NobilApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.openstreetmap.OSMReferenceData
|
||||
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
|
||||
@@ -68,6 +69,12 @@ abstract class ChargeLocationsDao {
|
||||
@Query("DELETE FROM chargelocation WHERE NOT EXISTS (SELECT 1 FROM favorite WHERE favorite.chargerId = chargelocation.id)")
|
||||
abstract suspend fun deleteAllIfNotFavorite()
|
||||
|
||||
@Query("SELECT id FROM chargelocation WHERE dataSource == :dataSource")
|
||||
abstract suspend fun getAllIds(dataSource: String): List<Long>
|
||||
|
||||
@Query("DELETE FROM chargelocation WHERE dataSource == :dataSource AND id IN (:chargerIds)")
|
||||
abstract suspend fun deleteById(dataSource: String, chargerIds: List<Long>)
|
||||
|
||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND id == :id AND isDetailed == 1 AND timeRetrieved > :after")
|
||||
abstract suspend fun getChargeLocationById(
|
||||
id: Long,
|
||||
@@ -83,7 +90,7 @@ abstract class ChargeLocationsDao {
|
||||
): List<ChargeLocation>
|
||||
|
||||
@SkipQueryVerification
|
||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND timeRetrieved > :after AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildMbr(:lng1, :lat1, :lng2, :lat2))")
|
||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND timeRetrieved > :after AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' AND search_frame = BuildMbr(:lng1, :lat1, :lng2, :lat2))")
|
||||
abstract suspend fun getChargeLocationsInBounds(
|
||||
lat1: Double,
|
||||
lat2: Double,
|
||||
@@ -94,7 +101,7 @@ abstract class ChargeLocationsDao {
|
||||
): List<ChargeLocation>
|
||||
|
||||
@SkipQueryVerification
|
||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND PtDistWithin(coordinates, MakePoint(:lng, :lat, 4326), :radius) AND timeRetrieved > :after AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildCircleMbr(:lng, :lat, :radius)) ORDER BY Distance(coordinates, MakePoint(:lng, :lat, 4326))")
|
||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND PtDistWithin(coordinates, MakePoint(:lng, :lat, 4326), :radius) AND timeRetrieved > :after AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' AND search_frame = BuildCircleMbr(:lng, :lat, :radius)) ORDER BY Distance(coordinates, MakePoint(:lng, :lat, 4326))")
|
||||
abstract suspend fun getChargeLocationsRadius(
|
||||
lat: Double,
|
||||
lng: Double,
|
||||
@@ -193,6 +200,10 @@ class ChargeLocationsRepository(
|
||||
).getReferenceData()
|
||||
}
|
||||
|
||||
is NobilApiWrapper -> {
|
||||
NobilReferenceDataRepository(scope, prefs).getReferenceData()
|
||||
}
|
||||
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
@@ -235,6 +246,7 @@ class ChargeLocationsRepository(
|
||||
val dbResult = if (filters.isNullOrEmpty()) {
|
||||
liveData {
|
||||
emit(
|
||||
Resource.success(
|
||||
chargeLocationsDao.getChargeLocationsClustered(
|
||||
bounds.southwest.latitude,
|
||||
bounds.northeast.latitude,
|
||||
@@ -244,6 +256,7 @@ class ChargeLocationsRepository(
|
||||
cacheLimitDate(api),
|
||||
zoom
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -251,7 +264,7 @@ class ChargeLocationsRepository(
|
||||
}.map {
|
||||
val t2 = System.currentTimeMillis()
|
||||
Log.d(TAG, "DB loading time: ${t2 - t1}")
|
||||
Log.d(TAG, "number of chargers: ${it.size}")
|
||||
Log.d(TAG, "number of chargers: ${it.data?.size}")
|
||||
it
|
||||
}
|
||||
val filtersSerialized =
|
||||
@@ -321,7 +334,7 @@ class ChargeLocationsRepository(
|
||||
job.join()
|
||||
progressJob.cancelAndJoin()
|
||||
}
|
||||
emit(Resource.success(dbResult.await()))
|
||||
emit(dbResult.await())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +376,7 @@ class ChargeLocationsRepository(
|
||||
val dbResult = if (filters.isNullOrEmpty()) {
|
||||
liveData {
|
||||
emit(
|
||||
Resource.success(
|
||||
chargeLocationsDao.getChargeLocationsRadius(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
@@ -370,6 +384,7 @@ class ChargeLocationsRepository(
|
||||
api.id,
|
||||
cacheLimitDate(api)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -446,7 +461,7 @@ class ChargeLocationsRepository(
|
||||
job.join()
|
||||
progressJob.cancelAndJoin()
|
||||
}
|
||||
emit(Resource.success(dbResult.await()))
|
||||
emit(dbResult.await())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,7 +561,7 @@ class ChargeLocationsRepository(
|
||||
api: ChargepointApi<ReferenceData>,
|
||||
filters: FilterValues,
|
||||
bounds: LatLngBounds
|
||||
): LiveData<List<ChargeLocation>> {
|
||||
): LiveData<Resource<List<ChargeLocation>>> {
|
||||
return queryWithFilters(api, filters, boundsSpatialIndexQuery(bounds))
|
||||
}
|
||||
|
||||
@@ -555,7 +570,7 @@ class ChargeLocationsRepository(
|
||||
filters: FilterValues,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float
|
||||
): LiveData<List<ChargepointListItem>> {
|
||||
): LiveData<Resource<List<ChargepointListItem>>> {
|
||||
return queryWithFiltersClustered(api, filters, boundsSpatialIndexQuery(bounds), zoom)
|
||||
}
|
||||
|
||||
@@ -564,7 +579,7 @@ class ChargeLocationsRepository(
|
||||
filters: FilterValues,
|
||||
location: LatLng,
|
||||
radius: Double
|
||||
): LiveData<List<ChargeLocation>> {
|
||||
): LiveData<Resource<List<ChargeLocation>>> {
|
||||
val region =
|
||||
radiusSpatialIndexQuery(location, radius)
|
||||
val order =
|
||||
@@ -573,17 +588,17 @@ class ChargeLocationsRepository(
|
||||
}
|
||||
|
||||
private fun boundsSpatialIndexQuery(bounds: LatLngBounds) =
|
||||
"ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildMbr(${bounds.southwest.longitude}, ${bounds.southwest.latitude}, ${bounds.northeast.longitude}, ${bounds.northeast.latitude}))"
|
||||
"ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' AND search_frame = BuildMbr(${bounds.southwest.longitude}, ${bounds.southwest.latitude}, ${bounds.northeast.longitude}, ${bounds.northeast.latitude}))"
|
||||
|
||||
private fun radiusSpatialIndexQuery(location: LatLng, radius: Double) =
|
||||
"PtDistWithin(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326), ${radius}) AND ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildCircleMbr(${location.longitude}, ${location.latitude}, $radius))"
|
||||
"PtDistWithin(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326), ${radius}) AND ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' AND search_frame = BuildCircleMbr(${location.longitude}, ${location.latitude}, $radius))"
|
||||
|
||||
private fun queryWithFilters(
|
||||
api: ChargepointApi<ReferenceData>,
|
||||
filters: FilterValues,
|
||||
regionSql: String,
|
||||
orderSql: String? = null
|
||||
): LiveData<List<ChargeLocation>> = referenceData.singleSwitchMap { refData ->
|
||||
): LiveData<Resource<List<ChargeLocation>>> = referenceData.singleSwitchMap { refData ->
|
||||
try {
|
||||
val query = api.convertFiltersToSQL(filters, refData)
|
||||
val after = cacheLimitDate(api)
|
||||
@@ -591,12 +606,14 @@ class ChargeLocationsRepository(
|
||||
|
||||
liveData {
|
||||
emit(
|
||||
Resource.success(
|
||||
chargeLocationsDao.getChargeLocationsCustom(
|
||||
SimpleSQLiteQuery(
|
||||
sql,
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: NotImplementedError) {
|
||||
@@ -610,7 +627,7 @@ class ChargeLocationsRepository(
|
||||
regionSql: String,
|
||||
zoom: Float,
|
||||
orderSql: String? = null
|
||||
): LiveData<List<ChargepointListItem>> = referenceData.singleSwitchMap { refData ->
|
||||
): LiveData<Resource<List<ChargepointListItem>>> = referenceData.singleSwitchMap { refData ->
|
||||
try {
|
||||
if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
|
||||
queryWithFilters(api, filters, regionSql, orderSql).map { it }
|
||||
@@ -632,13 +649,19 @@ class ChargeLocationsRepository(
|
||||
.map { it.ids }
|
||||
.flatten(), prefs.dataSource, after)
|
||||
emit(
|
||||
Resource.success(
|
||||
clusters.filter { it.clusterCount > 1 }
|
||||
.map { it.convert() } + singleChargers
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
} catch (e: NotImplementedError) {
|
||||
MutableLiveData() // in this case we cannot get a DB result
|
||||
MutableLiveData(
|
||||
Resource.error(
|
||||
e.message,
|
||||
null
|
||||
)
|
||||
) // in this case we cannot get a DB result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +709,7 @@ class ChargeLocationsRepository(
|
||||
val result = api.fullDownload()
|
||||
try {
|
||||
var insertJob: Job? = null
|
||||
val idsToDelete = chargeLocationsDao.getAllIds(api.id).toMutableSet()
|
||||
result.chargers.chunked(1024).forEach {
|
||||
insertJob?.join()
|
||||
insertJob = withContext(Dispatchers.IO) {
|
||||
@@ -693,8 +717,12 @@ class ChargeLocationsRepository(
|
||||
chargeLocationsDao.insert(*it.toTypedArray())
|
||||
}
|
||||
}
|
||||
idsToDelete.removeAll(it.map { it.id })
|
||||
fullDownloadProgress.value = result.progress
|
||||
}
|
||||
// delete chargers that have been removed
|
||||
chargeLocationsDao.deleteById(api.id, idsToDelete.toList())
|
||||
|
||||
val region = Mbr(
|
||||
-180.0,
|
||||
-90.0,
|
||||
|
||||
@@ -15,7 +15,7 @@ class CleanupCacheWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||
val savedRegionDao = db.savedRegionDao()
|
||||
val now = Instant.now()
|
||||
|
||||
val dataSources = listOf("openchargemap", "openstreetmap", "goingelectric")
|
||||
val dataSources = listOf("openchargemap", "openstreetmap", "goingelectric", "nobil")
|
||||
for (dataSource in dataSources) {
|
||||
val api = createApi(dataSource, applicationContext)
|
||||
val limit = now.minus(api.cacheLimit).toEpochMilli()
|
||||
|
||||
@@ -40,7 +40,7 @@ import net.vonforst.evmap.model.SliderFilterValue
|
||||
OCMOperator::class,
|
||||
OSMNetwork::class,
|
||||
SavedRegion::class
|
||||
], version = 24
|
||||
], version = 28
|
||||
)
|
||||
@TypeConverters(Converters::class, GeometryConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -84,12 +84,14 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
|
||||
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
|
||||
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21,
|
||||
MIGRATION_22, MIGRATION_23, MIGRATION_24
|
||||
MIGRATION_22, MIGRATION_23, MIGRATION_24, MIGRATION_25, MIGRATION_26,
|
||||
MIGRATION_27, MIGRATION_28
|
||||
)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
// create default filter profile for each data source
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('goingelectric', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('nobil', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openchargemap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openstreetmap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
// initialize spatialite columns
|
||||
@@ -501,6 +503,58 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_25 = object : Migration(24, 25) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// API nobil added
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('nobil', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_26 = object : Migration(25, 26) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// adding dataSourceUrl and making url optional
|
||||
try {
|
||||
db.beginTransaction()
|
||||
db.execSQL(
|
||||
"CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `coordinatesProjected` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, PRIMARY KEY(`id`, `dataSource`))"
|
||||
)
|
||||
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT `id`, `dataSource`, `name`, `coordinates`, `coordinatesProjected`, `chargepoints`, `network`, '', `url`, `editUrl`, `verified`, `barrierFree`, `operator`, `generalInformation`, `amenities`, `locationDescription`, `photos`, `chargecards`, `license`, `timeRetrieved`, `isDetailed`, `city`, `country`, `postcode`, `street`, `fault_report_created`, `fault_report_description`, `twentyfourSeven`, `description`, `mostart`, `moend`, `tustart`, `tuend`, `westart`, `weend`, `thstart`, `thend`, `frstart`, `frend`, `sastart`, `saend`, `sustart`, `suend`, `hostart`, `hoend`, `freecharging`, `freeparking`, `descriptionShort`, `descriptionLong`, `chargepricecountry`, `chargepricenetwork`, `chargepriceplugTypes`, `networkUrl`, `chargerUrl` FROM `ChargeLocation`")
|
||||
db.execSQL("UPDATE ChargeLocationNew SET `dataSourceUrl` = 'https://www.goingelectric.de/' WHERE `dataSource` = 'goingelectric'")
|
||||
db.execSQL("UPDATE ChargeLocationNew SET `dataSourceUrl` = 'https://openchargemap.org/' WHERE `dataSource` = 'openchargemap'")
|
||||
db.execSQL("UPDATE ChargeLocationNew SET `dataSourceUrl` = 'https://www.openstreetmap.org/' WHERE `dataSource` = 'openstreetmap'")
|
||||
db.query("SELECT DropGeoTable('ChargeLocation', FALSE)").moveToNext()
|
||||
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
|
||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinates', 4326, 'POINT', 'XY');")
|
||||
.moveToNext()
|
||||
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinates');")
|
||||
.moveToNext()
|
||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinatesProjected', 3857, 'POINT', 'XY');")
|
||||
.moveToNext()
|
||||
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinatesProjected');")
|
||||
.moveToNext()
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_27 = object : Migration(26, 27) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// adding accessibility to ChargeLocation
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `accessibility` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_28 = object : Migration(27, 28) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// Force nobil data refresh to fetch EVSE UId attributes needed for real-time data
|
||||
db.execSQL("DELETE FROM SavedRegion WHERE `dataSource` = 'nobil'")
|
||||
db.execSQL("DELETE FROM ChargeLocation WHERE `dataSource` = 'nobil'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,10 +13,10 @@ interface FavoritesDao {
|
||||
@Delete
|
||||
suspend fun delete(vararg favorites: Favorite)
|
||||
|
||||
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
|
||||
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL")
|
||||
fun getAllFavorites(): LiveData<List<FavoriteWithDetail>>
|
||||
|
||||
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
|
||||
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL")
|
||||
suspend fun getAllFavoritesAsync(): List<FavoriteWithDetail>
|
||||
|
||||
@SkipQueryVerification
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.nobil.*
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Dao
|
||||
abstract class NobilReferenceDataDao {
|
||||
}
|
||||
|
||||
class NobilReferenceDataRepository(
|
||||
private val scope: CoroutineScope,
|
||||
private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getReferenceData(): LiveData<NobilReferenceData> {
|
||||
return MediatorLiveData<NobilReferenceData>().apply {
|
||||
value = NobilReferenceData(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import java.time.Instant
|
||||
import androidx.core.content.edit
|
||||
|
||||
class PreferenceDataSource(val context: Context) {
|
||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -152,87 +153,6 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceNativeIntegration: Boolean
|
||||
get() = sp.getBoolean("chargeprice_native_integration", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_native_integration", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyVehicles: Set<String>
|
||||
get() = try {
|
||||
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
|
||||
} catch (e: ClassCastException) {
|
||||
// backwards compatibility
|
||||
sp.getString("chargeprice_my_vehicle", null)?.let { setOf(it) } ?: emptySet()
|
||||
}
|
||||
set(value) {
|
||||
sp.edit().putStringSet("chargeprice_my_vehicle", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceLastSelectedVehicle: String?
|
||||
get() = sp.getString("chargeprice_last_vehicle", null)
|
||||
set(value) {
|
||||
sp.edit().putString("chargeprice_last_vehicle", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyTariffs: Set<String>?
|
||||
get() = sp.getStringSet("chargeprice_my_tariffs", null)
|
||||
set(value) {
|
||||
sp.edit().putStringSet("chargeprice_my_tariffs", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyTariffsAll: Boolean
|
||||
get() = sp.getBoolean("chargeprice_my_tariffs_all", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_my_tariffs_all", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceNoBaseFee: Boolean
|
||||
get() = sp.getBoolean("chargeprice_no_base_fee", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_no_base_fee", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceShowProviderCustomerTariffs: Boolean
|
||||
get() = sp.getBoolean("chargeprice_show_provider_customer_tariffs", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_show_provider_customer_tariffs", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceAllowUnbalancedLoad: Boolean
|
||||
get() = sp.getBoolean("chargeprice_allow_unbalanced_load", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_allow_unbalanced_load", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceCurrency: String
|
||||
get() = sp.getString("chargeprice_currency", null) ?: "EUR"
|
||||
set(value) {
|
||||
sp.edit().putString("chargeprice_currency", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceBatteryRange: List<Float>
|
||||
get() = listOf(
|
||||
sp.getFloat("chargeprice_battery_range_min", 20f),
|
||||
sp.getFloat("chargeprice_battery_range_max", 80f),
|
||||
)
|
||||
set(value) {
|
||||
sp.edit().putFloat("chargeprice_battery_range_min", value[0])
|
||||
.putFloat("chargeprice_battery_range_max", value[1])
|
||||
.apply()
|
||||
}
|
||||
|
||||
var chargepriceBatteryRangeAndroidAuto: List<Float>
|
||||
get() = listOf(
|
||||
sp.getFloat("chargeprice_battery_range_android_auto_min", 20f),
|
||||
sp.getFloat("chargeprice_battery_range_android_auto_max", 80f),
|
||||
)
|
||||
set(value) {
|
||||
sp.edit().putFloat("chargeprice_battery_range_android_auto_min", value[0])
|
||||
.putFloat("chargeprice_battery_range_android_auto_max", value[1])
|
||||
.apply()
|
||||
}
|
||||
|
||||
/** App start counter, introduced with Version 1.0.0 */
|
||||
var appStartCounter: Long
|
||||
get() = sp.getLong("app_start_counter", 0)
|
||||
@@ -248,6 +168,12 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putLong("chargeprice_counter", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceRemoval2025DialogShown: Boolean
|
||||
get() = sp.getBoolean("chargeprice_removal_2025_dialog_shown", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_removal_2025_dialog_shown", value).apply()
|
||||
}
|
||||
|
||||
var opensourceDonationsDialogLastShown: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("opensource_donations_dialog_last_shown", 0L))
|
||||
set(value) {
|
||||
@@ -323,6 +249,18 @@ class PreferenceDataSource(val context: Context) {
|
||||
set(value) {
|
||||
sp.edit().putBoolean("privacy_accepted", value).apply()
|
||||
}
|
||||
|
||||
var androidAutoCompassEnabled: Boolean
|
||||
get() = sp.getBoolean("android_auto_compass_enabled", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("android_auto_compass_enabled", value).apply()
|
||||
}
|
||||
|
||||
var androidAutoNewMapScreenEnabled: Boolean
|
||||
get() = sp.getBoolean("android_auto_new_map_screen_enabled", false)
|
||||
set(value) {
|
||||
sp.edit { putBoolean("android_auto_new_map_screen_enabled", value) }
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.getLatLng(key: String): LatLng? =
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.nobil.NobilChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.openchargemap.OCMChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.openstreetmap.ImgurChargerPhoto
|
||||
import net.vonforst.evmap.autocomplete.AutocompletePlaceType
|
||||
@@ -23,6 +24,7 @@ class Converters {
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ChargerPhoto::class.java, "type")
|
||||
.withSubtype(GEChargerPhotoAdapter::class.java, "goingelectric")
|
||||
.withSubtype(NobilChargerPhotoAdapter::class.java, "nobil")
|
||||
.withSubtype(OCMChargerPhotoAdapter::class.java, "openchargemap")
|
||||
.withSubtype(ImgurChargerPhoto::class.java, "imgur")
|
||||
.withDefaultValue(null)
|
||||
|
||||
@@ -23,6 +23,7 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
|
||||
|
||||
var insertJob: Job? = null
|
||||
val result = api.fullDownload()
|
||||
val idsToDelete = chargeLocations.getAllIds(api.id).toMutableSet()
|
||||
result.chargers.chunked(1024).forEach {
|
||||
insertJob?.join()
|
||||
insertJob = withContext(Dispatchers.IO) {
|
||||
@@ -30,8 +31,12 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
|
||||
chargeLocations.insert(*it.toTypedArray())
|
||||
}
|
||||
}
|
||||
idsToDelete.removeAll(it.map { it.id })
|
||||
}
|
||||
|
||||
// delete chargers that have been removed
|
||||
chargeLocations.deleteById(api.id, idsToDelete.toList())
|
||||
|
||||
when (api) {
|
||||
is OpenStreetMapApiWrapper -> {
|
||||
val refData = result.referenceData
|
||||
@@ -40,7 +45,6 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove deleted chargers
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
@@ -254,19 +254,6 @@ fun setChargepriceTagColor(view: TextView, kind: String) {
|
||||
)
|
||||
}
|
||||
|
||||
@BindingAdapter("chargepriceTagIcon")
|
||||
fun setChargepriceTagIcon(view: TextView, kind: String) {
|
||||
view.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
when (kind) {
|
||||
"star" -> R.drawable.ic_chargeprice_star
|
||||
"alert" -> R.drawable.ic_chargeprice_alert
|
||||
"info" -> R.drawable.ic_chargeprice_info
|
||||
"lock" -> R.drawable.ic_chargeprice_lock
|
||||
else -> 0
|
||||
}, 0, 0, 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun availabilityColor(
|
||||
status: List<ChargepointStatus>?,
|
||||
context: Context
|
||||
|
||||
27
app/src/main/java/net/vonforst/evmap/ui/CustomUrlSpan.kt
Normal file
27
app/src/main/java/net/vonforst/evmap/ui/CustomUrlSpan.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import android.view.View
|
||||
import androidx.core.text.getSpans
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
|
||||
class CustomUrlSpan(url: String): URLSpan(url) {
|
||||
override fun onClick(widget: View) {
|
||||
(widget.context as? MapsActivity)?.let {
|
||||
it.openUrl(url, widget.rootView)
|
||||
} ?: {
|
||||
super.onClick(widget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Spanned.replaceUrlSpansWithCustom(): Spanned {
|
||||
val builder = SpannableStringBuilder(this)
|
||||
builder.getSpans<URLSpan>().forEach {
|
||||
builder.setSpan(CustomUrlSpan(it.url), builder.getSpanStart(it), builder.getSpanEnd(it), builder.getSpanFlags(it))
|
||||
builder.removeSpan(it)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import jsonapi.Meta
|
||||
import jsonapi.Relationship
|
||||
import jsonapi.Relationships
|
||||
import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class ChargepriceViewModel(
|
||||
application: Application,
|
||||
chargepriceApiKey: String,
|
||||
chargepriceApiUrl: String,
|
||||
private val state: SavedStateHandle
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
val charger: MutableLiveData<ChargeLocation> by lazy {
|
||||
state.getLiveData("charger")
|
||||
}
|
||||
|
||||
val chargepoint: MutableLiveData<Chargepoint?> by lazy {
|
||||
state.getLiveData("chargepoint")
|
||||
}
|
||||
|
||||
private val vehicleIds: MutableLiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>().apply {
|
||||
value = prefs.chargepriceMyVehicles
|
||||
}
|
||||
}
|
||||
|
||||
val vehicles: LiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
addSource(vehicleIds.distinctUntilChanged()) { vehicleIds ->
|
||||
if (vehicleIds.isEmpty()) {
|
||||
value = Resource.success(emptyList())
|
||||
} else {
|
||||
value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
value = try {
|
||||
val result = api.getVehicles()
|
||||
Resource.success(result.filter {
|
||||
it.id in vehicleIds
|
||||
})
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
observeForever {
|
||||
vehicle.value = it.data?.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val vehicle: MutableLiveData<ChargepriceCar> by lazy {
|
||||
state.getLiveData("vehicle")
|
||||
}
|
||||
|
||||
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
|
||||
MediatorLiveData<List<String>>().apply {
|
||||
addSource(vehicle) {
|
||||
value = it?.compatibleEvmapConnectors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val noCompatibleConnectors: LiveData<Boolean> by lazy {
|
||||
MediatorLiveData<Boolean>().apply {
|
||||
value = false
|
||||
listOf(charger, vehicleCompatibleConnectors).forEach {
|
||||
addSource(it) {
|
||||
val charger = charger.value ?: return@addSource
|
||||
val connectors = vehicleCompatibleConnectors.value ?: return@addSource
|
||||
value = !charger.chargepoints.flatMap { equivalentPlugTypes(it.type) }
|
||||
.any { it in connectors }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val batteryRange: MutableLiveData<List<Float>> by lazy {
|
||||
MutableLiveData<List<Float>>().apply {
|
||||
value = prefs.chargepriceBatteryRange
|
||||
observeForever {
|
||||
if (it[0] == it[1]) {
|
||||
value = if (it[0] < 1.0) {
|
||||
listOf(it[0], it[1] + 1)
|
||||
} else {
|
||||
listOf(it[0] - 1, it[1])
|
||||
}
|
||||
}
|
||||
prefs.chargepriceBatteryRange = value!!
|
||||
}
|
||||
}
|
||||
}
|
||||
val batteryRangeSliderDragging: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
|
||||
val chargePrices: MutableLiveData<Resource<List<ChargePrice>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
value = state["chargePrices"] ?: Resource.loading(null)
|
||||
listOf(
|
||||
vehicle,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors,
|
||||
myTariffs, myTariffsAll, charger
|
||||
).forEach {
|
||||
addSource(it.distinctUntilChanged()) {
|
||||
if (!batteryRangeSliderDragging.value!!) {
|
||||
loadPrices()
|
||||
state["chargePrices"] = this.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chargePriceMeta: MutableLiveData<Resource<ChargepriceMeta>> by lazy {
|
||||
MutableLiveData<Resource<ChargepriceMeta>>().apply {
|
||||
value = Resource.loading(null)
|
||||
}
|
||||
}
|
||||
|
||||
val chargePricesForChargepoint: MediatorLiveData<Resource<List<ChargePrice>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
listOf(chargePrices, chargepoint).forEach {
|
||||
addSource(it) {
|
||||
val cps = chargePrices.value
|
||||
val chargepoint = chargepoint.value
|
||||
if (cps == null || chargepoint == null) {
|
||||
value = null
|
||||
} else if (cps.status == Status.ERROR) {
|
||||
value = Resource.error(cps.message, null)
|
||||
} else if (cps.status == Status.LOADING) {
|
||||
value = Resource.loading(null)
|
||||
} else {
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
value = Resource.success(cps.data!!.mapNotNull { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == getChargepricePlugType(chargepoint) && it.power == chargepoint.power
|
||||
}
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.copy(
|
||||
chargepointPrices = filteredPrices
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadPrefs() {
|
||||
vehicleIds.value = prefs.chargepriceMyVehicles
|
||||
}
|
||||
|
||||
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
|
||||
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
|
||||
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
|
||||
return type
|
||||
}
|
||||
|
||||
val myTariffs: LiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>().apply {
|
||||
value = prefs.chargepriceMyTariffs
|
||||
}
|
||||
}
|
||||
val myTariffsAll: LiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = prefs.chargepriceMyTariffsAll
|
||||
}
|
||||
}
|
||||
|
||||
val chargepriceMetaForChargepoint: MediatorLiveData<Resource<ChargepriceChargepointMeta>> by lazy {
|
||||
MediatorLiveData<Resource<ChargepriceChargepointMeta>>().apply {
|
||||
listOf(chargePriceMeta, chargepoint).forEach {
|
||||
addSource(it) {
|
||||
val cpMeta = chargePriceMeta.value
|
||||
val chargepoint = chargepoint.value
|
||||
if (cpMeta == null || chargepoint == null) {
|
||||
value = null
|
||||
} else if (cpMeta.status == Status.ERROR) {
|
||||
value = Resource.error(cpMeta.message, null)
|
||||
} else if (cpMeta.status == Status.LOADING) {
|
||||
value = Resource.loading(null)
|
||||
} else {
|
||||
val result = cpMeta.data!!.chargePoints.filter {
|
||||
it.plug == getChargepricePlugType(
|
||||
chargepoint
|
||||
) && it.power == chargepoint.power
|
||||
}.elementAtOrNull(0)
|
||||
value = if (result != null) {
|
||||
Resource.success(result)
|
||||
} else {
|
||||
Resource.error("matching chargepoint not found", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadPricesJob: Job? = null
|
||||
fun loadPrices() {
|
||||
chargePrices.value = Resource.loading(null)
|
||||
chargePriceMeta.value = Resource.loading(null)
|
||||
val charger = charger.value
|
||||
val car = vehicle.value
|
||||
val compatibleConnectors = vehicleCompatibleConnectors.value
|
||||
val myTariffs = myTariffs.value
|
||||
val myTariffsAll = myTariffsAll.value
|
||||
if (charger == null || car == null || compatibleConnectors == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
|
||||
chargePrices.value = Resource.error(null, null)
|
||||
return
|
||||
}
|
||||
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, compatibleConnectors)
|
||||
if (cpStation.chargePoints.isEmpty()) {
|
||||
// no compatible connectors
|
||||
chargePrices.value = Resource.success(emptyList())
|
||||
chargePriceMeta.value = Resource.success(ChargepriceMeta(emptyList()))
|
||||
return
|
||||
}
|
||||
|
||||
loadPricesJob?.cancel()
|
||||
loadPricesJob = viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getChargePrices(
|
||||
ChargepriceRequest(
|
||||
dataAdapter = ChargepriceApi.getDataAdapter(charger),
|
||||
station = cpStation,
|
||||
vehicle = car,
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange.value!!.map { it.toDouble() },
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad,
|
||||
showPriceUnavailable = true
|
||||
),
|
||||
relationships = if (!myTariffsAll) {
|
||||
Relationships(
|
||||
"tariffs" to Relationship.ToMany(
|
||||
(myTariffs ?: emptySet()).map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
id = it
|
||||
)
|
||||
},
|
||||
meta = Meta.from(
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
|
||||
ChargepriceApi.moshi
|
||||
)
|
||||
)
|
||||
)
|
||||
} else null
|
||||
), ChargepriceApi.getChargepriceLanguage()
|
||||
)
|
||||
|
||||
val meta = result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||
chargePrices.value = Resource.success(result.data)
|
||||
chargePriceMeta.value = Resource.success(meta)
|
||||
} catch (e: IOException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetBatteryRangeToDefault() {
|
||||
batteryRange.value = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
}
|
||||
}
|
||||
@@ -6,38 +6,16 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class SettingsViewModel(
|
||||
application: Application,
|
||||
chargepriceApiKey: String,
|
||||
chargepriceApiUrl: String
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
private val api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
|
||||
private val db = AppDatabase.getInstance(application)
|
||||
private val prefs = PreferenceDataSource(application)
|
||||
|
||||
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
loadVehicles()
|
||||
}
|
||||
}
|
||||
|
||||
val tariffs: MutableLiveData<Resource<List<ChargepriceTariff>>> by lazy {
|
||||
MutableLiveData<Resource<List<ChargepriceTariff>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
loadTariffs()
|
||||
}
|
||||
}
|
||||
|
||||
val chargerCacheCount: LiveData<Long> by lazy {
|
||||
db.chargeLocationsDao().getCount()
|
||||
}
|
||||
@@ -52,32 +30,6 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVehicles() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getVehicles()
|
||||
vehicles.value = Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTariffs() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getTariffs()
|
||||
tariffs.value = Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
tariffs.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
tariffs.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRecentSearchResults() {
|
||||
viewModelScope.launch {
|
||||
db.recentAutocompletePlaceDao().deleteAll()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="15.811624dp" android:viewportHeight="131.5"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="199.6" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M197.544,65.685l-9.2,-4.8l8,-4.2c2.7,-1.4 2.7,-3.8 0,-5.2l-8.6,-4.5l8.6,-4.5c2.7,-1.4 2.7,-3.8 0,-5.2l-68.9,-36.1c-2.7,-1.4 -7.2,-1.4 -9.9,0l-115.5,59.7c-2.7,1.4 -2.7,3.7 0,5.1l8.8,4.5l-8.8,4.6c-2.7,1.4 -2.7,3.7 0,5.1l9.4,4.8l-8.2,4.3c-2.7,1.4 -2.7,3.7 0,5.1l70.4,36.2c2.7,1.4 7.2,1.4 9.9,0l114,-59.6C200.344,69.385 200.344,67.085 197.544,65.685L197.544,65.685zM123.144,18.785L105.844,38.685c-0.9,1 -0.6,2.3 0.6,2.9l13.7,7.1c1.2,0.6 1.2,1.6 0,2.2l-43.1,22.3c-1.2,0.6 -1.4,0.3 -0.6,-0.7l17.3,-19.9c0.9,-1 0.6,-2.3 -0.6,-2.9l-13.7,-7.1c-1.2,-0.6 -1.2,-1.6 0,-2.2l43.1,-22.3C123.744,17.485 123.944,17.785 123.144,18.785L123.144,18.785z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector android:height="20dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="20dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector android:height="20dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="20dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector android:height="16dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="16dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector android:height="16dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="16dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_compass.xml
Normal file
12
app/src/main/res/drawable/ic_compass.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,10.9c-0.61,0 -1.1,0.49 -1.1,1.1s0.49,1.1 1.1,1.1c0.61,0 1.1,-0.49 1.1,-1.1s-0.49,-1.1 -1.1,-1.1zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM14.19,14.19L6,18l3.81,-8.19L18,6l-3.81,8.19z" />
|
||||
|
||||
</vector>
|
||||
@@ -1,149 +0,0 @@
|
||||
<vector android:height="26dp"
|
||||
android:viewportHeight="257.0819"
|
||||
android:viewportWidth="1289.0747"
|
||||
android:width="130.4dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m339.23,124.6q14.22,0 23.58,4.5 9.54,4.5 9.54,11.52 0,3.06 -1.98,5.58 -1.98,2.34 -5.04,2.34 -2.34,0 -3.78,-0.72 -1.26,-0.72 -3.6,-2.34 -1.08,-1.08 -3.42,-2.52 -2.16,-1.08 -6.12,-1.8 -3.96,-0.72 -7.2,-0.72 -9.36,0 -16.56,4.32 -7.2,4.32 -11.16,12.06 -3.96,7.56 -3.96,16.92 0,9.54 3.78,17.1 3.96,7.56 10.98,11.88 7.02,4.32 16.02,4.32 9.36,0 15.12,-2.88 1.26,-0.72 3.42,-2.34 1.8,-1.44 3.06,-2.16 1.44,-0.72 3.42,-0.72 3.6,0 5.58,2.34 2.16,2.16 2.16,5.76 0,3.78 -4.86,7.56 -4.68,3.6 -12.78,5.94 -7.92,2.34 -17.1,2.34 -13.68,0 -24.12,-6.3 -10.44,-6.48 -16.2,-17.64 -5.58,-11.34 -5.58,-25.2 0,-13.86 5.94,-25.02 5.94,-11.34 16.56,-17.64 10.62,-6.48 24.3,-6.48z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m437.63,125.14q31.68,0 31.68,39.24l0,48.06q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.52 -2.34,-6.12l0,-48.06q0,-23.4 -19.8,-23.4 -10.62,0 -17.64,6.84 -7.02,6.66 -7.02,16.56l0,48.06q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-115.92q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,45.54q4.5,-7.02 12.6,-11.88 8.1,-5.04 17.28,-5.04z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m571.21,125.5q3.78,0 6.12,2.52 2.52,2.34 2.52,6.3l0,78.12q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-4.68q-4.68,6.3 -12.78,10.8 -8.1,4.32 -17.46,4.32 -12.24,0 -22.32,-6.3 -9.9,-6.3 -15.66,-17.46 -5.58,-11.34 -5.58,-25.38 0,-14.04 5.58,-25.2 5.76,-11.34 15.66,-17.64 9.9,-6.3 21.78,-6.3 9.54,0 17.64,3.96 8.28,3.96 13.14,10.08l0,-4.32q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52zM534.49,207.04q8.46,0 14.94,-4.32 6.66,-4.32 10.26,-11.88 3.78,-7.56 3.78,-17.1 0,-9.36 -3.78,-16.92 -3.6,-7.56 -10.26,-11.88 -6.48,-4.5 -14.94,-4.5 -8.46,0 -15.12,4.32 -6.48,4.32 -10.26,11.88 -3.6,7.56 -3.6,17.1 0,9.54 3.6,17.1 3.78,7.56 10.26,11.88 6.66,4.32 15.12,4.32z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m652.08,124.6q4.68,0 8.1,2.52 3.42,2.34 3.42,5.94 0,4.32 -2.34,6.66 -2.16,2.16 -5.4,2.16 -1.62,0 -4.86,-1.08 -3.78,-1.26 -5.94,-1.26 -5.58,0 -10.98,3.96 -5.22,3.78 -8.64,10.62 -3.24,6.66 -3.24,14.94l0,43.38q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-77.04q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,9.18q3.96,-8.82 11.88,-14.22 7.92,-5.58 18,-5.76z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m755.31,125.5q3.78,0 6.12,2.52 2.52,2.34 2.52,6.3l0,79.2q0,14.58 -6.3,24.3 -6.12,9.9 -16.74,14.58 -10.62,4.68 -23.94,4.68 -7.2,0 -16.92,-2.52 -9.54,-2.52 -12.24,-5.22 -5.58,-2.88 -5.58,-7.2 0,-1.08 0.72,-2.88 1.98,-4.5 6.66,-4.5 2.34,0 5.04,1.08 14.4,5.58 22.5,5.58 14.4,0 21.96,-7.02 7.74,-6.84 7.74,-18.9l0,-9.72q-3.78,7.02 -12.78,12.06 -8.82,5.04 -18.72,5.04 -12.42,0 -22.68,-6.3 -10.26,-6.3 -16.2,-17.46 -5.76,-11.34 -5.76,-25.38 0,-14.04 5.76,-25.2 5.94,-11.34 16.02,-17.64 10.26,-6.3 22.5,-6.3 9.9,0 18.36,4.5 8.64,4.5 13.5,10.98l0,-5.76q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52zM717.33,207.04q8.82,0 15.66,-4.14 6.84,-4.32 10.62,-11.88 3.96,-7.74 3.96,-17.28 0,-9.54 -3.96,-17.1 -3.78,-7.56 -10.62,-11.88 -6.84,-4.32 -15.66,-4.32 -8.64,0 -15.48,4.32 -6.84,4.32 -10.8,12.06 -3.78,7.56 -3.78,16.92 0,9.36 3.78,17.1 3.96,7.56 10.8,11.88 6.84,4.32 15.48,4.32z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m868.61,170.32q-0.18,3.24 -2.7,5.58 -2.52,2.16 -5.94,2.16l-63.36,0q1.26,13.14 9.9,21.06 8.82,7.92 21.42,7.92 8.64,0 14.04,-2.52 5.4,-2.52 9.54,-6.48 2.7,-1.62 5.22,-1.62 3.06,0 5.04,2.16 2.16,2.16 2.16,5.04 0,3.78 -3.6,6.84 -5.22,5.22 -13.86,8.82 -8.64,3.6 -17.64,3.6 -14.58,0 -25.74,-6.12 -10.98,-6.12 -17.1,-17.1 -5.94,-10.98 -5.94,-24.84 0,-15.12 6.12,-26.46 6.3,-11.52 16.38,-17.64 10.26,-6.12 21.96,-6.12 11.52,0 21.6,5.94 10.08,5.94 16.2,16.38 6.12,10.44 6.3,23.4zM824.51,140.44q-10.08,0 -17.46,5.76 -7.38,5.58 -9.72,17.46l53.1,0l0,-1.44q-0.9,-9.54 -8.64,-15.66 -7.56,-6.12 -17.28,-6.12z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m935.8,125.14q12.24,0 22.14,6.3 9.9,6.12 15.48,17.28 5.76,11.16 5.76,25.2 0,14.04 -5.76,25.2 -5.58,10.98 -15.48,17.28 -9.9,6.3 -21.78,6.3 -9.36,0 -17.46,-4.14 -8.1,-4.14 -13.14,-10.08l0,39.96q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.34 -2.34,-6.12l0,-113.22q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.3l0,5.22q4.32,-6.3 12.6,-10.8 8.28,-4.5 17.64,-4.5zM933.82,206.86q8.28,0 14.94,-4.32 6.66,-4.32 10.26,-11.7 3.78,-7.56 3.78,-16.92 0,-9.36 -3.78,-16.74 -3.6,-7.56 -10.26,-11.88 -6.66,-4.32 -14.94,-4.32 -8.46,0 -15.12,4.32 -6.66,4.14 -10.44,11.7 -3.6,7.56 -3.6,16.92 0,9.36 3.6,16.92 3.78,7.56 10.44,11.88 6.66,4.14 15.12,4.14z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m1045.83,124.6q4.68,0 8.1,2.52 3.42,2.34 3.42,5.94 0,4.32 -2.34,6.66 -2.16,2.16 -5.4,2.16 -1.62,0 -4.86,-1.08 -3.78,-1.26 -5.94,-1.26 -5.58,0 -10.98,3.96 -5.22,3.78 -8.64,10.62 -3.24,6.66 -3.24,14.94l0,43.38q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-77.04q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,9.18q3.96,-8.82 11.88,-14.22 7.92,-5.58 18,-5.76z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m1089.23,212.44q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.52 -2.34,-6.12l0,-77.94q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12zM1080.59,113.98q-5.22,0 -7.56,-1.8 -2.16,-1.98 -2.16,-6.12l0,-2.88q0,-4.32 2.34,-6.12 2.52,-1.8 7.56,-1.8 5.04,0 7.2,1.98 2.34,1.8 2.34,5.94l0,2.88q0,4.32 -2.34,6.12 -2.34,1.8 -7.38,1.8z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m1154.85,124.6q14.22,0 23.58,4.5 9.54,4.5 9.54,11.52 0,3.06 -1.98,5.58 -1.98,2.34 -5.04,2.34 -2.34,0 -3.78,-0.72 -1.26,-0.72 -3.6,-2.34 -1.08,-1.08 -3.42,-2.52 -2.16,-1.08 -6.12,-1.8 -3.96,-0.72 -7.2,-0.72 -9.36,0 -16.56,4.32 -7.2,4.32 -11.16,12.06 -3.96,7.56 -3.96,16.92 0,9.54 3.78,17.1 3.96,7.56 10.98,11.88 7.02,4.32 16.02,4.32 9.36,0 15.12,-2.88 1.26,-0.72 3.42,-2.34 1.8,-1.44 3.06,-2.16 1.44,-0.72 3.42,-0.72 3.6,0 5.58,2.34 2.16,2.16 2.16,5.76 0,3.78 -4.86,7.56 -4.68,3.6 -12.78,5.94 -7.92,2.34 -17.1,2.34 -13.68,0 -24.12,-6.3 -10.44,-6.48 -16.2,-17.64 -5.58,-11.34 -5.58,-25.2 0,-13.86 5.94,-25.02 5.94,-11.34 16.56,-17.64 10.62,-6.48 24.3,-6.48z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m1289.07,170.32q-0.18,3.24 -2.7,5.58 -2.52,2.16 -5.94,2.16l-63.36,0q1.26,13.14 9.9,21.06 8.82,7.92 21.42,7.92 8.64,0 14.04,-2.52 5.4,-2.52 9.54,-6.48 2.7,-1.62 5.22,-1.62 3.06,0 5.04,2.16 2.16,2.16 2.16,5.04 0,3.78 -3.6,6.84 -5.22,5.22 -13.86,8.82 -8.64,3.6 -17.64,3.6 -14.58,0 -25.74,-6.12 -10.98,-6.12 -17.1,-17.1 -5.94,-10.98 -5.94,-24.84 0,-15.12 6.12,-26.46 6.3,-11.52 16.38,-17.64 10.26,-6.12 21.96,-6.12 11.52,0 21.6,5.94 10.08,5.94 16.2,16.38 6.12,10.44 6.3,23.4zM1244.97,140.44q-10.08,0 -17.46,5.76 -7.38,5.58 -9.72,17.46l53.1,0l0,-1.44q-0.9,-9.54 -8.64,-15.66 -7.56,-6.12 -17.28,-6.12z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m321.33,1q5.1,0 9.7,3.1 4.6,3 7.4,8.2 2.8,5.1 2.8,11.2 0,6 -2.8,11.2 -2.8,5.2 -7.4,8.3 -4.6,3 -9.7,3l-17.4,0l0,18.9q0,2.7 -1.6,4.4 -1.6,1.7 -4.2,1.7 -2.5,0 -4.1,-1.7 -1.6,-1.8 -1.6,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8zM321.33,34.6q1.9,0 3.7,-1.6 1.9,-1.6 3,-4.1 1.2,-2.6 1.2,-5.4 0,-2.8 -1.2,-5.3 -1.1,-2.6 -3,-4.1 -1.8,-1.6 -3.7,-1.6l-17.4,0l0,22.1z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m417.18,36q0,9.9 -4.4,18.2 -4.4,8.2 -12.2,13 -7.7,4.8 -17.4,4.8 -9.7,0 -17.5,-4.8 -7.7,-4.8 -12.1,-13 -4.3,-8.3 -4.3,-18.2 0,-9.9 4.3,-18.1 4.4,-8.3 12.1,-13.1 7.8,-4.8 17.5,-4.8 9.7,0 17.4,4.8 7.8,4.8 12.2,13.1 4.4,8.2 4.4,18.1zM404.18,36q0,-6.7 -2.7,-12.1 -2.7,-5.5 -7.5,-8.7 -4.8,-3.2 -10.8,-3.2 -6.1,0 -10.9,3.2 -4.7,3.1 -7.4,8.6 -2.6,5.5 -2.6,12.2 0,6.7 2.6,12.2 2.7,5.5 7.4,8.7 4.8,3.1 10.9,3.1 6,0 10.8,-3.2 4.8,-3.2 7.5,-8.6 2.7,-5.5 2.7,-12.2z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m506.87,0.7q2.4,0 4.4,1.9 2.1,1.8 2.1,4.6 0,0.9 -0.3,2l-19.7,58q-0.6,1.7 -2.1,2.7 -1.5,1 -3.3,1.1 -1.8,0 -3.4,-1 -1.6,-1 -2.5,-2.9l-14.2,-32.3 -14.3,32.3q-0.9,1.9 -2.5,2.9 -1.6,1 -3.4,1 -1.8,-0.1 -3.3,-1.1 -1.5,-1 -2.1,-2.7l-19.7,-58q-0.3,-1.1 -0.3,-2 0,-2.8 2,-4.6 2.1,-1.9 4.6,-1.9 2,0 3.6,1.1 1.6,1 2.2,2.8l14.9,45.2 13,-31.2q0.8,-1.8 2.3,-2.8 1.5,-1.1 3.4,-1 1.9,-0.1 3.3,1 1.5,1 2.3,2.8l12.3,30.9 14.8,-44.9q0.6,-1.8 2.2,-2.8 1.7,-1.1 3.7,-1.1z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m562.11,59.5q2.6,0 4.3,1.8 1.8,1.7 1.8,4 0,2.5 -1.8,4.1 -1.7,1.6 -4.3,1.6l-33.5,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l33.5,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-27.1,0l0,17l22.6,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-22.6,0l0,18.5z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m634.63,61.2q1.3,0.8 2,2.1 0.8,1.3 0.8,2.7 0,1.8 -1.2,3.3 -1.5,1.8 -4.6,1.8 -2.4,0 -4.4,-1.1 -7.2,-4.1 -7.2,-16.7 0,-3.6 -2.4,-5.7 -2.3,-2.1 -6.7,-2.1L592.23,45.5l0,19.4q0,2.7 -1.5,4.4 -1.4,1.7 -3.8,1.7 -2.9,0 -5.1,-1.7 -2.1,-1.8 -2.1,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l28.8,0q5.2,0 9.8,2.8 4.6,2.8 7.3,7.7 2.8,4.9 2.8,11 0,5 -2.7,9.8 -2.7,4.7 -7,7.5 6.3,4.4 6.9,11.8 0.3,1.6 0.3,3.1 0.4,3.1 0.8,4.5 0.4,1.3 1.8,2zM614.13,35.2q1.8,0 3.5,-1.7 1.7,-1.7 2.8,-4.5 1.1,-2.9 1.1,-6.2 0,-2.8 -1.1,-5.1 -1.1,-2.4 -2.8,-3.8 -1.7,-1.4 -3.5,-1.4l-21.9,0l0,22.7z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m688.08,59.5q2.6,0 4.3,1.8 1.8,1.7 1.8,4 0,2.5 -1.8,4.1 -1.7,1.6 -4.3,1.6l-33.5,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l33.5,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-27.1,0l0,17l22.6,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-22.6,0l0,18.5z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m735.71,1q9.4,0 16.1,4.7 6.8,4.6 10.3,12.6 3.6,7.9 3.6,17.7 0,9.8 -3.6,17.8 -3.5,7.9 -10.3,12.6 -6.7,4.6 -16.1,4.6l-23.9,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8zM734.71,59.5q9,0 13.5,-6.6 4.5,-6.7 4.5,-16.9 0,-10.2 -4.6,-16.8 -4.5,-6.7 -13.4,-6.7l-16.5,0l0,47z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m847.12,32.2q5.3,2.1 8.6,6.4 3.4,4.3 3.4,11.1 0,11.9 -6.8,16.6 -6.8,4.7 -16.2,4.7l-24.9,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l25.2,0q19,0 19,17.8 0,4.5 -2.2,8 -2.1,3.4 -6.1,5.4zM842.42,21q0,-4.1 -2.1,-6.1 -2,-2.1 -5.7,-2.1l-16.5,0l0,15.6l16.8,0q3,0 5.2,-2 2.3,-2 2.3,-5.4zM836.12,59.5q4.7,0 7.3,-2.5 2.7,-2.5 2.7,-7.3 0,-5.9 -3.1,-7.7 -3.1,-1.8 -7.6,-1.8l-17.3,0l0,19.3z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m918.81,6.9q0,2 -1.1,3.7l-20.9,29.9l0,24.4q0,2.6 -1.7,4.4 -1.7,1.7 -4.1,1.7 -2.5,0 -4.3,-1.7 -1.7,-1.8 -1.7,-4.4l0,-25.8l-20.8,-27.6q-1.8,-2.4 -1.8,-4.7 0,-2.6 2,-4.3 2.1,-1.8 4.4,-1.8 2.8,0 4.9,2.8l17.6,24.3 16.5,-24.1q2.1,-3 5,-3 2.4,0 4.2,1.8 1.8,1.8 1.8,4.4z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#000016"
|
||||
android:pathData="m246.94,173.65 l-11.5,-6.03 10.02,-5.24c3.41,-1.78 3.42,-4.71 0.01,-6.5l-10.76,-5.65 10.76,-5.62c3.41,-1.79 3.42,-4.71 0.01,-6.5L159.4,92.94c-3.41,-1.79 -9,-1.81 -12.42,-0.04L2.56,167.49c-3.42,1.77 -3.42,4.65 0.01,6.41l11.02,5.66 -11.02,5.69c-3.42,1.77 -3.42,4.65 0.01,6.41l11.76,6.04 -10.29,5.31c-3.42,1.77 -3.42,4.65 0.01,6.41l88.01,45.22c3.43,1.76 9.02,1.74 12.43,-0.04l142.46,-74.47c3.41,-1.78 3.41,-4.71 0,-6.5zM153.91,115.02 L132.31,139.92c-1.08,1.25 -0.76,2.88 0.7,3.64l17.18,8.83c1.47,0.75 1.47,1.99 0,2.75l-53.92,27.85c-1.47,0.76 -1.78,0.36 -0.7,-0.89l21.59,-24.9c1.08,-1.25 0.77,-2.88 -0.7,-3.64l-17.18,-8.83c-1.47,-0.75 -1.47,-1.99 0,-2.75l53.92,-27.85c1.47,-0.76 1.78,-0.36 0.7,0.89z" />
|
||||
</vector>
|
||||
@@ -61,4 +61,22 @@
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/data_source_openstreetmap_desc" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbNobil"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/data_source_nobil"
|
||||
android:textColor="#69bf9c"
|
||||
app:buttonTint="#69bf9c"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView30"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-8dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/data_source_nobil_desc" />
|
||||
|
||||
</RadioGroup>
|
||||
@@ -543,9 +543,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
android:transitionName="@string/shared_element_chargeprice"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}"
|
||||
app:icon="@drawable/ic_chargeprice" />
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnChargerWebsite"
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="ChargepriceViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:transitionName="@string/shared_element_chargeprice">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:actionBarSize">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgChargepriceLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/powered_by_chargeprice"
|
||||
android:focusable="true"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:layout_gravity="right"
|
||||
app:srcCompat="@drawable/ic_powered_by_chargeprice"
|
||||
app:tint="?android:textColorPrimary"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/charge_prices_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/fragment_chargeprice_preview" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar5"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="280dp"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_tariffs_found"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS && vm.chargePricesForChargepoint.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_compatible_connectors"
|
||||
app:goneUnless="@{vm.noCompatibleConnectors}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_select_car_first"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/settings"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -1,122 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="ChargepriceViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_select_connector"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/vehicle_selection" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/connectors_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:data="@{vm.charger.chargepointsMerged}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector_button"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvChargeFromTo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?selectableItemBackground"
|
||||
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="Charge from 20% to 80%" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toStartOf="@+id/tvChargeFromTo"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvChargeFromTo"
|
||||
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVehicleHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_vehicle"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/vehicle_selection"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvVehicleHeader"
|
||||
app:data="@{vm.vehicles.data}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
android:orientation="horizontal"
|
||||
tools:listitem="@layout/item_chargeprice_vehicle_chip" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/battery_range"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView4"
|
||||
app:values="@={vm.batteryRange}" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/fragment_chargeprice_header" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
</LinearLayout>
|
||||
@@ -46,6 +46,7 @@
|
||||
android:id="@+id/favs_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
app:data="@{vm.listData}" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
android:id="@+id/filters_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
app:data="@{vm.filtersWithValue}"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_filter_boolean" />
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
android:id="@+id/filter_profiles_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
app:data="@{vm.filterProfiles}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
@@ -247,6 +247,15 @@
|
||||
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
|
||||
android:theme="@style/NoElevationOverlay" />
|
||||
|
||||
<View
|
||||
android:id="@+id/navBarScrim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:background="?android:colorBackground"
|
||||
android:layout_gravity="bottom"
|
||||
app:invisibleUnless="@{vm.bottomSheetState == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED}"
|
||||
tools:visibility="invisible" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/layers_sheet"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargePrice" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<import type="java.util.Set" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargePrice" />
|
||||
|
||||
<variable
|
||||
name="meta"
|
||||
type="ChargepriceChargepointMeta" />
|
||||
|
||||
<variable
|
||||
name="myTariffs"
|
||||
type="Set<String>" />
|
||||
|
||||
<variable
|
||||
name="myTariffsAll"
|
||||
type="Boolean" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll && myTariffs.contains(item.tariffId), item.branding.backgroundColor)}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtTariff"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.tariffName}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="CheapCharge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtProvider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.provider}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/rvTags"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
|
||||
tools:text="Cheap Charging Co." />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvTags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{item.tags}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/item_chargeprice_tag" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtProviderCustomerTariff"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/chargeprice_provider_customer_tariff"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{item.providerCustomerTariff}"
|
||||
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtMonthlyFee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.formatMonthlyFees(context)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
|
||||
tools:text="Base fee 1 €/month" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtPrice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(@string/charge_price_format, item.chargepointPrices.get(0).price, BindingAdaptersKt.currency(item.currency)) : @string/chargeprice_price_not_available}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtAveragePrice"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="1,50 €" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtAveragePrice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(item.chargepointPrices.get(0).priceDistribution.isOnlyKwh ? @string/charge_price_kwh_format : @string/charge_price_average_format, item.chargepointPrices.get(0).price / meta.energy, BindingAdaptersKt.currency(item.currency)) : item.chargepointPrices.get(0).noPriceReason}"
|
||||
app:goneUnless="@{item.chargepointPrices.get(0).price > 0 || item.chargepointPrices.get(0).price == null}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintBottom_toTopOf="@id/txtPriceDetails"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtPrice"
|
||||
tools:text="⌀ 0,29 €/kWh" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtPriceDetails"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{item.chargepointPrices.get(0).formatDistribution(context)}"
|
||||
app:goneUnless="@{!item.chargepointPrices.get(0).formatDistribution(context).empty}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtAveragePrice"
|
||||
tools:text="pro kWh + ab 4h Blockiergeb." />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.65" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivLogo"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:invisibleUnless="@{item.branding.logoUrl != null}"
|
||||
app:imageUrl="@{item.branding.logoUrl}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceTag" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargepriceTag" />
|
||||
</data>
|
||||
|
||||
<net.vonforst.evmap.ui.BalancedBreakingTextView
|
||||
android:id="@+id/rvTags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@drawable/rounded_rect_16dp"
|
||||
android:maxLines="3"
|
||||
android:text="@{item.text}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
||||
android:gravity="center_vertical"
|
||||
android:drawablePadding="4dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:drawableTint="?android:textColorPrimary"
|
||||
android:breakStrategy="balanced"
|
||||
app:chargepriceTagColor="@{item.kind}"
|
||||
app:chargepriceTagIcon="@{item.kind}"
|
||||
tools:backgroundTint="@color/chargeprice_alert"
|
||||
tools:drawableLeft="@drawable/ic_chargeprice_alert"
|
||||
tools:text="Only for drivers of blue cars" />
|
||||
</layout>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceCar" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargepriceCar" />
|
||||
|
||||
<variable
|
||||
name="selectedItem"
|
||||
type="ChargepriceCar" />
|
||||
</data>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:text="@{item.brand + ' ' + item.name}"
|
||||
android:checked="@{item == selectedItem}"
|
||||
tools:text="Tesla Model 2" />
|
||||
</layout>
|
||||
@@ -27,9 +27,6 @@
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_chargepriceFragment"
|
||||
app:destination="@id/chargeprice" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_opensource_donations"
|
||||
app:destination="@id/opensource_donations" />
|
||||
@@ -88,16 +85,6 @@
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settings_chargeprice"
|
||||
android:name="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
|
||||
android:label="@string/settings_chargeprice"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/settings_android_auto"
|
||||
android:name="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
|
||||
android:label="@string/settings_android_auto"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/settings_developer"
|
||||
android:name="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
|
||||
@@ -126,25 +113,6 @@
|
||||
android:name="net.vonforst.evmap.fragment.FilterProfilesFragment"
|
||||
android:label="@string/menu_manage_filter_profiles"
|
||||
tools:layout="@layout/fragment_filter_profiles" />
|
||||
<fragment
|
||||
android:id="@+id/chargeprice"
|
||||
android:name="net.vonforst.evmap.fragment.ChargepriceFragment"
|
||||
android:label="@string/chargeprice_title"
|
||||
tools:layout="@layout/fragment_chargeprice">
|
||||
<action
|
||||
android:id="@+id/action_chargeprice_to_chargepriceSettingsFragment"
|
||||
app:destination="@id/settings_chargeprice"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_chargeprice_to_donateFragment"
|
||||
app:destination="@id/donate" />
|
||||
<argument
|
||||
android:name="charger"
|
||||
app:argType="net.vonforst.evmap.model.ChargeLocation" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/donate"
|
||||
android:name="net.vonforst.evmap.fragment.DonateFragment"
|
||||
|
||||
@@ -377,4 +377,20 @@
|
||||
<string name="referral_link">https://ev-map.app/referrals/</string>
|
||||
<string name="mastodon">Mastodon</string>
|
||||
<string name="tff_forum">Vlákno na fóru TFF-Forum.de</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_openstreetmap_desc">Experimentální podpora v EVMap, nejsou dostupné všechny funkce.</string>
|
||||
<string name="downloading_chargers_percent">Stahování… %.0f%%</string>
|
||||
<string name="plug_type_2_tethered">Provázaný kabel typ 2</string>
|
||||
<string name="no_email_app_found">Nejprve si nainstalujte e-mailovou aplikaci</string>
|
||||
<string name="filter_accessibility">Přístupnost nabíječky</string>
|
||||
<string name="data_source_nobil">NOBIL</string>
|
||||
<string name="data_source_nobil_desc"><![CDATA[Otevřená data poskytovaná vládou a komunitou ve Švédsku a Norsku.]]></string>
|
||||
<string name="accessibility_public">Veřejné</string>
|
||||
<string name="accessibility_visitors">Návštěvníci</string>
|
||||
<string name="accessibility_employees">Zaměstnanci</string>
|
||||
<string name="accessibility_by_appointment">Po domluvě</string>
|
||||
<string name="accessibility_residents">Obyvatelé</string>
|
||||
<string name="chargeprice_removal_2025_dialog_title">Omlouváme se!</string>
|
||||
<string name="chargeprice_removal_2025_dialog_detail">Náklady na přístup k údajům ze služby Chargeprice prudce vzrostly a nelze je pokrýt z darů, takže EVMap již nemůže tyto údaje přímo zobrazovat. Prozatím se otevře webová stránka Chargeprice. Alternativní řešení se vyvíjí, ale bude to nějakou dobu trvat a zpočátku bude mít omezené funkce. Děkujeme za trpělivost a podporu!</string>
|
||||
<string name="auto_use_new_map_screen">Nová obrazovka mapy (beta)</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user