Compare commits

..

2 Commits

Author SHA1 Message Date
johan12345
1a830fda5b add Jawg Maps sponsor logo 2024-05-08 21:39:29 +02:00
johan12345
3df83f2d56 Migrate Mapbox -> MapLibre
Use Jawg Maps for basemap, ArcGIS for satellite maps

fixes #141
refs #169, #197

hide traffic checkbox if traffic is not supported by map
2024-05-08 21:39:29 +02:00
136 changed files with 1496 additions and 3100 deletions

View File

View File

@@ -26,7 +26,7 @@ jobs:
cache: 'gradle'
- name: Copy apikeys.xml
run: cp _ci/apikeys-ci.xml app/src/main/res/values/apikeys.xml
run: cp .github/workflows/apikeys-ci.xml app/src/main/res/values/apikeys.xml
- name: Build app
run: ./gradlew assemble${{ matrix.buildvariant }}Debug --no-daemon
@@ -34,55 +34,3 @@ jobs:
run: ./gradlew test${{ matrix.buildvariant }}DebugUnitTest --no-daemon
- name: Run Android Lint
run: ./gradlew lint${{ matrix.buildvariant }}Debug --no-daemon
- name: Check licenses
run: ./gradlew exportLibraryDefinitions --no-daemon
apk_check:
name: Release APK checks (${{ matrix.buildvariant }})
runs-on: ubuntu-latest
strategy:
matrix:
buildvariant: [ FossNormal, FossAutomotive, GoogleNormal, GoogleAutomotive ]
steps:
- name: Install checksec
run: sudo apt install -y checksec
- name: Check out code
uses: actions/checkout@v4
- name: Set up Java environment
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Copy apikeys.xml
run: cp _ci/apikeys-ci.xml app/src/main/res/values/apikeys.xml
- name: Build app
run: ./gradlew assemble${{ matrix.buildvariant }}Release --no-daemon
- name: Unpack native libraries from APK
run: |
VARIANT_FILENAME=$(echo ${{ matrix.buildvariant }} | sed -E 's/([a-z])([A-Z])/\1-\2/g' | tr 'A-Z' 'a-z')
VARIANT_FOLDER=$(echo ${{ matrix.buildvariant }} | sed -E 's/^([A-Z])/\L\1/')
APK_FILE="app/build/outputs/apk/$VARIANT_FOLDER/release/app-$VARIANT_FILENAME-release-unsigned.apk"
unzip $APK_FILE "lib/*"
- name: Run checksec on native libraries
run: |
checksec --output=json --dir=lib > checksec_output.json
jq --argjson exceptions '[
"lib/armeabi-v7a/libc++_shared.so",
"lib/x86/libc++_shared.so"
]' '
to_entries
| map(select(.value.fortify_source == "no" and (.key as $lib | $exceptions | index($lib) | not)))
| if length > 0 then
error("The following libraries do not have fortify enabled (and are not in the exception list): " + (map(.key) | join(", ")))
else
"All libraries have fortify enabled or are in the exception list."
end
' checksec_output.json

3
.gitignore vendored
View File

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

View File

@@ -43,13 +43,13 @@ EVMap uses and put them into the app in the form of a resource file called `apik
`app/src/main/res/values`. You can find more information on which API keys are necessary for which
features and how they can be obtained in our [documentation page](doc/api_keys.md).
There are four different build flavors, `googleNormal`, `fossNormal`, `googleAutomotive`, and
`fossAutomotive`.
There are three different build flavors, `googleNormal`, `fossNormal` and `googleAutomotive`.
- The `foss` variants only use OSM data for the base map and place search. They should run on most Android devices, even those without Google Play Services.
- The `foss` variants only use OSM data and should run on most Android devices, even without
Google Play Services.
- `fossNormal` is intended to run on smartphones and tablets, and also includes the Android
Auto app for use on the car display (however Android Auto may not work if the app is not
installed from Google Play, see https://github.com/ev-map/EVMap/issues/319).
Auto app for use on the car display (however for that to work, the Android Auto app is
necessary, which in turn does require Google Play Services).
- `fossAutomotive` can be installed directly on
[Android Automotive OS (AAOS)](https://source.android.com/docs/automotive/start/what_automotive)
headunits without Google services.
@@ -83,13 +83,6 @@ into new languages.
Sponsors
--------
Many users currently support the development EVMap with their donations. You can find more
information on the [Donate page](https://ev-map.app/donate/) on the EVMap website.
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="58"/></a><br>
Since May 2024, **JawgMaps** provides their OpenStreetMap vector map tiles service to EVMap for
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="58"/></a><br>
Since April 2021, **Chargeprice.app** provide their price comparison API at a greatly reduced
price for EVMap. This data is used in EVMap's price comparison feature.
Since mid 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.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

View File

@@ -6,37 +6,31 @@ plugins {
id("kotlin-android")
id("kotlin-parcelize")
id("kotlin-kapt")
id("com.google.devtools.ksp").version("2.0.21-1.0.28")
id("androidx.navigation.safeargs.kotlin")
id("com.mikepenz.aboutlibraries.plugin")
}
val supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro,cs"
android {
useLibrary("android.car")
defaultConfig {
applicationId = "net.vonforst.evmap"
compileSdk = 35
compileSdk = 34
minSdk = 21
targetSdk = 35
targetSdk = 34
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 256
versionName = "1.9.18"
versionCode = 212
versionName = "1.8.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
resourceConfigurations += supportedLocales.split(",")
buildConfigField("String", "supportedLocales", "\"$supportedLocales\"")
}
val isRunningOnCI = System.getenv("CI") == "true"
val isCIKeystoreAvailable = System.getenv("KEYSTORE_PASSWORD") != null
signingConfigs {
create("release") {
if (isRunningOnCI && isCIKeystoreAvailable) {
val isRunningOnCI = System.getenv("CI") == "true"
if (isRunningOnCI) {
// configure keystore
storeFile = file("../_ci/keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
@@ -53,16 +47,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = if (isRunningOnCI && !isCIKeystoreAvailable) {
null
} else {
signingConfigs.getByName("release")
}
}
create("releaseAutomotivePackageName") {
// Faurecia Aptoide requires the automotive variant to use a separate package name
initWith(getByName("release"))
applicationIdSuffix = ".automotive"
signingConfig = signingConfigs.getByName("release")
}
debug {
applicationIdSuffix = ".debug"
@@ -70,10 +55,6 @@ android {
}
}
sourceSets {
getByName("releaseAutomotivePackageName").setRoot("src/release")
}
flavorDimensions += listOf("dependencies", "automotive")
productFlavors {
create("foss") {
@@ -96,12 +77,18 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
}
buildFeatures {
@@ -113,9 +100,6 @@ android {
disable += listOf("NullSafeMutableLiveData")
warning += listOf("MissingTranslation")
}
androidResources {
generateLocaleConfig = true
}
testOptions {
unitTests {
@@ -234,22 +218,6 @@ android {
}
}
androidComponents {
beforeVariants { variantBuilder ->
if (variantBuilder.buildType == "releaseAutomotivePackageName"
&& !variantBuilder.productFlavors.containsAll(
listOf(
"automotive" to "automotive",
"dependencies" to "foss"
)
)
) {
// releaseAutomotivePackageName type is only needed for fossAutomotive
variantBuilder.enable = false
}
}
}
configurations {
create("googleNormalImplementation") {}
create("googleAutomotiveImplementation") {}
@@ -257,16 +225,11 @@ configurations {
aboutLibraries {
allowedLicenses = arrayOf(
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
"Apache-2.0", "mit", "BSD-2-Clause",
"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
"Google Maps Platform Terms of Service" // Google Maps SDK
)
excludeFields = arrayOf("generated")
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
}
@@ -281,29 +244,29 @@ 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.appcompat:appcompat:1.6.1")
implementation("androidx.core:core-ktx:1.12.0")
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.activity:activity-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
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("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.browser:browser:1.8.0")
implementation("androidx.browser:browser:1.7.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("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.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.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
implementation("com.squareup.moshi:moshi-adapters:1.15.0")
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
implementation("io.coil-kt:coil:2.6.0")
implementation("io.coil-kt:coil:2.4.0")
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
@@ -312,29 +275,22 @@ dependencies {
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")
// Android Auto
val carAppVersion = "1.7.0-rc01"
val carAppVersion = "1.4.0-rc02"
implementation("androidx.car.app:app:$carAppVersion")
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
// AnyMaps
val anyMapsVersion = "a3290b148d"
val anyMapsVersion = "c087b3e7c2"
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")
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") {
exclude("org.maplibre.gl", "android-sdk-geojson")
}
googleImplementation("com.google.android.gms:play-services-maps:18.2.0")
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion")
// Google Places
googleImplementation("com.google.android.libraries.places:places:3.5.0")
googleImplementation("com.google.android.libraries.places:places:3.3.0")
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
// Mapbox Geocoding
@@ -345,24 +301,22 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
// viewmodel library
val lifecycle_version = "2.8.1"
val lifecycle_version = "2.6.2"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// room library
val room_version = "2.7.1"
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
ksp("androidx.room:room-compiler:$room_version")
kapt("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")
implementation("com.github.anboralabs:spatia-room:0.2.9") {
exclude(group = "com.github.dalgarins", module = "android-spatialite")
}
// forked version with upgraded sqlite & libxml
// https://github.com/dalgarins/android-spatialite/pull/10
implementation("com.github.ev-map:android-spatialite:31495dcd81")
implementation("com.github.EV-map:android-spatialite:e5495c83ad") // version with minSdk increased to 21 & FORTIFY_SOURCE enabled
// billing library
val billing_version = "7.0.0"
val billing_version = "6.1.0"
googleImplementation("com.android.billingclient:billing:$billing_version")
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
@@ -376,12 +330,10 @@ dependencies {
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")
// testing
testImplementation("junit:junit:4.13.2")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0")
//noinspection GradleDependency
testImplementation("org.json:json:20080701")
testImplementation("org.robolectric:robolectric:4.11.1")
@@ -394,7 +346,7 @@ dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}

View File

@@ -1,904 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "5dbaaa5adf8cb9b6e8a8314bb7766447",
"entities": [
{
"tableName": "ChargeLocation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "coordinates",
"columnName": "coordinates",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "chargepoints",
"columnName": "chargepoints",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "network",
"columnName": "network",
"affinity": "TEXT"
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "editUrl",
"columnName": "editUrl",
"affinity": "TEXT"
},
{
"fieldPath": "verified",
"columnName": "verified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "barrierFree",
"columnName": "barrierFree",
"affinity": "INTEGER"
},
{
"fieldPath": "operator",
"columnName": "operator",
"affinity": "TEXT"
},
{
"fieldPath": "generalInformation",
"columnName": "generalInformation",
"affinity": "TEXT"
},
{
"fieldPath": "amenities",
"columnName": "amenities",
"affinity": "TEXT"
},
{
"fieldPath": "locationDescription",
"columnName": "locationDescription",
"affinity": "TEXT"
},
{
"fieldPath": "photos",
"columnName": "photos",
"affinity": "TEXT"
},
{
"fieldPath": "chargecards",
"columnName": "chargecards",
"affinity": "TEXT"
},
{
"fieldPath": "license",
"columnName": "license",
"affinity": "TEXT"
},
{
"fieldPath": "networkUrl",
"columnName": "networkUrl",
"affinity": "TEXT"
},
{
"fieldPath": "chargerUrl",
"columnName": "chargerUrl",
"affinity": "TEXT"
},
{
"fieldPath": "timeRetrieved",
"columnName": "timeRetrieved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "address.city",
"columnName": "city",
"affinity": "TEXT"
},
{
"fieldPath": "address.country",
"columnName": "country",
"affinity": "TEXT"
},
{
"fieldPath": "address.postcode",
"columnName": "postcode",
"affinity": "TEXT"
},
{
"fieldPath": "address.street",
"columnName": "street",
"affinity": "TEXT"
},
{
"fieldPath": "faultReport.created",
"columnName": "fault_report_created",
"affinity": "INTEGER"
},
{
"fieldPath": "faultReport.description",
"columnName": "fault_report_description",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.twentyfourSeven",
"columnName": "twentyfourSeven",
"affinity": "INTEGER"
},
{
"fieldPath": "openinghours.description",
"columnName": "description",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.monday.start",
"columnName": "mostart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.monday.end",
"columnName": "moend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.tuesday.start",
"columnName": "tustart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.tuesday.end",
"columnName": "tuend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.wednesday.start",
"columnName": "westart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.wednesday.end",
"columnName": "weend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.thursday.start",
"columnName": "thstart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.thursday.end",
"columnName": "thend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.friday.start",
"columnName": "frstart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.friday.end",
"columnName": "frend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.saturday.start",
"columnName": "sastart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.saturday.end",
"columnName": "saend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.sunday.start",
"columnName": "sustart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.sunday.end",
"columnName": "suend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.holiday.start",
"columnName": "hostart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.holiday.end",
"columnName": "hoend",
"affinity": "TEXT"
},
{
"fieldPath": "cost.freecharging",
"columnName": "freecharging",
"affinity": "INTEGER"
},
{
"fieldPath": "cost.freeparking",
"columnName": "freeparking",
"affinity": "INTEGER"
},
{
"fieldPath": "cost.descriptionShort",
"columnName": "descriptionShort",
"affinity": "TEXT"
},
{
"fieldPath": "cost.descriptionLong",
"columnName": "descriptionLong",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.country",
"columnName": "chargepricecountry",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.network",
"columnName": "chargepricenetwork",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.plugTypes",
"columnName": "chargepriceplugTypes",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
}
},
{
"tableName": "Favorite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "favoriteId",
"columnName": "favoriteId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargerId",
"columnName": "chargerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargerDataSource",
"columnName": "chargerDataSource",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"favoriteId"
]
},
"indices": [
{
"name": "index_Favorite_chargerId_chargerDataSource",
"unique": false,
"columnNames": [
"chargerId",
"chargerDataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
}
],
"foreignKeys": [
{
"table": "ChargeLocation",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"chargerId",
"chargerDataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "BooleanFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_BooleanFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "MultipleChoiceFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "values",
"columnName": "values",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "all",
"columnName": "all",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "SliderFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_SliderFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "FilterProfile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"dataSource",
"id"
]
},
"indices": [
{
"name": "index_FilterProfile_dataSource_name",
"unique": true,
"columnNames": [
"dataSource",
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
}
]
},
{
"tableName": "RecentAutocompletePlace",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primaryText",
"columnName": "primaryText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secondaryText",
"columnName": "secondaryText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "latLng",
"columnName": "latLng",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "viewport",
"columnName": "viewport",
"affinity": "TEXT"
},
{
"fieldPath": "types",
"columnName": "types",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
}
},
{
"tableName": "GEPlug",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "GENetwork",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "GEChargeCard",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMConnectionType",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formalName",
"columnName": "formalName",
"affinity": "TEXT"
},
{
"fieldPath": "discontinued",
"columnName": "discontinued",
"affinity": "INTEGER"
},
{
"fieldPath": "obsolete",
"columnName": "obsolete",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMCountry",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isoCode",
"columnName": "isoCode",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "continentCode",
"columnName": "continentCode",
"affinity": "TEXT"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMOperator",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "websiteUrl",
"columnName": "websiteUrl",
"affinity": "TEXT"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contactEmail",
"columnName": "contactEmail",
"affinity": "TEXT"
},
{
"fieldPath": "contactTelephone1",
"columnName": "contactTelephone1",
"affinity": "TEXT"
},
{
"fieldPath": "contactTelephone2",
"columnName": "contactTelephone2",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "SavedRegion",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "region",
"columnName": "region",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeRetrieved",
"columnName": "timeRetrieved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filters",
"columnName": "filters",
"affinity": "TEXT"
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_SavedRegion_filters_dataSource",
"unique": false,
"columnNames": [
"filters",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5dbaaa5adf8cb9b6e8a8314bb7766447')"
]
}
}

View File

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

View File

@@ -11,7 +11,6 @@ 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()
@@ -25,8 +24,6 @@ fun addDebugInterceptors(context: Context) {
client.addPlugin(DatabasesFlipperPlugin(context))
client.addPlugin(SharedPreferencesFlipperPlugin(context))
client.start()
Timber.plant(Timber.DebugTree())
}
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {

View File

@@ -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>

View File

@@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.transition.MaterialSharedAxis
@@ -42,7 +43,7 @@ class DonateFragment : DonateFragmentBase() {
)
binding.btnDonate.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link), binding.root)
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
}
setupReferrals(referrals)

View File

@@ -24,10 +24,6 @@
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<package android:name="com.google.android.projection.gearhead" />
<package android:name="com.google.android.apps.automotive.templates.host" />
@@ -46,7 +42,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:enableOnBackInvokedCallback="true">
android:localeConfig="@xml/locales_config">
<meta-data
android:name="com.mapbox.ACCESS_TOKEN"
@@ -352,8 +348,9 @@
android:exported="true"
android:foregroundServiceType="location">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.POI" />
<action
android:name="androidx.car.app.CarAppService"
android:category="androidx.car.app.category.POI" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View File

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

View File

@@ -3,8 +3,6 @@ package net.vonforst.evmap
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -13,6 +11,7 @@ import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen
@@ -45,11 +44,14 @@ const val EXTRA_DONATE = "donate"
class MapsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
interface FragmentCallback {
fun getRootView(): View
}
private var reenterState: Bundle? = null
private lateinit var navController: NavController
private lateinit var navHostFragment: NavHostFragment
lateinit var appBarConfiguration: AppBarConfiguration
var fragmentCallback: FragmentCallback? = null
private lateinit var prefs: PreferenceDataSource
override fun onCreate(savedInstanceState: Bundle?) {
@@ -58,7 +60,6 @@ class MapsActivity : AppCompatActivity(),
setContentView(R.layout.activity_maps)
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.map,
@@ -66,9 +67,9 @@ class MapsActivity : AppCompatActivity(),
R.id.about,
R.id.settings
),
drawerLayout
findViewById<DrawerLayout>(R.id.drawer_layout)
)
navHostFragment =
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
@@ -86,17 +87,6 @@ class MapsActivity : AppCompatActivity(),
checkPlayServices(this)
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
var deepLink: PendingIntent? = null
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.onboarding) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
} else {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
}
}
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// wait for splash screen animation to finish on first start
@@ -114,128 +104,137 @@ class MapsActivity : AppCompatActivity(),
}
})
}
} else if (intent?.scheme == "geo") {
val query = intent.data?.query?.split("=")?.get(1)
val coords = getLocationFromIntent(intent)
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else if (!prefs.privacyAccepted) {
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else {
navGraph.setStartDestination(R.id.map)
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
var deepLink: PendingIntent? = null
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
} else if (!query.isNullOrEmpty()) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "goingelectric") {
prefs.dataSource = "goingelectric"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_goingelectric)
),
Toast.LENGTH_LONG
).show()
if (intent?.scheme == "geo") {
val query = intent.data?.query?.split("=")?.get(1)
val coords = getLocationFromIntent(intent)
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
} else if (!query.isNullOrEmpty()) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host in listOf(
"openchargemap.org",
"map.openchargemap.io"
)
) {
val id = when (intent.data?.host) {
"openchargemap.org" -> intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
else -> null
}
if (id != null) {
if (prefs.dataSource != "openchargemap") {
prefs.dataSource = "openchargemap"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_openchargemap)
),
Toast.LENGTH_LONG
).show()
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "goingelectric") {
prefs.dataSource = "goingelectric"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_goingelectric)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent.scheme == "net.vonforst.evmap") {
intent.data?.let {
if (it.host == "find_charger") {
val lat = it.getQueryParameter("latitude")?.toDouble()
val lon = it.getQueryParameter("longitude")?.toDouble()
val name = it.getQueryParameter("name")
if (lat != null && lon != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
latLng = LatLng(lat, lon),
locationName = name
).toBundle()
)
.createPendingIntent()
} else if (name != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = name).toBundle())
.createPendingIntent()
} else if (intent?.scheme == "https" && intent?.data?.host in listOf("openchargemap.org", "map.openchargemap.io")) {
val id = when (intent.data?.host) {
"openchargemap.org" -> intent.data?.pathSegments?.last()?.toLongOrNull()
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
else -> null
}
if (id != null) {
if (prefs.dataSource != "openchargemap") {
prefs.dataSource = "openchargemap"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_openchargemap)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent.scheme == "net.vonforst.evmap") {
intent.data?.let {
if (it.host == "find_charger") {
val lat = it.getQueryParameter("latitude")?.toDouble()
val lon = it.getQueryParameter("longitude")?.toDouble()
val name = it.getQueryParameter("name")
if (lat != null && lon != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
latLng = LatLng(lat, lon),
locationName = name
).toBundle()
)
.createPendingIntent()
} else if (name != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = name).toBundle())
.createPendingIntent()
}
}
}
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
deepLink = navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.favs)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.createPendingIntent()
}
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
deepLink = navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.favs)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.createPendingIntent()
}
deepLink?.send()
deepLink?.send()
}
}
fun navigateTo(charger: ChargeLocation, rootView: View) {
fun navigateTo(charger: ChargeLocation) {
// google maps navigation
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
@@ -245,11 +244,11 @@ class MapsActivity : AppCompatActivity(),
startActivity(intent)
} else {
// fallback: generic geo intent
showLocation(charger, rootView)
showLocation(charger)
}
}
fun showLocation(charger: ChargeLocation, rootView: View) {
fun showLocation(charger: ChargeLocation) {
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(
@@ -257,33 +256,20 @@ class MapsActivity : AppCompatActivity(),
Uri.encode(charger.name)
})"
)
val resolveInfo =
packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg =
resolveInfo?.activityInfo?.packageName.takeIf { it != "android" && it != packageName }
if (pkg == null) {
// There is no default maps app or EVMap itself is the current default, fall back to app chooser
val chooserIntent = Intent.createChooser(intent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
startActivity(chooserIntent)
return
}
intent.setPackage(pkg)
try {
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
} else {
val cb = fragmentCallback ?: return
Snackbar.make(
rootView,
cb.getRootView(),
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
).show()
}
}
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = true) {
fun openUrl(url: String, preferBrowser: Boolean = true) {
val pkg = CustomTabsClient.getPackageName(this, null)
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
@@ -291,49 +277,17 @@ class MapsActivity : AppCompatActivity(),
.build()
)
.build()
val uri = Uri.parse(url)
val viewIntent = Intent(Intent.ACTION_VIEW, uri)
if (preferBrowser) {
// EVMap may be set as default app for this link, but we want to open it in a browser
// try to find default web browser
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
val resolveInfo =
packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg = resolveInfo?.activityInfo?.packageName.takeIf { it != "android" }
if (pkg == null) {
// There is no default browser, fall back to app chooser
val chooserIntent = Intent.createChooser(viewIntent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
val targets: List<ResolveInfo> = packageManager.queryIntentActivities(
viewIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
// add missing browsers (if EVMap is already set as default, Android might not find other browsers with the specific intent)
val browsers = packageManager.queryIntentActivities(
browserIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
val extraIntents = browsers.filter { browser ->
targets.find { it.activityInfo.packageName == browser.activityInfo.packageName } == null
}.map { browser ->
Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(browser.activityInfo.packageName)
}
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toTypedArray())
startActivity(chooserIntent)
return
}
intent.intent.setPackage(pkg)
pkg?.let {
// prefer to open URL in custom tab, even if native app
// available (such as EVMap itself)
if (preferBrowser) intent.intent.setPackage(pkg)
}
try {
intent.launchUrl(this, uri)
intent.launchUrl(this, Uri.parse(url))
} catch (e: ActivityNotFoundException) {
val cb = fragmentCallback ?: return
Snackbar.make(
rootView,
cb.getRootView(),
R.string.no_browser_app_found,
Snackbar.LENGTH_SHORT
).show()

View File

@@ -16,10 +16,7 @@ import android.text.SpannableStringBuilder
import android.text.SpannedString
import android.text.TextUtils
import android.text.style.StyleSpan
import android.view.View
import android.view.ViewTreeObserver
import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.Currency
import java.util.Locale
fun Bundle.optDouble(name: String): Double? {
@@ -142,14 +139,4 @@ fun PackageManager.isAppInstalled(packageName: String): Boolean {
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
fun currencyDisplayName(code: String) = "${Currency.getInstance(code).displayName} ($code)"
inline fun View.waitForLayout(crossinline f: () -> Unit) =
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
})
}

View File

@@ -9,6 +9,7 @@ import androidx.core.text.buildSpannedString
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.availability.tesla.Rates
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.bold
import net.vonforst.evmap.joinToSpannedString
import net.vonforst.evmap.model.ChargeCard

View File

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

View File

@@ -7,6 +7,8 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.cartesianProduct
import net.vonforst.evmap.model.ChargeLocation
@@ -16,6 +18,7 @@ import net.vonforst.evmap.viewmodel.Resource
import okhttp3.Cache
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.HttpException
import java.io.IOException
import java.net.CookieManager
@@ -38,6 +41,16 @@ interface AvailabilityDetector {
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
protected val radius = 150 // max radius in meters
protected suspend fun httpGet(url: String): String {
val request = Request.Builder().url(url).build()
val response = client.newCall(request).await()
if (!response.isSuccessful) throw IOException(response.message)
val str = response.body!!.string()
return str
}
protected fun getCorrespondingChargepoint(
cps: Iterable<Chargepoint>, type: String, power: Double
): Chargepoint? {

View File

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

View File

@@ -2,6 +2,7 @@ package net.vonforst.evmap.api.availability
import net.vonforst.evmap.api.availability.tesla.ChargerAvailability
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingGuestGraphQlApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.api.availability.tesla.asTeslaCoord
import net.vonforst.evmap.model.ChargeLocation
@@ -58,7 +59,7 @@ class TeslaOwnerAvailabilityDetector(
val details = api.getChargingSiteInformation(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables(
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.locationGUID),
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.id.text),
TeslaChargingOwnershipGraphQlApi.VehicleMakeType.NON_TESLA
)
)

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ import retrofit2.http.POST
import retrofit2.http.Query
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.LocalTime
interface TeslaAuthenticationApi {
@POST("oauth2/v3/token")
@@ -130,8 +131,8 @@ interface TeslaOwnerApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)
@@ -172,7 +173,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetNearbyChargingSitesVariables,
override val operationName: String = "GetNearbyChargingSites",
override val query: String =
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n locationGUID\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n teslaExclusive\n amenities\n chargingAccessibility\n ownerType\n isThirdPartySite\n usabilityArchetype\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isMagicDockSupportedSite\n hasParkingBenefit\n hasTou\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n"
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n "
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -201,7 +202,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getChargingSiteInformation",
override val query: String =
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n upsellingBanner(vehicleMakeType: \$vehicleMakeType) {\n header\n caption\n backgroundImageUrl\n routeName\n }\n nacsOnlyAssets {\n banner {\n header\n caption\n link\n }\n disclaimer {\n text\n sheetTitle\n sheetContent\n }\n }\n enableChargingSiteReportIssue\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n locationGUID\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isThirdPartySite\n isMagicDockSupportedSite\n trtId {\n value\n }\n siteDisclaimer\n chargingAccessibility\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isCanvasSite\n ownerDisclaimer\n chargingFeesDisclaimer {\n title\n description\n }\n idleFeesDisclaimer {\n title\n description\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n stateOfCharge\n chargerDisabled\n }\n waitEstimateBucket\n currentCongestion\n usabilityArchetype\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n dynamicRates {\n enabled\n }\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n congestion {\n ...ChargingUserRateFragment\n }\n service {\n ...ChargingUserRateFragment\n }\n electricity {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n stateOfCharge\n congestionGracePeriodSecs\n congestionPercent\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isMagicDockSupportedSite\n trtId {\n value\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n }\n waitEstimateBucket\n currentCongestion\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -216,11 +217,11 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier(
val id: String,
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.LOCATION_GUID
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID
)
enum class ChargingSiteIdentifierType {
SITE_ID, LOCATION_GUID
SITE_ID
}
enum class VehicleMakeType {
@@ -241,6 +242,7 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSite(
val activeOutages: List<Outage>,
val availableStalls: Value<Int>?,
val centroid: Coordinate,
val drivingDistanceMiles: Value<Double>?,
@@ -249,8 +251,7 @@ interface TeslaChargingOwnershipGraphQlApi {
val id: Text,
val localizedSiteName: Value<String>,
val maxPowerKw: Value<Int>,
val totalStalls: Value<Int>,
val locationGUID: String
val totalStalls: Value<Int>
// TODO: siteType, accessType
)
@@ -273,6 +274,7 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class SiteDynamic(
val activeOutages: List<Outage>,
val chargerDetails: List<ChargerDetail>,
val chargersAvailable: Value<Int>?,
val currentCongestion: Double,
@@ -371,8 +373,8 @@ interface TeslaChargingOwnershipGraphQlApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $t")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,22 +3,13 @@ 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.car.app.model.*
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
@@ -27,16 +18,7 @@ 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.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
@@ -50,9 +32,7 @@ import retrofit2.HttpException
import java.io.IOException
import kotlin.math.roundToInt
@ExperimentalCarApi
class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger: ChargeLocation) :
Screen(ctx) {
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
@@ -90,7 +70,7 @@ class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger:
carContext.stringProvider(),
chargepoint.type
)
} ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
} ${chargepoint.formatPower()} ${
carContext.getString(
R.string.chargeprice_stats,
meta.energy,
@@ -150,7 +130,7 @@ class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger:
)
).build()
).setOnClickListener {
openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
openUrl(carContext, ChargepriceApi.getPoiUrl(charger))
}.build()
).build()
)

View File

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

View File

@@ -9,36 +9,14 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Pane
import androidx.car.app.model.PaneTemplate
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.model.BooleanFilter
import net.vonforst.evmap.model.BooleanFilterValue
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.MultipleChoiceFilter
import net.vonforst.evmap.model.MultipleChoiceFilterValue
import net.vonforst.evmap.model.SliderFilter
import net.vonforst.evmap.model.SliderFilterValue
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -254,6 +232,7 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
),
CarToast.LENGTH_SHORT
).show()
invalidate()
}
}
}.build())
@@ -370,6 +349,7 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
),
CarToast.LENGTH_SHORT
).show()
invalidate()
screenManager.pop()
}
}
@@ -401,6 +381,7 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
}
if (!saveSuccess) return@pushForResult
}
invalidate()
}
.build()
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -100,9 +100,9 @@ class ChargepriceFragment : Fragment() {
inflater,
R.layout.fragment_chargeprice_header, container, false
)
binding.lifecycleOwner = viewLifecycleOwner
binding.lifecycleOwner = this
binding.vm = vm
headerBinding.lifecycleOwner = viewLifecycleOwner
headerBinding.lifecycleOwner = this
headerBinding.vm = vm
binding.toolbar.inflateMenu(R.menu.chargeprice)
@@ -141,7 +141,7 @@ class ChargepriceFragment : Fragment() {
val chargepriceAdapter = ChargepriceAdapter().apply {
onClickListener = {
(requireActivity() as MapsActivity).openUrl(it.url, binding.root)
(requireActivity() as MapsActivity).openUrl(it.url)
}
}
val joinedAdapter = ConcatAdapter(
@@ -194,10 +194,7 @@ class ChargepriceFragment : Fragment() {
}
binding.imgChargepriceLogo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root
)
(requireActivity() as MapsActivity).openUrl(ChargepriceApi.getPoiUrl(charger))
}
binding.btnSettings.setOnClickListener {
@@ -216,19 +213,11 @@ class ChargepriceFragment : Fragment() {
}
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
)
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
true
}
else -> false

View File

@@ -14,14 +14,14 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.databinding.DialogConnectorDetailsBinding
import net.vonforst.evmap.databinding.DialogConnectorDetailsHeaderBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
class ConnectorDetailsDialog(
binding: DialogConnectorDetailsBinding,
val binding: DialogConnectorDetailsBinding,
context: Context,
onClose: () -> Unit
) {
private var headerBinding_: DialogConnectorDetailsHeaderBinding? = null
private val headerBinding get() = headerBinding_!!
private val headerBinding: DialogConnectorDetailsHeaderBinding
private val detailsAdapter = ConnectorDetailsAdapter()
init {
@@ -30,7 +30,7 @@ class ConnectorDetailsDialog(
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
headerBinding_ = DataBindingUtil.inflate(
headerBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.dialog_connector_details_header, binding.list, false
)
@@ -60,8 +60,4 @@ class ConnectorDetailsDialog(
headerBinding.divider.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
headerBinding.item = ConnectorAdapter.ChargepointWithAvailability(cp, cpStatus)
}
fun onDestroy() {
headerBinding_ = null
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,7 @@ 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
@@ -75,11 +73,6 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
}
}
updateNativeIntegrationState()
val currencyPreference = findPreference<ListPreference>("chargeprice_currency")!!
currencyPreference.entries = currencyPreference.entryValues.map {
currencyDisplayName(it.toString()).replaceFirstChar { it.uppercase() }
}.toTypedArray()
}
private fun updateNativeIntegrationState() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,9 @@
package net.vonforst.evmap.ui
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.github.erfansn.localeconfigx.configuredLocales
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.Locale
fun updateNightMode(prefs: PreferenceDataSource) {
AppCompatDelegate.setDefaultNightMode(
@@ -28,17 +25,13 @@ fun updateAppLocale(language: String) {
)
}
fun getAppLocale(context: Context): String? {
fun getAppLocale(): String? {
val locales = AppCompatDelegate.getApplicationLocales()
return if (locales.isEmpty) {
"default"
} else {
val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() }
val choices = context.configuredLocales
choices.getFirstMatch(arr)?.toLanguageTag()
LocaleListCompat.forLanguageTags(BuildConfig.supportedLocales).getFirstMatch(arr)
?.toLanguageTag()
}
}
inline fun <R> LocaleListCompat.map(transform: (Locale) -> R): List<R> = List(size()) {
transform(get(it)!!)
}

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,14 @@
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 androidx.lifecycle.*
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.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
@@ -313,8 +298,4 @@ class ChargepriceViewModel(
}
}
}
fun resetBatteryRangeToDefault() {
batteryRange.value = prefs.chargepriceBatteryRangeAndroidAuto
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,14 +48,11 @@
tools:orientation="horizontal" />
<TextView
android:id="@+id/tvChargeFromTo"
android:id="@+id/textView2"
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"
@@ -71,8 +68,8 @@
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging &amp;&amp; vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
app:layout_constraintStart_toStartOf="@+id/tvChargeFromTo"
app:layout_constraintTop_toBottomOf="@+id/tvChargeFromTo"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
<TextView

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
<navigation 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"
app:startDestination="@id/map"
android:id="@+id/nav_graph">
<navigation

View File

@@ -1 +0,0 @@
unqualifiedResLocale=en-US

View File

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

View File

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

View File

@@ -128,6 +128,9 @@
<string name="pref_darkmode_device_default">Utiliser le réglage de l\'appareil</string>
<string name="pref_darkmode_always_on">toujours allumé</string>
<string name="pref_darkmode_always_off">toujours éteint</string>
<string name="pref_chargeprice_currency_czk">Couronne tchèque (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Couronne danoise (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="show_more">plus…</string>
<string name="fav_remove">Retirer des favoris</string>
<string name="amenities">Commodités</string>
@@ -227,6 +230,9 @@
<item quantity="other">(seront mis en évidence dans la comparaison des prix)</item>
</plurals>
<string name="deleted_recent_search_results">Les résultats de recherche récents ont été supprimés</string>
<string name="pref_chargeprice_currency_gbp">Livre sterling (GBP)</string>
<string name="pref_chargeprice_currency_isk">Couronne islandaise (ISK)</string>
<string name="pref_chargeprice_currency_nok">Couronne norvégienne (NOK)</string>
<string name="settings_charger_data">Stations de recharge</string>
<string name="got_it">J\'ai compris</string>
<string name="powered_by_mapbox">propulsé par Mapbox</string>
@@ -240,9 +246,15 @@
<string name="settings_data_sources">Sources de données</string>
<string name="data_sources_description">Veuillez choisir une source de données pour les stations de recharge. Vous pourrez la modifier ultérieurement dans les paramètres de l\'application.</string>
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses à récupérer. Veuillez envisager de faire un don via \"À propos\" -&gt; \"Faire un don\".</string>
<string name="pref_chargeprice_currency_hrk">Kuna croate (HRK)</string>
<string name="help">Aide</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge en courant alternatif monophasé de plus de 4,5 kW</string>
<string name="pref_chargeprice_currency_huf">Forint hongrois (HUF)</string>
<string name="pref_chargeprice_currency_pln">Złoty polonais (PLN)</string>
<string name="pref_map_rotate_gestures_on">Utilisez deux doigts pour faire pivoter la carte</string>
<string name="pref_chargeprice_currency_chf">Franc suisse (CHF)</string>
<string name="pref_chargeprice_currency_usd">Dollar américain (USD)</string>
<string name="pref_chargeprice_currency_sek">Couronne suédoise (SEK)</string>
<string name="cost_detail_charging"><b>Recharge %s</b></string>
<string name="cost_detail_parking"><b>Stationnement %s</b></string>
<string name="navigate">Naviguer vers</string>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="directions_fab_translationx">44dp</dimen>
</resources>

View File

@@ -84,6 +84,10 @@
<string name="got_it">Skjønner</string>
<string name="settings_data_sources">Datakilder</string>
<string name="pref_search_delete_recent">Slett nylige søkeresultater</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_nok">Norske kroner (NOK)</string>
<string name="pref_chargeprice_currency_gbp">Britiske pund (GBP)</string>
<string name="pref_chargeprice_currency_sek">Svenske kroner (SEK)</string>
<string name="realtime_data_loading">Sjekker sanntidsstatus …</string>
<string name="realtime_data_source">Kilde for sanntidsstatus (beta): %s</string>
<string name="realtime_data_unavailable">Sanntidsstatus utilgjengelig</string>
@@ -179,9 +183,16 @@
<string name="deleted_recent_search_results">Nylige søkeresultater slettet</string>
<string name="autocomplete_connection_error">Kunne ikke laste inn forslag</string>
<string name="pref_language_device_default">Enhetsforvalg</string>
<string name="pref_chargeprice_currency_chf">Sveitserfranc (CHF)</string>
<string name="pref_chargeprice_currency_czk">Tsjekkiske kroner (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Danske kroner (DKK)</string>
<string name="pref_chargeprice_currency_hrk">Kroatiske kroner (HRK)</string>
<string name="pref_map_rotate_gestures_on">Bruk to fingre for å rotere kartet</string>
<string name="pref_map_rotate_gestures_off">Rotasjon avslått (nord er alltid oppover)</string>
<string name="refresh_live_data">oppdater sanntidsstatus</string>
<string name="pref_chargeprice_currency_isk">Islandske kroner (ISK)</string>
<string name="pref_chargeprice_currency_pln">Polske zloty (PLN)</string>
<string name="pref_chargeprice_currency_usd">Amerikanske dollar (USD)</string>
<string name="filters_deactivated">Filtre deaktivert</string>
<string name="pref_navigate_use_maps_off">Navigasjonsknapp åpner kartprogrammet med laderposisjon</string>
<string name="show_more">flere …</string>
@@ -206,6 +217,7 @@
<string name="filter_exclude_faults">Utelat ladere med rapporterte feil</string>
<string name="plug_cee_blau">CEE blå</string>
<string name="filter_open_247">Døgnåpent</string>
<string name="pref_chargeprice_currency_huf">Ungarske forint (HUF)</string>
<string name="donation_dialog_detail">EVMap er fri programvare. Kodebidrag mottas med takk. Overvei å gi din støtte gjennom GitHub-sponsorer for å dekke løpende utgifter.</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Tillat enfaselading over 4.5 kW</string>
<string name="connectors">Tilkobling</string>

View File

@@ -5,6 +5,7 @@
<string name="pref_search_provider_info">Gegevens opzoeken is duur, vooral via Google Maps. Overweeg aub om een donatie te doen via “Over” -&gt; “Doneer”.</string>
<string name="data_source_openchargemap_desc">Werelddekkend, met variabele kwaliteit. Beschrijving in Engels of lokale taal. Onderhouden door de gebruikers. Ook open overheidswege eens in sommige landen (bv. Noord-Amerika, UK, Frankrijk, Noorwegen).</string>
<string name="pref_darkmode_always_off">altijd uit</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="chargeprice_select_car_first">Kiest eerst je voertuig model in de instellingen</string>
<string name="chargeprice_no_compatible_connectors">Geen compatibele connectoren aan dit laadstation</string>
<string name="license">Licentie</string>
@@ -227,6 +228,17 @@
<string name="pref_language_device_default">Standaardtaal van toestel</string>
<string name="pref_darkmode_device_default">Standaardinstelling van toestel</string>
<string name="pref_darkmode_always_on">altijd aan</string>
<string name="pref_chargeprice_currency_chf">Zwitserse Frank (CHF)</string>
<string name="pref_chargeprice_currency_czk">Tsjechische koruna (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Deense kroon (DKK)</string>
<string name="pref_chargeprice_currency_gbp">Britse Pond (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Hongaarse Forint (HUF)</string>
<string name="pref_chargeprice_currency_isk">IJslandse Kroon (ISK)</string>
<string name="pref_chargeprice_currency_nok">Noorse Kroon (NOK)</string>
<string name="pref_chargeprice_currency_pln">Poolse Złoty (PLN)</string>
<string name="pref_chargeprice_currency_sek">Zweedse Kroon (SEK)</string>
<string name="pref_chargeprice_currency_usd">Amerikaanse Dollar (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="edit_filter_profile">“%s” editeren</string>
<string name="compass">Kompas</string>

View File

@@ -148,7 +148,7 @@
<string name="pref_data_source">Fonte da informação</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="next">próximo</string>
<string name="data_source_openchargemap_desc"><![CDATA[Mundial, com vários níveis de qualidade. Descrições em inglês ou língua local. Mantido pela comunidade e usa informação governamental publica em alguns países (ex: América do Norte, Reino Unido, França, Noruega, etc).]]></string>
<string name="data_source_openchargemap_desc">Mundial, com vários níveis de qualidade. Descrições em inglês ou língua local. Mantido pela comunidade e usa informação governamental publica em alguns países (ex: América do Norte, Reino Unido, França, Noruega, etc).</string>
<string name="get_started">Começar</string>
<string name="lets_go">Vamos lá</string>
<string name="crash_report_text">O EVMap encontrou um problema. Por favor envie um relatório do erro para o criador da app.</string>
@@ -160,7 +160,7 @@
<string name="pref_map_rotate_gestures_on">Use dois dedos para girar o mapa</string>
<string name="pref_map_rotate_gestures_off">Rotação desligada (norte sempre para cima)</string>
<string name="refresh_live_data">atualizar estado em tempo real</string>
<string name="pref_search_provider_info"><![CDATA[As pesquisas são caras, especialmente quando o Google Maps é utilizado. Por favor considere doar através de "Sobre" → "Doar".]]></string>
<string name="pref_search_provider_info">As pesquisas são caras, especialmente se o Google Maps for utilizado. Por favor considere doar através de \"Sobre\" → \"Doar\".</string>
<string name="github_sponsors_desc">Apoie o EVMap através do GitHub</string>
<string name="unnamed_filter_profile">Filtro sem nome</string>
<string name="deleted_recent_search_results">As pesquisas recentes foram eliminadas</string>
@@ -177,6 +177,11 @@
<string name="pref_language_device_default">Língua do dispositivo</string>
<string name="pref_darkmode_device_default">Padrão do dispositivo</string>
<string name="pref_darkmode_always_on">Sempre ligado</string>
<string name="pref_chargeprice_currency_chf">Franco suíço (CHF)</string>
<string name="pref_chargeprice_currency_czk">Coroa checa (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Coroa dinamarquesa (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Libra esterlina (GBP)</string>
<string name="chargeprice_header_my_tariffs">Os meus planos</string>
<string name="chargeprice_header_other_tariffs">Outros planos</string>
<string name="developer_options">Opções de desenvolvedor</string>
@@ -205,18 +210,18 @@
<string name="operator">Operador</string>
<string name="network">Rede</string>
<string name="hours">Horário de abertura</string>
<string name="open_247"><![CDATA[<b>Aberto 24/7</b>]]></string>
<string name="closed"><![CDATA[<b>Fechado</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Aberto</b> · Fecha às %s]]></string>
<string name="closed_opensat"><![CDATA[<b>Fechado</b> · Abre às %s]]></string>
<string name="open_247"><b>Aberto 24/7</b></string>
<string name="closed"><b>Fechado</b></string>
<string name="open_closesat"><b>Aberto</b> · Fecha às %s</string>
<string name="closed_opensat"><b>Fechado</b> · Abre às %s</string>
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="closed_unfmt">Fechado</string>
<string name="holiday">Feriado</string>
<string name="cost">Custo</string>
<string name="cost_detail"><![CDATA[<b>Carregamento:</b> %1$s · <b>Parque:</b> %2$s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>Carregamento %s</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>Parque %s</b>]]></string>
<string name="cost_detail"><b>Carregamento:</b> %1$s · <b>Parque:</b> %2$s</string>
<string name="cost_detail_charging"><b>Carregamento %s</b></string>
<string name="cost_detail_parking"><b>Parque %s</b></string>
<string name="charging_free">Gratuito</string>
<string name="charging_paid">Pago</string>
<string name="parking_free">Gratuito</string>
@@ -282,6 +287,13 @@
<string name="welcome_1">Encontre carregadores elétricos perto de si</string>
<string name="close">Fechar</string>
<string name="edit_filter_profile">Editar “%s”</string>
<string name="pref_chargeprice_currency_hrk">Cuna croata (HRK)</string>
<string name="pref_chargeprice_currency_huf">Florim húngaro (HUF)</string>
<string name="pref_chargeprice_currency_isk">Coroa islandesa (ISK)</string>
<string name="pref_chargeprice_currency_nok">Coroa norueguesa (NOK)</string>
<string name="pref_chargeprice_currency_pln">Złoty polaco (PLN)</string>
<string name="pref_chargeprice_currency_sek">Coroa sueca (SEK)</string>
<string name="pref_chargeprice_currency_usd">Dólar americano (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contribuidores</string>
@@ -368,9 +380,4 @@
<string name="status_faulted">Fora de serviço</string>
<string name="status_since">%1$s desde %2$s</string>
<string name="status_unknown">Estado Desconhecido</string>
<string name="pref_chargeprice_native_integration">Comparação de preços no EVMap</string>
<string name="pref_chargeprice_native_integration_on">Os preços serão exibidos diretamente no EVMap</string>
<string name="pref_chargeprice_native_integration_off">O botão de comparação de preços abrirá a app ou site do Chargeprice</string>
<string name="pref_provider_osm">OpenStreetMap</string>
<string name="filterprofile_name_not_unique">Já existe um filtro com este nome</string>
</resources>

View File

@@ -259,6 +259,18 @@
<string name="pref_darkmode_device_default">Implicit dispozitiv</string>
<string name="pref_darkmode_always_on">permanent</string>
<string name="pref_darkmode_always_off">dezactivat</string>
<string name="pref_chargeprice_currency_chf">Franci elvetieni (CHF)</string>
<string name="pref_chargeprice_currency_czk">Coroane cehe (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Coroane daneze (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Lire sterline (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Croatian kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Forinti maghiari (HUF)</string>
<string name="pref_chargeprice_currency_isk">Coroane islandeze (ISK)</string>
<string name="pref_chargeprice_currency_nok">Coroane norvegiene (NOK)</string>
<string name="pref_chargeprice_currency_pln">Zloti polonezi (PLN)</string>
<string name="pref_chargeprice_currency_sek">Coroane suedeze (SEK)</string>
<string name="pref_chargeprice_currency_usd">Dolari americani (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contribuitori</string>

View File

@@ -1,5 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_language_names">
<item>@string/pref_language_device_default</item>
<item>@string/pref_language_en</item>
<item>@string/pref_language_de</item>
<item>@string/pref_language_cs</item>
<item>@string/pref_language_fr</item>
<item>@string/pref_language_nb_rNO</item>
<item>@string/pref_language_nl</item>
<item>@string/pref_language_pt</item>
<item>@string/pref_language_ro</item>
</string-array>
<string-array name="pref_language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>de</item>
<item>cs</item>
<item>fr</item>
<item>nb-NO</item>
<item>nl</item>
<item>pt</item>
<item>ro</item>
</string-array>
<string-array name="pref_darkmode_names">
<item>@string/pref_darkmode_device_default</item>
<item>@string/pref_darkmode_always_on</item>
@@ -10,7 +32,21 @@
<item>on</item>
<item>off</item>
</string-array>
<string-array name="pref_chargeprice_currencies" translatable="false">
<string-array name="pref_chargeprice_currency_names">
<item>@string/pref_chargeprice_currency_chf</item>
<item>@string/pref_chargeprice_currency_czk</item>
<item>@string/pref_chargeprice_currency_dkk</item>
<item>@string/pref_chargeprice_currency_eur</item>
<item>@string/pref_chargeprice_currency_gbp</item>
<item>@string/pref_chargeprice_currency_hrk</item>
<item>@string/pref_chargeprice_currency_huf</item>
<item>@string/pref_chargeprice_currency_isk</item>
<item>@string/pref_chargeprice_currency_nok</item>
<item>@string/pref_chargeprice_currency_pln</item>
<item>@string/pref_chargeprice_currency_sek</item>
<item>@string/pref_chargeprice_currency_usd</item>
</string-array>
<string-array name="pref_chargeprice_currency_values" translatable="false">
<item>CHF</item>
<item>CZK</item>
<item>DKK</item>

View File

@@ -12,5 +12,4 @@
<dimen name="map_toolbar_width">@dimen/match_parent</dimen>
<dimen name="layers_fab_top_padding">100dp</dimen>
<dimen name="directions_fab_translationx">0dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@@ -5,14 +5,19 @@
<string name="github_link">https://github.com/ev-map/EVMap</string>
<string name="twitter_handle">\@ev_map</string>
<string name="twitter_url">https://twitter.com/ev_map</string>
<string name="mastodon_handle">\@evmap\@electroverse.tech</string>
<string name="mastodon_url">https://electroverse.tech/@evmap</string>
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
<string name="tff_forum_url"><![CDATA[https://tff-forum.de/t/283834]]></string>
<string name="github_sponsors_link">https://github.com/sponsors/johan12345/</string>
<string name="chargeprice_api_url">https://api.chargeprice.app/v1/</string>
<string name="fronyx_url">https://fronyx.io/</string>
<string name="website_url">https://ev-map.app</string>
<string name="pref_language_en">English</string>
<string name="pref_language_de">Deutsch</string>
<string name="pref_language_fr">Français</string>
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
<string name="pref_language_nl">Nederlands</string>
<string name="pref_language_pt">Português</string>
<string name="pref_language_ro">Romana</string>
<string name="pref_language_cs">Czech</string>
<string name="about_contributors_list">
Danilo Bargen\n
Altonss\n
@@ -30,6 +35,17 @@
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
<string name="donate_link" translatable="false">https://ev-map.app/donate/</string>
<string name="tesla_referral_link" translatable="false">http://ts.la/johan94494</string>
<string name="juicify_referral_link" translatable="false">https://trck.juicify.green/trck/eclick/9dba357fbfed1e82fb05c7ec004ee2972ea174ce46d8ae0d</string>
<string name="geldfuereauto_referral_link" translatable="false">https://trck.geld-fuer-eauto.de/trck/eclick/c4713e9520bdb8842a3f1fbfa3a0669b3e58421043df78ad</string>
<string name="maingau_referral_link" translatable="false">https://trck.maingau-energie.de/trck/eclick/799b39cda39575dab1dcd3351abeb77b62dc33e4f9558a57</string>
<string name="ewieeinfach_referral_link" translatable="false">https://trck.e-wie-einfach.de/trck/eclick/fca74c186b54e7287a62102a13e073be4fc963825b85f7df</string>
<string name="eprimo_referral_link" translatable="false">https://netzwerk.uppr.de/trck/eclick/781768d2e779806b5e09229932662c14adddd69323594c52</string>
<string name="referral_juicify" translatable="false">Juicify</string>
<string name="referral_geldfuereauto" translatable="false">Geld für eAuto</string>
<string name="referral_maingau" translatable="false">Maingau</string>
<string name="referral_ewieeinfach" translatable="false">E wie einfach</string>
<string name="referral_eprimo" translatable="false">eprimo</string>
<string name="copyright_summary">©20202024 Johan von Forstner and contributors</string>
<string name="acra_backend_url" translatable="false">https://acra.muc.vonforst.net/report</string>
</resources>

View File

@@ -101,7 +101,6 @@
<string name="pref_language">App language</string>
<string name="pref_darkmode">Dark mode</string>
<string name="connection_error">Could not load charging stations</string>
<string name="zoom_in_to_see_more">Zoom in to see all charging stations</string>
<string name="location_error">Failed to detect location. Please check system settings</string>
<string name="retry">Retry</string>
<string name="filter_open_247">Available 24/7</string>
@@ -111,9 +110,7 @@
<string name="and_n_others">and %d others</string>
<string name="pref_map_provider">Map provider</string>
<string name="twitter">Twitter</string>
<string name="mastodon">Mastodon</string>
<string name="goingelectric_forum">Forum thread at GoingElectric.de</string>
<string name="tff_forum">Forum thread at TFF-Forum.de</string>
<string name="contact">Contact</string>
<string name="menu_report_new_charger">New charger</string>
<string name="edit_at_datasource">Edit at %s</string>
@@ -154,7 +151,6 @@
<string name="delete">Delete</string>
<string name="save_as_profile">Save as profile</string>
<string name="save_profile_enter_name">Enter the name of the filter profile:</string>
<string name="filterprofile_name_not_unique">There is already a filter profile with that name</string>
<string name="filterprofiles_empty_state">You have no filter profiles saved</string>
<string name="welcome_to_evmap">Welcome to EVMap</string>
<string name="welcome_1">Find electric vehicle chargers around you</string>
@@ -244,7 +240,6 @@
<string name="privacy_link">https://ev-map.app/privacypolicy/</string>
<string name="faq_link">https://ev-map.app/faq/</string>
<string name="chargeprice_faq_link">https://ev-map.app/faq/#price-comparison-feature</string>
<string name="referral_link">https://ev-map.app/referrals/</string>
<string name="required">required</string>
<string name="edit_filter_profile">Edit “%s”</string>
<string name="pref_search_delete_recent">Delete recent search results</string>
@@ -263,6 +258,18 @@
<string name="pref_darkmode_device_default">Device default</string>
<string name="pref_darkmode_always_on">always on</string>
<string name="pref_darkmode_always_off">always off</string>
<string name="pref_chargeprice_currency_chf">Swiss franc (CHF)</string>
<string name="pref_chargeprice_currency_czk">Czech koruna (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Danish krone (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Pound sterling (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Croatian kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Hungarian forint (HUF)</string>
<string name="pref_chargeprice_currency_isk">Icelandic króna (ISK)</string>
<string name="pref_chargeprice_currency_nok">Norwegian krone (NOK)</string>
<string name="pref_chargeprice_currency_pln">Polish złoty (PLN)</string>
<string name="pref_chargeprice_currency_sek">Swedish krona (SEK)</string>
<string name="pref_chargeprice_currency_usd">US dollar (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm">OpenStreetMap</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>

View File

@@ -39,10 +39,6 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/contact">
<Preference
android:key="mastodon"
android:title="@string/mastodon"
android:summary="@string/mastodon_handle" />
<Preference
android:key="twitter"
@@ -53,10 +49,6 @@
android:key="goingelectric"
android:title="@string/goingelectric_forum" />
<Preference
android:key="tffforum"
android:title="@string/tff_forum" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/other">

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="de" />
<locale android:name="fr" />
<locale android:name="nb-NO" />
</locale-config>

View File

@@ -18,7 +18,8 @@
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entryValues="@array/pref_chargeprice_currencies"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference

View File

@@ -10,11 +10,11 @@
android:defaultValue="goingelectric"
app:useSimpleSummaryProvider="true" />
<!--<CheckBoxPreference
<CheckBoxPreference
android:key="prediction_enabled"
android:title="@string/pref_prediction_enabled"
android:defaultValue="true"
android:summary="@string/pref_prediction_enabled_summary" />-->
android:summary="@string/pref_prediction_enabled_summary" />
<Preference
android:key="tesla_account"

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