mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 23:57:45 -05:00
Compare commits
80 Commits
1.7.1
...
screenshot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e1e22a44b | ||
|
|
d638a88f4d | ||
|
|
b9c08a8e75 | ||
|
|
ab2a77d25a | ||
|
|
e0ddc9f734 | ||
|
|
168fa244d3 | ||
|
|
76388ccc6e | ||
|
|
76aac7dae4 | ||
|
|
ee8b586079 | ||
|
|
360e7767bd | ||
|
|
5f0c9fd31d | ||
|
|
536c884f23 | ||
|
|
7daf5a0adb | ||
|
|
862f2b06d8 | ||
|
|
198a9ecc48 | ||
|
|
2762a32105 | ||
|
|
8a83a80e75 | ||
|
|
75e8569964 | ||
|
|
00b26d224f | ||
|
|
836f42b299 | ||
|
|
3de994f09d | ||
|
|
d78eda9d97 | ||
|
|
ed4be05aed | ||
|
|
45de4c8ff0 | ||
|
|
b5b785be07 | ||
|
|
0b8589d599 | ||
|
|
ba757831f3 | ||
|
|
1990152836 | ||
|
|
50fd433439 | ||
|
|
381e6f3d98 | ||
|
|
9c5582f19c | ||
|
|
1c16d8cbb6 | ||
|
|
1734f1c09e | ||
|
|
ebf0f82597 | ||
|
|
85a38a6da1 | ||
|
|
60ca97179c | ||
|
|
4413cba9fa | ||
|
|
e2e6a3060b | ||
|
|
7e507cad70 | ||
|
|
3b8aca3eff | ||
|
|
7a525d3f28 | ||
|
|
b0d7f465bc | ||
|
|
5bf839d4e9 | ||
|
|
73247b5a23 | ||
|
|
526addd010 | ||
|
|
287c5f60bc | ||
|
|
b68ab8f960 | ||
|
|
17d8bfc46a | ||
|
|
640f98c90f | ||
|
|
b1cf9809f6 | ||
|
|
c171c859af | ||
|
|
75c8114676 | ||
|
|
1d10ffeb52 | ||
|
|
0e278bfedb | ||
|
|
c6395feaa3 | ||
|
|
4b38c0de2d | ||
|
|
22d24f3bd0 | ||
|
|
d8e8475666 | ||
|
|
c3148796d4 | ||
|
|
8551966348 | ||
|
|
5f8be9dc0c | ||
|
|
0dc1a0270e | ||
|
|
609d984df1 | ||
|
|
5830965d3a | ||
|
|
b613b4d626 | ||
|
|
2e96ebbcd1 | ||
|
|
579ce088dc | ||
|
|
be15be00bd | ||
|
|
3266c623eb | ||
|
|
f6feb2cf8c | ||
|
|
ac1a0e01e3 | ||
|
|
7b038ad850 | ||
|
|
d02c9cc005 | ||
|
|
c83ecf1e5a | ||
|
|
8287084818 | ||
|
|
8f433a02a0 | ||
|
|
de6890e27e | ||
|
|
55b3a10919 | ||
|
|
08b6902020 | ||
|
|
7183475f31 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: johan12345
|
||||
custom: ['https://paypal.me/johan98', 'http://ts.la/johan94494']
|
||||
custom: ['https://paypal.me/johan98', 'https://ev-map.app/donate/']
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
|
||||
115
.github/workflows/screenshots.yml
vendored
Normal file
115
.github/workflows/screenshots.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
name: Generate Screenshots
|
||||
|
||||
jobs:
|
||||
|
||||
screenshot:
|
||||
name: Generate screenshots
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
device:
|
||||
- profile: pixel_6
|
||||
api-level: 34
|
||||
display_size: 1080x2336 # subtracted the 64px bottom navigation bar
|
||||
type: phone
|
||||
- profile: pixel_tablet
|
||||
api-level: 34
|
||||
display_size: 2560x1488 # subtracted the 64px navigation bar and 48px status bar
|
||||
type: tenInch
|
||||
env:
|
||||
ANDROID_USER_HOME: /home/runner/.android
|
||||
steps:
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Retrieve debug keystore
|
||||
env:
|
||||
DEBUG_KEYSTORE_BASE64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
mkdir ~/.config/.android
|
||||
echo $DEBUG_KEYSTORE_BASE64 | base64 --decode > ~/.config/.android/debug.keystore
|
||||
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "cmdline-tools;latest"
|
||||
rm -r $ANDROID_HOME/cmdline-tools/latest
|
||||
mv $ANDROID_HOME/cmdline-tools/latest-2 $ANDROID_HOME/cmdline-tools/latest
|
||||
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --version
|
||||
|
||||
- name: AVD cache
|
||||
uses: actions/cache@v3
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: ${{ runner.os }}-avd-api${{ matrix.device.api-level }}-${{ matrix.device.profile }}
|
||||
|
||||
- name: create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.device.api-level }}
|
||||
target: google_atd
|
||||
arch: x86_64
|
||||
profile: ${{ matrix.device.profile }}
|
||||
force-avd-creation: false
|
||||
ram-size: 2048M
|
||||
disk-size: 4096M
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
|
||||
disable-animations: true
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assembleGoogleNormalDebug assembleGoogleNormalAndroidTest
|
||||
env:
|
||||
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
||||
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
||||
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
|
||||
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
|
||||
|
||||
- name: Run emulator and generate screenshots
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.device.api-level }}
|
||||
target: google_atd
|
||||
arch: x86_64
|
||||
profile: ${{ matrix.device.profile }}
|
||||
force-avd-creation: false
|
||||
ram-size: 2048M
|
||||
disk-size: 4096M
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
|
||||
disable-animations: true
|
||||
script: |
|
||||
adb shell pm clear net.vonforst.evmap.debug || true
|
||||
adb shell wm size ${{ matrix.device.display_size }}
|
||||
adb logcat -c
|
||||
|
||||
adb logcat *:E &
|
||||
fastlane screengrab --app_apk_path app/build/outputs/apk/googleNormal/debug/app-google-normal-debug.apk --test_apk_path app/build/outputs/apk/androidTest/googleNormal/debug/app-google-normal-debug-androidTest.apk --tests_package_name=net.vonforst.evmap.debug.test --app_package_name net.vonforst.evmap.debug -p net.vonforst.evmap.screenshot --use_timestamp_suffix false --clear_previous_screenshots true --device_type=${{ matrix.device.type }} -q en-US,de-DE,fr-FR,nb-rNO,nl-NL,pt-PT,ro-RO
|
||||
|
||||
- name: Upload screenshots as artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshots-${{ matrix.device.profile }}-${{ matrix.device.api-level }}
|
||||
path: fastlane/metadata/android
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 Johan von Forstner and contributors
|
||||
Copyright (c) 2020-2024 Johan von Forstner and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -37,7 +37,7 @@ Development setup
|
||||
The App is developed using Android Studio and should pretty much work out-of-the-box when you clone
|
||||
the Git repository and open the project with Android Studio.
|
||||
|
||||
The only exception is that you need to obtain some free API keys for the different data sources that
|
||||
The only exception is that you need to obtain some API keys for the different data sources that
|
||||
EVMap uses and put them into the app in the form of a resource file called `apikeys.xml` under
|
||||
`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).
|
||||
|
||||
@@ -8,10 +8,8 @@ plugins {
|
||||
id("kotlin-kapt")
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
id("pt.jcosta.resourceplaceholders")
|
||||
}
|
||||
|
||||
val supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro"
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
@@ -20,12 +18,10 @@ android {
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode = 206
|
||||
versionName = "1.7.1"
|
||||
versionCode = 212
|
||||
versionName = "1.8.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
resourceConfigurations += supportedLocales.split(",")
|
||||
buildConfigField("String", "supportedLocales", "\"$supportedLocales\"")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -95,11 +91,15 @@ android {
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
lint {
|
||||
disable += listOf("NullSafeMutableLiveData")
|
||||
warning += listOf("MissingTranslation")
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
@@ -107,9 +107,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
resourcePlaceholders {
|
||||
files("xml/shortcuts.xml")
|
||||
}
|
||||
namespace = "net.vonforst.evmap"
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
@@ -228,17 +225,17 @@ dependencies {
|
||||
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.8.0")
|
||||
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.10.0")
|
||||
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.6.0")
|
||||
implementation("androidx.browser:browser:1.7.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||
implementation("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")
|
||||
@@ -258,13 +255,13 @@ dependencies {
|
||||
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
||||
|
||||
// Android Auto
|
||||
val carAppVersion = "1.4.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 = "8f1226e1c5"
|
||||
val anyMapsVersion = "4854581f72"
|
||||
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
|
||||
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
|
||||
googleImplementation("com.google.android.gms:play-services-maps:18.2.0")
|
||||
@@ -279,8 +276,16 @@ dependencies {
|
||||
// patched version that removes build-time dependency on GMS (-> no Google location services)
|
||||
fossImplementation("com.github.ev-map:mapbox-events-android:a21c324501")
|
||||
|
||||
implementation("com.mapbox.mapboxsdk:mapbox-android-sdk") {
|
||||
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-accounts")
|
||||
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-telemetry")
|
||||
version {
|
||||
strictly("9.1.0-SNAPSHOT")
|
||||
}
|
||||
}
|
||||
|
||||
// Google Places
|
||||
googleImplementation("com.google.android.libraries.places:places:3.2.0")
|
||||
googleImplementation("com.google.android.libraries.places:places:3.3.0")
|
||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||
|
||||
// Mapbox Geocoding
|
||||
@@ -296,14 +301,17 @@ dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
|
||||
|
||||
// room library
|
||||
val room_version = "2.6.0"
|
||||
val room_version = "2.6.1"
|
||||
implementation("androidx.room:room-runtime:$room_version")
|
||||
kapt("androidx.room:room-compiler:$room_version")
|
||||
implementation("androidx.room:room-ktx:$room_version")
|
||||
implementation("com.github.anboralabs:spatia-room:0.2.7")
|
||||
implementation("com.github.anboralabs:spatia-room:0.2.9") {
|
||||
exclude(group = "com.github.dalgarins", module = "android-spatialite")
|
||||
}
|
||||
implementation("com.github.EV-map:android-spatialite:e5495c83ad") // version with minSdk increased to 21 & FORTIFY_SOURCE enabled
|
||||
|
||||
// billing library
|
||||
val billing_version = "6.0.1"
|
||||
val billing_version = "6.1.0"
|
||||
googleImplementation("com.android.billingclient:billing:$billing_version")
|
||||
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
|
||||
|
||||
@@ -314,26 +322,28 @@ dependencies {
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// debug tools
|
||||
debugImplementation("com.facebook.flipper:flipper:0.190.0")
|
||||
debugImplementation("com.facebook.flipper:flipper:0.238.0")
|
||||
debugImplementation("com.facebook.soloader:soloader:0.10.5")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.190.0")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0")
|
||||
debugImplementation("androidx.test.espresso:espresso-idling-resource:3.5.1")
|
||||
|
||||
// testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0")
|
||||
//noinspection GradleDependency
|
||||
testImplementation("org.json:json:20080701")
|
||||
testImplementation("org.robolectric:robolectric:4.10.3")
|
||||
testImplementation("org.robolectric:robolectric:4.11.1")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
|
||||
// testing for car app
|
||||
testGoogleImplementation("androidx.car.app:app-testing:$carAppVersion")
|
||||
testGoogleImplementation("androidx.test:core:1.5.0")
|
||||
testImplementation("androidx.car.app:app-testing:$carAppVersion")
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
|
||||
androidTestImplementation("androidx.test:rules:1.5.0")
|
||||
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
androidTestImplementation("tools.fastlane:screengrab:2.1.1")
|
||||
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
|
||||
|
||||
|
||||
6
app/lint.xml
Normal file
6
app/lint.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingQuantity">
|
||||
<ignore regexp=".*?Czech.*?many" />
|
||||
</issue>
|
||||
</lint>
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.vonforst.evmap.screenshot
|
||||
|
||||
import android.view.View
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.IdlingResource
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* An espresso idling resource implementation that reports idle status for all data binding
|
||||
* layouts. Data Binding uses a mechanism to post messages which Espresso doesn't track yet.
|
||||
*
|
||||
* Since this application only uses fragments, the resource only checks the fragments and their
|
||||
* children instead of the whole view tree.
|
||||
*
|
||||
* Tracking bug: https://github.com/android/android-test/issues/317
|
||||
*/
|
||||
class DataBindingIdlingResource(
|
||||
activityScenarioRule: ActivityScenarioRule<out FragmentActivity>
|
||||
) : IdlingResource {
|
||||
// list of registered callbacks
|
||||
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
|
||||
|
||||
// give it a unique id to workaround an espresso bug where you cannot register/unregister
|
||||
// an idling resource w/ the same name.
|
||||
private val id = UUID.randomUUID().toString()
|
||||
|
||||
// holds whether isIdle is called and the result was false. We track this to avoid calling
|
||||
// onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
|
||||
private var wasNotIdle = false
|
||||
|
||||
lateinit var activity: FragmentActivity
|
||||
|
||||
override fun getName() = "DataBinding $id"
|
||||
|
||||
init {
|
||||
monitorActivity(activityScenarioRule.scenario)
|
||||
}
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
val idle = !getBindings().any { it.hasPendingBindings() }
|
||||
@Suppress("LiftReturnOrAssignment")
|
||||
if (idle) {
|
||||
if (wasNotIdle) {
|
||||
// notify observers to avoid espresso race detector
|
||||
idlingCallbacks.forEach { it.onTransitionToIdle() }
|
||||
}
|
||||
wasNotIdle = false
|
||||
} else {
|
||||
wasNotIdle = true
|
||||
// check next frame
|
||||
activity.findViewById<View>(android.R.id.content).postDelayed({
|
||||
isIdleNow
|
||||
}, 16)
|
||||
}
|
||||
return idle
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
|
||||
idlingCallbacks.add(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource].
|
||||
*/
|
||||
private fun monitorActivity(
|
||||
activityScenario: ActivityScenario<out FragmentActivity>
|
||||
) {
|
||||
activityScenario.onActivity {
|
||||
this.activity = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all binding classes in all currently available fragments.
|
||||
*/
|
||||
private fun getBindings(): List<ViewDataBinding> {
|
||||
val fragments = (activity as? FragmentActivity)
|
||||
?.supportFragmentManager
|
||||
?.fragments
|
||||
|
||||
val bindings =
|
||||
fragments?.mapNotNull {
|
||||
it.view?.getBinding()
|
||||
} ?: emptyList()
|
||||
val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments }
|
||||
?.mapNotNull { it.view?.getBinding() } ?: emptyList()
|
||||
|
||||
return bindings + childrenBindings
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this)
|
||||
@@ -0,0 +1,155 @@
|
||||
package net.vonforst.evmap.screenshot
|
||||
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.pressBack
|
||||
import androidx.test.espresso.contrib.DrawerActions
|
||||
import androidx.test.espresso.contrib.NavigationViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.test.rule.GrantPermissionRule.grant
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.EXTRA_CHARGER_ID
|
||||
import net.vonforst.evmap.EXTRA_LAT
|
||||
import net.vonforst.evmap.EXTRA_LON
|
||||
import net.vonforst.evmap.EspressoIdlingResource
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.model.Favorite
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import tools.fastlane.screengrab.Screengrab
|
||||
import tools.fastlane.screengrab.locale.LocaleTestRule
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ScreenshotTest {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun beforeAll() {
|
||||
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
|
||||
|
||||
Screengrab.setDefaultScreenshotStrategy { screenshotName, screenshotCallback ->
|
||||
screenshotCallback.screenshotCaptured(
|
||||
screenshotName,
|
||||
androidx.test.core.app.takeScreenshot()
|
||||
)
|
||||
}
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val prefs = PreferenceDataSource(context)
|
||||
prefs.dataSourceSet = true
|
||||
prefs.welcomeDialogShown = true
|
||||
prefs.privacyAccepted = true
|
||||
prefs.opensourceDonationsDialogLastShown = Instant.now()
|
||||
prefs.chargepriceMyVehicles = setOf("b58bc94d-d929-ad71-d95b-08b877bf76ba")
|
||||
prefs.appStartCounter = 0
|
||||
prefs.mapProvider = "google"
|
||||
|
||||
// insert favorites
|
||||
val db = AppDatabase.getInstance(context)
|
||||
val api = GoingElectricApiWrapper(
|
||||
context.getString(R.string.goingelectric_key),
|
||||
context = context
|
||||
)
|
||||
val ids = listOf(70774L to true, 40315L to true, 65330L to true, 62489L to false)
|
||||
runBlocking {
|
||||
val refData = api.getReferenceData().data as GEReferenceData
|
||||
ids.forEachIndexed { i, (id, favorite) ->
|
||||
val detail = api.getChargepointDetail(refData, id).data!!
|
||||
db.chargeLocationsDao().insert(detail)
|
||||
if (db.favoritesDao().findFavorite(id, "goingelectric") == null && favorite) {
|
||||
db.favoritesDao().insert(
|
||||
Favorite(
|
||||
chargerId = id,
|
||||
chargerDataSource = "goingelectric"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val localeTestRule = LocaleTestRule()
|
||||
|
||||
@get:Rule
|
||||
val activityRule: ActivityScenarioRule<MapsActivity> = ActivityScenarioRule(
|
||||
Intent(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
MapsActivity::class.java
|
||||
).apply {
|
||||
putExtra(EXTRA_CHARGER_ID, 62489L)
|
||||
putExtra(EXTRA_LAT, 53.099512)
|
||||
putExtra(EXTRA_LON, 9.981884)
|
||||
})
|
||||
|
||||
@get:Rule
|
||||
val permissionRule: GrantPermissionRule = grant(ACCESS_FINE_LOCATION)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
IdlingRegistry.getInstance().register(DataBindingIdlingResource(activityRule))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTakeScreenshot() {
|
||||
Thread.sleep(15000L)
|
||||
Screengrab.screenshot("01_map_google")
|
||||
|
||||
onView(withId(R.id.topPart)).perform(click())
|
||||
|
||||
Thread.sleep(1000L)
|
||||
Screengrab.screenshot("02_detail")
|
||||
|
||||
onView(withId(R.id.btnChargeprice)).perform(click())
|
||||
Thread.sleep(5000L)
|
||||
Screengrab.screenshot("03_prices")
|
||||
|
||||
onView(isRoot()).perform(pressBack())
|
||||
Thread.sleep(500L)
|
||||
onView(isRoot()).perform(pressBack())
|
||||
|
||||
Thread.sleep(2000L)
|
||||
|
||||
onView(withId(R.id.menu_filter)).perform(click())
|
||||
Thread.sleep(1000L)
|
||||
onView(withText(R.string.menu_edit_filters)).perform(click())
|
||||
|
||||
Thread.sleep(1000L)
|
||||
|
||||
Screengrab.screenshot("05_filters")
|
||||
onView(isRoot()).perform(pressBack())
|
||||
|
||||
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
|
||||
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.favs))
|
||||
|
||||
Thread.sleep(10000L)
|
||||
Screengrab.screenshot("04_favorites")
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
PreferenceDataSource(context).mapProvider = "mapbox"
|
||||
onView(isRoot()).perform(pressBack())
|
||||
|
||||
Thread.sleep(5000L)
|
||||
Screengrab.screenshot("01_map_mapbox")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.johan.evmap.storage
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
5
app/src/automotive/res/values-cs/strings.xml
Normal file
5
app/src/automotive/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Povolit</string>
|
||||
<string name="auto_location_permission_needed">Pro spuštění aplikace EVMap ve vašem autě musíte povolit přístup k vaší poloze.</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Permissions needed for fastlane screengrab -->
|
||||
|
||||
<!-- Allows unlocking your device and activating its screen so UI tests can succeed -->
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Allows for storing and retrieving screenshots -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- Allows changing locales -->
|
||||
<uses-permission
|
||||
android:name="android.permission.CHANGE_CONFIGURATION"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!-- Allows changing SystemUI demo mode -->
|
||||
<uses-permission
|
||||
android:name="android.permission.DUMP"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<application>
|
||||
<activity
|
||||
|
||||
@@ -2,6 +2,8 @@ package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.idling.CountingIdlingResource
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
@@ -34,9 +36,29 @@ fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
||||
} catch (e: ClassNotFoundException) {
|
||||
isRunningTest = false
|
||||
}
|
||||
|
||||
|
||||
if (!isRunningTest) {
|
||||
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains a static reference to [IdlingResource], only available in the 'debug' build type.
|
||||
*/
|
||||
object EspressoIdlingResource {
|
||||
private const val RESOURCE = "GLOBAL"
|
||||
|
||||
@JvmField
|
||||
val countingIdlingResource = CountingIdlingResource(RESOURCE)
|
||||
|
||||
fun increment() {
|
||||
countingIdlingResource.increment()
|
||||
}
|
||||
|
||||
fun decrement() {
|
||||
if (!countingIdlingResource.isIdleNow) {
|
||||
countingIdlingResource.decrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,11 @@ import com.google.android.material.transition.MaterialSharedAxis
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.FragmentDonateBinding
|
||||
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
class DonateFragment : DonateFragmentBase() {
|
||||
private lateinit var binding: FragmentDonateBinding
|
||||
private lateinit var referrals: FragmentDonateReferralBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -28,6 +30,7 @@ class DonateFragment : Fragment() {
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentDonateBinding.inflate(inflater, container, false)
|
||||
referrals = binding.referrals
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -43,8 +46,6 @@ class DonateFragment : Fragment() {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
|
||||
}
|
||||
|
||||
binding.referrals.referralTesla.setOnClickListener {
|
||||
(requireActivity() as MapsActivity).openUrl(getString(R.string.tesla_referral_link))
|
||||
}
|
||||
setupReferrals(referrals)
|
||||
}
|
||||
}
|
||||
6
app/src/foss/res/values-cs/strings.xml
Normal file
6
app/src/foss/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj zasláním finančního daru vývojáři.</string>
|
||||
<string name="donate_paypal">Přispět pomocí PayPalu</string>
|
||||
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
@@ -6,9 +6,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
@@ -25,7 +23,7 @@ import net.vonforst.evmap.databinding.FragmentDonateHeaderBinding
|
||||
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
||||
import net.vonforst.evmap.viewmodel.DonateViewModel
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
class DonateFragment : DonateFragmentBase() {
|
||||
private lateinit var binding: FragmentDonateBinding
|
||||
private val vm: DonateViewModel by viewModels()
|
||||
private lateinit var header: FragmentDonateHeaderBinding
|
||||
@@ -86,9 +84,7 @@ class DonateFragment : Fragment() {
|
||||
Snackbar.make(view, R.string.donation_failed, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
referrals.referralTesla.setOnClickListener {
|
||||
(requireActivity() as MapsActivity).openUrl(getString(R.string.tesla_referral_link))
|
||||
}
|
||||
setupReferrals(referrals)
|
||||
|
||||
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
|
||||
|
||||
7
app/src/google/res/values-cs/strings.xml
Normal file
7
app/src/google/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři.
|
||||
\n
|
||||
\nGoogle si z každého daru strhne 15 %.</string>
|
||||
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<package android:name="com.google.android.projection.gearhead" />
|
||||
<package android:name="com.google.android.apps.automotive.templates.host" />
|
||||
<package android:name="com.google.android.apps.maps" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
@@ -40,8 +41,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:localeConfig="@xml/locales_config">
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<meta-data
|
||||
android:name="com.mapbox.ACCESS_TOKEN"
|
||||
@@ -273,6 +273,10 @@
|
||||
android:host="openchargemap.org"
|
||||
android:pathPattern="/site/poi/details/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="map.openchargemap.io"
|
||||
android:path="/"
|
||||
android:scheme="https" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
@@ -76,7 +76,5 @@ class EvMapApplication : Application(), Configuration.Provider {
|
||||
)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration {
|
||||
return Configuration.Builder().build()
|
||||
}
|
||||
override val workManagerConfiguration = Configuration.Builder().build()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.app.ActivityOptions
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
@@ -155,8 +156,12 @@ class MapsActivity : AppCompatActivity(),
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "openchargemap.org") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
} 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"
|
||||
@@ -202,18 +207,15 @@ class MapsActivity : AppCompatActivity(),
|
||||
}
|
||||
}
|
||||
} 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()
|
||||
navController.navigate(
|
||||
R.id.map, MapFragmentArgs(
|
||||
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
|
||||
latLng = LatLng(
|
||||
intent.getDoubleExtra(EXTRA_LAT, 0.0),
|
||||
intent.getDoubleExtra(EXTRA_LON, 0.0)
|
||||
)
|
||||
).toBundle()
|
||||
)
|
||||
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
@@ -264,7 +266,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
fun openUrl(url: String, preferBrowser: Boolean = true) {
|
||||
val pkg = CustomTabsClient.getPackageName(this, null)
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
@@ -276,7 +278,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
pkg?.let {
|
||||
// prefer to open URL in custom tab, even if native app
|
||||
// available (such as EVMap itself)
|
||||
intent.intent.setPackage(pkg)
|
||||
if (preferBrowser) intent.intent.setPackage(pkg)
|
||||
}
|
||||
try {
|
||||
intent.launchUrl(this, Uri.parse(url))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
@@ -9,10 +10,14 @@ import android.icu.util.LocaleData
|
||||
import android.icu.util.ULocale
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.SpannedString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.StyleSpan
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
if (!this.containsKey(name)) return null
|
||||
@@ -117,4 +122,21 @@ fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): Pa
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int = 0): ApplicationInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") getApplicationInfo(packageName, flags)
|
||||
}
|
||||
|
||||
|
||||
fun PackageManager.isAppInstalled(packageName: String): Boolean {
|
||||
return try {
|
||||
getApplicationInfoCompat(packageName, 0).enabled
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
@@ -21,6 +22,7 @@ import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
|
||||
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.CheckableConstraintLayout
|
||||
import java.time.Instant
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean
|
||||
@@ -94,7 +96,19 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector
|
||||
}
|
||||
|
||||
class ChargepriceAdapter() :
|
||||
class ConnectorDetailsAdapter : DataBindingAdapter<ConnectorDetailsAdapter.ConnectorDetails>() {
|
||||
data class ConnectorDetails(
|
||||
val status: ChargepointStatus?,
|
||||
val evseId: String?,
|
||||
val label: String?,
|
||||
val lastChange: Instant?
|
||||
) :
|
||||
Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.dialog_connector_details_item
|
||||
}
|
||||
|
||||
class ChargepriceAdapter :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
|
||||
val viewPool = RecyclerView.RecycledViewPool()
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.text.style.StyleSpan
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
|
||||
import net.vonforst.evmap.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
|
||||
@@ -47,7 +49,7 @@ fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
teslaPricing: TeslaGraphQlApi.Pricing?,
|
||||
teslaPricing: Pricing?,
|
||||
ctx: Context
|
||||
): List<DetailsAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
@@ -139,7 +141,7 @@ fun buildDetails(
|
||||
)
|
||||
}
|
||||
|
||||
fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
fun formatTeslaParkingFee(teslaPricing: Pricing, ctx: Context) =
|
||||
teslaPricing.memberRates?.activePricebook?.parking?.let { parkingFee ->
|
||||
ctx.getString(
|
||||
R.string.tesla_pricing_blocking_fee,
|
||||
@@ -147,7 +149,7 @@ fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
)
|
||||
}
|
||||
|
||||
fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
fun formatTeslaPricing(teslaPricing: Pricing, ctx: Context) =
|
||||
buildSpannedString {
|
||||
teslaPricing.memberRates?.let { memberRates ->
|
||||
append(
|
||||
@@ -168,7 +170,7 @@ fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTeslaPricingRates(rates: TeslaGraphQlApi.Rates, ctx: Context) =
|
||||
private fun formatTeslaPricingRates(rates: Rates, ctx: Context) =
|
||||
buildSpannedString {
|
||||
val timeFmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
if (rates.activePricebook.charging.touRates.enabled) {
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.Cache
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -19,6 +23,7 @@ import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface AvailabilityDetector {
|
||||
@@ -136,7 +141,9 @@ data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String,
|
||||
val evseIds: Map<Chargepoint, List<String>>? = null,
|
||||
val labels: Map<Chargepoint, List<String?>>? = null,
|
||||
val congestionHistogram: List<Double>? = null,
|
||||
val lastChange: Map<Chargepoint, List<Instant?>>? = null,
|
||||
val extraData: Any? = null // API-specific data
|
||||
) {
|
||||
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
|
||||
@@ -152,59 +159,73 @@ data class ChargeLocationStatus(
|
||||
val totalChargepoints = status.map { it.key.count }.sum()
|
||||
}
|
||||
|
||||
enum class ChargepointStatus {
|
||||
@Parcelize
|
||||
enum class ChargepointStatus : Parcelable {
|
||||
AVAILABLE, UNKNOWN, CHARGING, OCCUPIED, FAULTED
|
||||
}
|
||||
|
||||
class AvailabilityDetectorException(message: String) : Exception(message)
|
||||
|
||||
class NotSignedInException : IOException("not signed in")
|
||||
|
||||
private val cookieManager = CookieManager().apply {
|
||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
}
|
||||
|
||||
class AvailabilityRepository(context: Context) {
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5MB
|
||||
private val okhttp = OkHttpClient.Builder()
|
||||
.addInterceptor(RateLimitInterceptor())
|
||||
.addDebugInterceptors()
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
.cache(Cache(context.cacheDir, cacheSize))
|
||||
.build()
|
||||
private val teslaAvailabilityDetector =
|
||||
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context))
|
||||
private val teslaOwnerAvailabilityDetector =
|
||||
TeslaOwnerAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context))
|
||||
private val availabilityDetectors = listOf(
|
||||
RheinenergieAvailabilityDetector(okhttp),
|
||||
teslaAvailabilityDetector,
|
||||
teslaOwnerAvailabilityDetector,
|
||||
TeslaGuestAvailabilityDetector(okhttp),
|
||||
EnBwAvailabilityDetector(okhttp),
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
)
|
||||
|
||||
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
var result: ChargeLocationStatus? = null
|
||||
var exception: Throwable? = null
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
if (!ad.isChargerSupported(charger)) continue
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
result = ad.getAvailability(charger)
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
exception = exception.takeIf { it is NotSignedInException } ?: e
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
exception = exception.takeIf { it is NotSignedInException } ?: e
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
exception = exception.takeIf { it is NotSignedInException } ?: e
|
||||
e.printStackTrace()
|
||||
} catch (e: NotSignedInException) {
|
||||
exception = e
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
result?.let {
|
||||
return Resource.success(it)
|
||||
}
|
||||
return Resource.error(exception?.message, null)
|
||||
}
|
||||
|
||||
fun isSupercharger(charger: ChargeLocation) =
|
||||
teslaAvailabilityDetector.isChargerSupported(charger)
|
||||
teslaOwnerAvailabilityDetector.isChargerSupported(charger)
|
||||
|
||||
fun isTeslaSupported(charger: ChargeLocation) =
|
||||
teslaAvailabilityDetector.isChargerSupported(charger) && teslaAvailabilityDetector.isSignedIn()
|
||||
teslaOwnerAvailabilityDetector.isChargerSupported(charger) && teslaOwnerAvailabilityDetector.isSignedIn()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import net.vonforst.evmap.api.availability.tesla.LocalTimeAdapter
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
@@ -10,6 +14,8 @@ import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
private const val maxDistance = 60 // max distance between reported positions in meters
|
||||
@@ -53,7 +59,8 @@ interface EnBwApi {
|
||||
data class EnBwChargePoint(
|
||||
val evseId: String?,
|
||||
val status: String,
|
||||
val connectors: List<EnBwConnector>
|
||||
val connectors: List<EnBwConnector>,
|
||||
val state: EnBwState?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -70,6 +77,11 @@ interface EnBwApi {
|
||||
val upperRightLon: Double
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EnBwState(
|
||||
val updatedAt: Instant?
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): EnBwApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
@@ -85,7 +97,11 @@ interface EnBwApi {
|
||||
}.build()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://enbw-emp.azure-api.net/emobility-public-api/api/v1/")
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(InstantAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(EnBwApi::class.java)
|
||||
@@ -93,6 +109,23 @@ interface EnBwApi {
|
||||
}
|
||||
}
|
||||
|
||||
internal class InstantAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: Long?): Instant? = value?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Instant?): Long? = value?.toEpochMilli()
|
||||
}
|
||||
|
||||
data class EnBwStatus(
|
||||
val conn: EnBwApi.EnBwConnector,
|
||||
val status: String,
|
||||
val evseId: String?,
|
||||
val lastChange: Instant?
|
||||
)
|
||||
|
||||
class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
val api = EnBwApi.create(client, baseUrl)
|
||||
@@ -157,14 +190,15 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
|
||||
val connectorStatus = details.flatMap { it.chargePoints }.flatMap { cp ->
|
||||
cp.connectors.map { connector ->
|
||||
Triple(connector, cp.status, cp.evseId)
|
||||
EnBwStatus(connector, cp.status, cp.evseId, cp.state?.updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
val enbwConnectors = mutableMapOf<Long, Pair<Double, String>>()
|
||||
val enbwStatus = mutableMapOf<Long, ChargepointStatus>()
|
||||
val enbwEvseId = mutableMapOf<Long, String>()
|
||||
connectorStatus.forEachIndexed { index, (connector, statusStr, evseId) ->
|
||||
val enbwLastChange = mutableMapOf<Long, Instant?>()
|
||||
connectorStatus.forEachIndexed { index, (connector, statusStr, evseId, updatedAt) ->
|
||||
val id = index.toLong()
|
||||
val power = connector.maxPowerInKw ?: 0.0
|
||||
val type = when (connector.plugTypeName) {
|
||||
@@ -187,6 +221,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
}
|
||||
enbwConnectors[id] = power to type
|
||||
enbwStatus[id] = status
|
||||
enbwLastChange[id] = updatedAt
|
||||
evseId?.let { enbwEvseId[id] = it }
|
||||
}
|
||||
|
||||
@@ -197,10 +232,13 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
val evseIds = if (enbwEvseId.size == enbwStatus.size) match.mapValues { entry ->
|
||||
entry.value.map { enbwEvseId[it]!! }
|
||||
} else null
|
||||
val lastChange =
|
||||
if (enbwLastChange.size == enbwStatus.size) match.mapValues { entry -> entry.value.map { enbwLastChange[it] } } else null
|
||||
return ChargeLocationStatus(
|
||||
chargepointStatus,
|
||||
"EnBW",
|
||||
evseIds
|
||||
evseIds,
|
||||
lastChange = lastChange
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import androidx.car.app.model.DateTimeWithZone
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
@@ -9,6 +13,11 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.*
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
@@ -42,7 +51,12 @@ interface NewMotionApi {
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMEvse(val evseId: String?, val status: String, val connectors: List<NMConnector>)
|
||||
data class NMEvse(
|
||||
val evseId: String?,
|
||||
val status: String,
|
||||
val connectors: List<NMConnector>,
|
||||
val updated: ZonedDateTime?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMConnector(
|
||||
@@ -78,7 +92,11 @@ interface NewMotionApi {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): NewMotionApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://ui-map.shellrecharge.com/api/map/v2/")
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(ZonedDateTimeAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(NewMotionApi::class.java)
|
||||
@@ -86,6 +104,21 @@ interface NewMotionApi {
|
||||
}
|
||||
}
|
||||
|
||||
internal class ZonedDateTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String): ZonedDateTime? = ZonedDateTime.parse(value)
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: ZonedDateTime): String = value.toString()
|
||||
}
|
||||
|
||||
data class NmStatus(
|
||||
val conn: NewMotionApi.NMConnector,
|
||||
val status: String,
|
||||
val evseId: String?,
|
||||
val updated: ZonedDateTime?
|
||||
)
|
||||
|
||||
class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
val api = NewMotionApi.create(client, baseUrl)
|
||||
@@ -111,9 +144,9 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
throw AvailabilityDetectorException("no candidates found")
|
||||
}
|
||||
|
||||
if (nearest.evseCount < location.totalChargepoints) {
|
||||
markers = if (nearest.evseCount < location.totalChargepoints) {
|
||||
// combine related stations
|
||||
markers = markers.filter { marker ->
|
||||
markers.filter { marker ->
|
||||
distanceBetween(
|
||||
marker.coordinates.latitude,
|
||||
marker.coordinates.longitude,
|
||||
@@ -122,7 +155,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
) < maxDistance
|
||||
}
|
||||
} else {
|
||||
markers = listOf(nearest)
|
||||
listOf(nearest)
|
||||
}
|
||||
|
||||
// load details
|
||||
@@ -135,14 +168,15 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
}
|
||||
val connectorStatus = details.flatMap { it.evses }.flatMap { evse ->
|
||||
evse.connectors.map { connector ->
|
||||
Triple(connector, evse.status, evse.evseId)
|
||||
NmStatus(connector, evse.status, evse.evseId, evse.updated)
|
||||
}
|
||||
}
|
||||
|
||||
val nmConnectors = mutableMapOf<Long, Pair<Double, String>>()
|
||||
val nmStatus = mutableMapOf<Long, ChargepointStatus>()
|
||||
val nmEvseId = mutableMapOf<Long, String>()
|
||||
connectorStatus.forEach { (connector, statusStr, evseId) ->
|
||||
val nmUpdated = mutableMapOf<Long, ZonedDateTime>()
|
||||
connectorStatus.forEach { (connector, statusStr, evseId, updated) ->
|
||||
val id = connector.uid
|
||||
val power = connector.electricalProperties.getPower()
|
||||
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
||||
@@ -168,6 +202,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
nmConnectors.put(id, power to type)
|
||||
nmStatus.put(id, status)
|
||||
evseId?.let { nmEvseId[id] = it }
|
||||
updated?.let { nmUpdated[id] = it }
|
||||
}
|
||||
|
||||
val match = matchChargepoints(nmConnectors, location.chargepointsMerged)
|
||||
@@ -177,10 +212,12 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
val evseIds = if (nmEvseId.size == nmStatus.size) match.mapValues { entry ->
|
||||
entry.value.map { nmEvseId[it]!! }
|
||||
} else null
|
||||
val updated = match.mapValues { entry -> entry.value.map { nmUpdated[it]?.toInstant() } }
|
||||
return ChargeLocationStatus(
|
||||
chargepointStatus,
|
||||
"NewMotion",
|
||||
evseIds
|
||||
evseIds,
|
||||
lastChange = updated
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import net.vonforst.evmap.api.availability.tesla.ChargerAvailability
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaChargingGuestGraphQlApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaCuaApi
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
|
||||
class TeslaGuestAvailabilityDetector(
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null
|
||||
) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
|
||||
private var cuaApi = TeslaCuaApi.create(client, baseUrl)
|
||||
private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl)
|
||||
|
||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||
val results = cuaApi.getTeslaLocations()
|
||||
|
||||
val result =
|
||||
results.minByOrNull {
|
||||
if (it.latitude != null && it.longitude != null) {
|
||||
distanceBetween(
|
||||
it.latitude,
|
||||
it.longitude,
|
||||
location.coordinates.lat,
|
||||
location.coordinates.lng
|
||||
)
|
||||
} else Double.POSITIVE_INFINITY
|
||||
} ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
val resultDetails = try {
|
||||
cuaApi.getTeslaLocation(result.locationId)
|
||||
} catch (e: JsonDataException) {
|
||||
// instead of a single location, this may also return an empty JSON list []. This is hard to fix with Moshi
|
||||
if (e.message == "Expected BEGIN_OBJECT but was BEGIN_ARRAY at path \$") {
|
||||
throw AvailabilityDetectorException("no candidates found.")
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
val trtId = resultDetails.trtId?.toLongOrNull()
|
||||
?: throw AvailabilityDetectorException("charger data not available through guest API")
|
||||
|
||||
val (detailsA, guestPricing) = coroutineScope {
|
||||
val details = async {
|
||||
api.getChargingSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
|
||||
TeslaChargingGuestGraphQlApi.Identifier(
|
||||
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
|
||||
trtId
|
||||
)
|
||||
),
|
||||
TeslaChargingGuestGraphQlApi.Experience.ADHOC
|
||||
)
|
||||
)
|
||||
).data.site ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
}
|
||||
val guestPricing = async {
|
||||
api.getChargingSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
|
||||
TeslaChargingGuestGraphQlApi.Identifier(
|
||||
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
|
||||
trtId
|
||||
)
|
||||
),
|
||||
TeslaChargingGuestGraphQlApi.Experience.GUEST
|
||||
)
|
||||
)
|
||||
).data.site?.pricing
|
||||
}
|
||||
details to guestPricing
|
||||
}
|
||||
val details = detailsA.await()
|
||||
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power <= 150
|
||||
}
|
||||
if (scV2CCSConnectors.sumOf { it.count } != 0 && scV2CCSConnectors.sumOf { it.count } != scV2Connectors.sumOf { it.count }) {
|
||||
throw AvailabilityDetectorException("number of V2 connectors does not match number of V2 CCS connectors")
|
||||
}
|
||||
val scV3Connectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power > 150
|
||||
}
|
||||
if (location.totalChargepoints != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } + scV2CCSConnectors.sumOf { it.count }) throw AvailabilityDetectorException(
|
||||
"charger has unknown connectors"
|
||||
)
|
||||
|
||||
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
|
||||
// If we have just one type of charger, we can still match
|
||||
val numMissing =
|
||||
scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - detailsSorted.size
|
||||
if ((scV2Connectors.isEmpty() || scV3Connectors.isEmpty()) && numMissing > 0) {
|
||||
detailsSorted =
|
||||
detailsSorted + List(numMissing) {
|
||||
TeslaChargingGuestGraphQlApi.ChargerDetail(
|
||||
ChargerAvailability.UNKNOWN,
|
||||
""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
mutableMapOf<Chargepoint, List<TeslaChargingGuestGraphQlApi.ChargerDetail>>()
|
||||
var i = 0
|
||||
for (connector in scV2Connectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
if (scV2CCSConnectors.isNotEmpty()) {
|
||||
i = 0
|
||||
for (connector in scV2CCSConnectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
}
|
||||
for (connector in scV3Connectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||
val labelsMap = detailsMap.mapValues { it.value.map { chargers[it.id]?.label } }
|
||||
|
||||
val pricing = details.pricing.copy(memberRates = guestPricing.await()?.userRates)
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
labels = labelsMap,
|
||||
extraData = pricing
|
||||
)
|
||||
}
|
||||
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
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
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.OkHttpClient
|
||||
import java.time.Instant
|
||||
import java.util.Collections
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
|
||||
class TeslaOwnerAvailabilityDetector(
|
||||
private val client: OkHttpClient,
|
||||
private val tokenStore: TokenStore,
|
||||
private val baseUrl: String? = null
|
||||
) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
|
||||
private val authApi = TeslaAuthenticationApi.create(client, null)
|
||||
private var api: TeslaChargingOwnershipGraphQlApi? = null
|
||||
|
||||
interface TokenStore {
|
||||
var teslaRefreshToken: String?
|
||||
var teslaAccessToken: String?
|
||||
var teslaAccessTokenExpiry: Long
|
||||
}
|
||||
|
||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||
val api = initApi()
|
||||
val req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest(
|
||||
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables(
|
||||
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesArgs(
|
||||
location.coordinates.asTeslaCoord(),
|
||||
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
||||
location.coordinates.lat + coordRange,
|
||||
location.coordinates.lng - coordRange
|
||||
),
|
||||
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
||||
location.coordinates.lat - coordRange,
|
||||
location.coordinates.lng + coordRange
|
||||
),
|
||||
TeslaChargingOwnershipGraphQlApi.OpenToNonTeslasFilterValue(false)
|
||||
)
|
||||
)
|
||||
)
|
||||
val results = api.getNearbyChargingSites(
|
||||
req,
|
||||
req.operationName
|
||||
).data.charging?.nearbySites?.sitesAndDistances
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
val result =
|
||||
results.minByOrNull { it.haversineDistanceMiles.value }
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
val details = api.getChargingSiteInformation(
|
||||
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest(
|
||||
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables(
|
||||
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.id.text),
|
||||
TeslaChargingOwnershipGraphQlApi.VehicleMakeType.NON_TESLA
|
||||
)
|
||||
)
|
||||
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power <= 150
|
||||
}
|
||||
if (scV2CCSConnectors.sumOf { it.count } != 0 && scV2CCSConnectors.sumOf { it.count } != scV2Connectors.sumOf { it.count }) {
|
||||
throw AvailabilityDetectorException("number of V2 connectors does not match number of V2 CCS connectors")
|
||||
}
|
||||
val scV3Connectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power > 150
|
||||
}
|
||||
if (location.totalChargepoints != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } + scV2CCSConnectors.sumOf { it.count }) throw AvailabilityDetectorException(
|
||||
"charger has unknown connectors"
|
||||
)
|
||||
|
||||
val chargerDetails = details.siteDynamic.chargerDetails
|
||||
val chargers = details.siteStatic.chargers.associateBy { it.id }
|
||||
var detailsSorted = chargerDetails
|
||||
.sortedBy { c -> c.charger.labelLetter ?: chargers[c.charger.id]?.labelLetter }
|
||||
.sortedBy { c -> c.charger.labelNumber ?: chargers[c.charger.id]?.labelNumber }
|
||||
if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
|
||||
// apparently some connectors are missing in Tesla data
|
||||
// If we have just one type of charger, we can still match
|
||||
val numMissing =
|
||||
scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - detailsSorted.size
|
||||
if ((scV2Connectors.isEmpty() || scV3Connectors.isEmpty()) && numMissing > 0) {
|
||||
detailsSorted =
|
||||
detailsSorted + List(numMissing) {
|
||||
TeslaChargingOwnershipGraphQlApi.ChargerDetail(
|
||||
ChargerAvailability.UNKNOWN,
|
||||
TeslaChargingOwnershipGraphQlApi.ChargerId(
|
||||
TeslaChargingOwnershipGraphQlApi.Text(""),
|
||||
null,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
emptyMap<Chargepoint, List<TeslaChargingOwnershipGraphQlApi.ChargerDetail>>().toMutableMap()
|
||||
var i = 0
|
||||
for (connector in scV2Connectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
if (scV2CCSConnectors.isNotEmpty()) {
|
||||
i = 0
|
||||
for (connector in scV2CCSConnectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
}
|
||||
for (connector in scV3Connectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
|
||||
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
|
||||
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
|
||||
val data = cph.data.toMutableList()
|
||||
Collections.rotate(data, -index)
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||
val labelsMap = detailsMap.mapValues {
|
||||
it.value.map {
|
||||
it.charger.label?.value ?: chargers[it.charger.id]?.label?.value
|
||||
}
|
||||
}
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
labels = labelsMap,
|
||||
congestionHistogram = congestionHistogram,
|
||||
extraData = details.pricing
|
||||
)
|
||||
}
|
||||
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initApi(): TeslaChargingOwnershipGraphQlApi {
|
||||
|
||||
return api ?: run {
|
||||
val newApi = TeslaChargingOwnershipGraphQlApi.create(client, baseUrl) {
|
||||
val now = Instant.now().epochSecond
|
||||
val token =
|
||||
tokenStore.teslaAccessToken.takeIf { tokenStore.teslaAccessTokenExpiry > now }
|
||||
?: run {
|
||||
val refreshToken = tokenStore.teslaRefreshToken
|
||||
?: throw NotSignedInException()
|
||||
val response =
|
||||
authApi.getToken(
|
||||
TeslaAuthenticationApi.RefreshTokenRequest(
|
||||
refreshToken
|
||||
)
|
||||
)
|
||||
tokenStore.teslaAccessToken = response.accessToken
|
||||
tokenStore.teslaAccessTokenExpiry = now + response.expiresIn
|
||||
response.accessToken
|
||||
}
|
||||
token
|
||||
}
|
||||
api = newApi
|
||||
newApi
|
||||
}
|
||||
}
|
||||
|
||||
fun isSignedIn() = tokenStore.teslaRefreshToken != null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package net.vonforst.evmap.api.availability.tesla
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.ToJson
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import java.time.LocalTime
|
||||
|
||||
sealed class GraphQlRequest {
|
||||
abstract val operationName: String
|
||||
abstract val query: String
|
||||
abstract val variables: Any?
|
||||
}
|
||||
|
||||
fun Coordinate.asTeslaCoord() =
|
||||
TeslaChargingOwnershipGraphQlApi.Coordinate(this.lat, this.lng)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Outage(val message: String /* TODO: */)
|
||||
|
||||
enum class ChargerAvailability {
|
||||
@Json(name = "CHARGER_AVAILABILITY_AVAILABLE")
|
||||
AVAILABLE,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_OCCUPIED")
|
||||
OCCUPIED,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_DOWN")
|
||||
DOWN,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_UNKNOWN")
|
||||
UNKNOWN;
|
||||
|
||||
fun toStatus() = when (this) {
|
||||
AVAILABLE -> ChargepointStatus.AVAILABLE
|
||||
OCCUPIED -> ChargepointStatus.OCCUPIED
|
||||
DOWN -> ChargepointStatus.FAULTED
|
||||
UNKNOWN -> ChargepointStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricing(
|
||||
val canDisplayCombinedComparison: Boolean?,
|
||||
val hasMSPPricing: Boolean?,
|
||||
val hasMembershipPricing: Boolean?,
|
||||
val memberRates: Rates?, // rates for Tesla drivers & non-Tesla drivers with subscription
|
||||
val userRates: Rates? // rates without subscription
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Rates(
|
||||
val activePricebook: Pricebook
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricebook(
|
||||
val charging: PricebookDetails,
|
||||
val parking: PricebookDetails,
|
||||
val priceBookID: Long?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PricebookDetails(
|
||||
val bucketUom: String, // unit of measurement for buckets (typically "kw")
|
||||
val buckets: List<Bucket>, // buckets of charging power (used for minute-based pricing)
|
||||
val currencyCode: String,
|
||||
val programType: String,
|
||||
val rates: List<Double>,
|
||||
val touRates: TouRates,
|
||||
val uom: String, // unit of measurement ("kwh" or "min")
|
||||
val vehicleMakeType: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Bucket(
|
||||
val start: Int,
|
||||
val end: Int
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TouRates(
|
||||
val activeRatesByTime: List<ActiveRatesByTime>,
|
||||
val enabled: Boolean
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActiveRatesByTime(
|
||||
val startTime: LocalTime,
|
||||
val endTime: LocalTime,
|
||||
val rates: List<Double>
|
||||
)
|
||||
|
||||
internal class LocalTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): LocalTime? = value?.let {
|
||||
if (it == "24:00") LocalTime.MAX else LocalTime.parse(it)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: LocalTime?): String? = value?.toString()
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
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 {
|
||||
@GET("tesla-locations")
|
||||
suspend fun getTeslaLocations(
|
||||
@Query("translate") translate: String = "en_US",
|
||||
@Query("usetrt") usetrt: Boolean = true,
|
||||
): List<TeslaLocation>
|
||||
|
||||
@GET("tesla-location")
|
||||
suspend fun getTeslaLocation(
|
||||
@Query("id") id: String,
|
||||
@Query("translate") translate: String = "en_US",
|
||||
@Query("usetrt") usetrt: Boolean = true
|
||||
): TeslaLocation
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TeslaLocation(
|
||||
val latitude: Double?,
|
||||
val longitude: Double?,
|
||||
@Json(name = "location_id") val locationId: String,
|
||||
val title: String?,
|
||||
@Json(name = "location_type") val locationType: List<String>,
|
||||
val trtId: String?
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null
|
||||
): TeslaCuaApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
// increase cache duration to 24h (useful for the large getTeslaLocations request)
|
||||
val request = chain.request().newBuilder()
|
||||
.cacheControl(CacheControl.Builder().maxStale(24, TimeUnit.HOURS).build())
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}.build()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://www.tesla.com/cua-api/")
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(LocalTimeAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(TeslaCuaApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TeslaChargingGuestGraphQlApi {
|
||||
@POST("graphql")
|
||||
suspend fun getChargingSiteDetails(
|
||||
@Body request: GetChargingSiteDetailsRequest,
|
||||
@Query("operationName") operationName: String = "getGuestChargingSiteDetails"
|
||||
): GetChargingSiteDetailsResponse
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsRequest(
|
||||
override val variables: GetChargingSiteInformationVariables,
|
||||
override val operationName: String = "getGuestChargingSiteDetails",
|
||||
override val query: String =
|
||||
"\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 GetChargingSiteInformationVariables(
|
||||
val identifier: Identifier,
|
||||
val experience: Experience,
|
||||
val deviceLocale: String = "de-DE",
|
||||
)
|
||||
|
||||
enum class Experience {
|
||||
ADHOC, GUEST
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Identifier(
|
||||
val siteId: ChargingSiteIdentifier
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteIdentifier(
|
||||
val id: Long,
|
||||
val siteType: SiteType = SiteType.SUPERCHARGER
|
||||
)
|
||||
|
||||
enum class SiteType {
|
||||
@Json(name = "SITE_TYPE_SUPERCHARGER")
|
||||
SUPERCHARGER
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseData)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponseData(val site: ChargingSiteInformation?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteInformation(
|
||||
val activeOutages: List<Outage>?,
|
||||
val chargers: List<ChargerId>,
|
||||
val chargersAvailable: ChargersAvailable,
|
||||
val id: Long,
|
||||
val maxPowerKw: Int,
|
||||
val name: String,
|
||||
val pricing: Pricing,
|
||||
val publicStallCount: Int
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerId(
|
||||
val id: String,
|
||||
val label: String?,
|
||||
) {
|
||||
val labelNumber
|
||||
get() = label?.replace(Regex("""\D"""), "")?.toInt()
|
||||
val labelLetter
|
||||
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,
|
||||
baseUrl: String? = null
|
||||
): TeslaChargingGuestGraphQlApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://www.tesla.com/de_DE/charging/guest/api/")
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(LocalTimeAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(TeslaChargingGuestGraphQlApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
package net.vonforst.evmap.api.availability.tesla
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
@@ -19,14 +14,9 @@ import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
import java.util.Collections
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
|
||||
interface TeslaAuthenticationApi {
|
||||
@POST("oauth2/v3/token")
|
||||
@@ -157,7 +147,7 @@ interface TeslaOwnerApi {
|
||||
}
|
||||
}
|
||||
|
||||
interface TeslaGraphQlApi {
|
||||
interface TeslaChargingOwnershipGraphQlApi {
|
||||
@POST("/graphql")
|
||||
suspend fun getNearbyChargingSites(
|
||||
@Body request: GetNearbyChargingSitesRequest,
|
||||
@@ -238,12 +228,6 @@ interface TeslaGraphQlApi {
|
||||
TESLA, NON_TESLA
|
||||
}
|
||||
|
||||
sealed class GraphQlRequest {
|
||||
abstract val operationName: String
|
||||
abstract val query: String
|
||||
abstract val variables: Any?
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesResponse(val data: GetNearbyChargingSitesResponseData)
|
||||
|
||||
@@ -271,15 +255,6 @@ interface TeslaGraphQlApi {
|
||||
// TODO: siteType, accessType
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Outage(val message: String /* TODO: */)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Value<T : Any>(val value: T)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Text(val text: String)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationResponse(val data: GetChargingSiteInformationResponseData)
|
||||
|
||||
@@ -307,12 +282,6 @@ interface TeslaGraphQlApi {
|
||||
val waitEstimateBucket: WaitEstimateBucket
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerDetail(
|
||||
val availability: ChargerAvailability,
|
||||
val charger: ChargerId
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerId(
|
||||
val id: Text,
|
||||
@@ -325,6 +294,12 @@ interface TeslaGraphQlApi {
|
||||
get() = label?.value?.replace(Regex("""\d"""), "")
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerDetail(
|
||||
val availability: ChargerAvailability,
|
||||
val charger: ChargerId
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SiteStatic(
|
||||
val accessCode: Value<String>?,
|
||||
@@ -342,58 +317,6 @@ interface TeslaGraphQlApi {
|
||||
// TODO: siteType, accessType, address, amenities, timeZone
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricing(
|
||||
val canDisplayCombinedComparison: Boolean,
|
||||
val hasMSPPricing: Boolean,
|
||||
val hasMembershipPricing: Boolean,
|
||||
val memberRates: Rates?, // rates for Tesla drivers & non-Tesla drivers with subscription
|
||||
val userRates: Rates? // rates without subscription
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Rates(
|
||||
val activePricebook: Pricebook
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricebook(
|
||||
val charging: PricebookDetails,
|
||||
val parking: PricebookDetails,
|
||||
val priceBookID: Long
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PricebookDetails(
|
||||
val bucketUom: String, // unit of measurement for buckets (typically "kw")
|
||||
val buckets: List<Bucket>, // buckets of charging power (used for minute-based pricing)
|
||||
val currencyCode: String,
|
||||
val programType: String,
|
||||
val rates: List<Double>,
|
||||
val touRates: TouRates,
|
||||
val uom: String, // unit of measurement ("kwh" or "min")
|
||||
val vehicleMakeType: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Bucket(
|
||||
val start: Int,
|
||||
val end: Int
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TouRates(
|
||||
val activeRatesByTime: List<ActiveRatesByTime>,
|
||||
val enabled: Boolean
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActiveRatesByTime(
|
||||
val startTime: LocalTime,
|
||||
val endTime: LocalTime,
|
||||
val rates: List<Double>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CongestionPriceHistogram(
|
||||
val data: List<Double>,
|
||||
@@ -406,25 +329,11 @@ interface TeslaGraphQlApi {
|
||||
val label: String // "1AM", "2AM", etc.
|
||||
)
|
||||
|
||||
enum class ChargerAvailability {
|
||||
@Json(name = "CHARGER_AVAILABILITY_AVAILABLE")
|
||||
AVAILABLE,
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Value<T : Any>(val value: T)
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_OCCUPIED")
|
||||
OCCUPIED,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_DOWN")
|
||||
DOWN,
|
||||
@Json(name = "CHARGER_AVAILABILITY_UNKNOWN")
|
||||
UNKNOWN;
|
||||
|
||||
fun toStatus() = when (this) {
|
||||
AVAILABLE -> ChargepointStatus.AVAILABLE
|
||||
OCCUPIED -> ChargepointStatus.OCCUPIED
|
||||
DOWN -> ChargepointStatus.FAULTED
|
||||
UNKNOWN -> ChargepointStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Text(val text: String)
|
||||
|
||||
enum class WaitEstimateBucket {
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_NO_WAIT")
|
||||
@@ -457,7 +366,7 @@ interface TeslaGraphQlApi {
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null,
|
||||
token: suspend () -> String
|
||||
): TeslaGraphQlApi {
|
||||
): TeslaChargingOwnershipGraphQlApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val t = runBlocking { token() }
|
||||
@@ -479,193 +388,7 @@ interface TeslaGraphQlApi {
|
||||
)
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(TeslaGraphQlApi::class.java)
|
||||
return retrofit.create(TeslaChargingOwnershipGraphQlApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): LocalTime? = value?.let {
|
||||
if (it == "24:00") LocalTime.MAX else LocalTime.parse(it)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: LocalTime?): String? = value?.toString()
|
||||
}
|
||||
|
||||
fun Coordinate.asTeslaCoord() =
|
||||
TeslaGraphQlApi.Coordinate(this.lat, this.lng)
|
||||
|
||||
class TeslaAvailabilityDetector(
|
||||
private val client: OkHttpClient,
|
||||
private val tokenStore: TokenStore,
|
||||
private val baseUrl: String? = null
|
||||
) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
|
||||
private val authApi = TeslaAuthenticationApi.create(client, null)
|
||||
private var api: TeslaGraphQlApi? = null
|
||||
|
||||
interface TokenStore {
|
||||
var teslaRefreshToken: String?
|
||||
var teslaAccessToken: String?
|
||||
var teslaAccessTokenExpiry: Long
|
||||
}
|
||||
|
||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||
val api = initApi()
|
||||
val req = TeslaGraphQlApi.GetNearbyChargingSitesRequest(
|
||||
TeslaGraphQlApi.GetNearbyChargingSitesVariables(
|
||||
TeslaGraphQlApi.GetNearbyChargingSitesArgs(
|
||||
location.coordinates.asTeslaCoord(),
|
||||
TeslaGraphQlApi.Coordinate(
|
||||
location.coordinates.lat + coordRange,
|
||||
location.coordinates.lng - coordRange
|
||||
),
|
||||
TeslaGraphQlApi.Coordinate(
|
||||
location.coordinates.lat - coordRange,
|
||||
location.coordinates.lng + coordRange
|
||||
),
|
||||
TeslaGraphQlApi.OpenToNonTeslasFilterValue(false)
|
||||
)
|
||||
)
|
||||
)
|
||||
val results = api.getNearbyChargingSites(
|
||||
req,
|
||||
req.operationName
|
||||
).data.charging?.nearbySites?.sitesAndDistances
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
val result =
|
||||
results.minByOrNull { it.haversineDistanceMiles.value }
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
val details = api.getChargingSiteInformation(
|
||||
TeslaGraphQlApi.GetChargingSiteInformationRequest(
|
||||
TeslaGraphQlApi.GetChargingSiteInformationVariables(
|
||||
TeslaGraphQlApi.ChargingSiteIdentifier(result.id.text),
|
||||
TeslaGraphQlApi.VehicleMakeType.NON_TESLA
|
||||
)
|
||||
)
|
||||
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power <= 150
|
||||
}
|
||||
if (scV2CCSConnectors.sumOf { it.count } != 0 && scV2CCSConnectors.sumOf { it.count } != scV2Connectors.sumOf { it.count }) {
|
||||
throw AvailabilityDetectorException("number of V2 connectors does not match number of V2 CCS connectors")
|
||||
}
|
||||
val scV3Connectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power > 150
|
||||
}
|
||||
if (location.totalChargepoints != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } + scV2CCSConnectors.sumOf { it.count }) throw AvailabilityDetectorException(
|
||||
"charger has unknown connectors"
|
||||
)
|
||||
|
||||
var statusSorted = details.siteDynamic.chargerDetails
|
||||
.sortedBy { c ->
|
||||
c.charger.labelLetter
|
||||
?: details.siteStatic.chargers.find { it.id == c.charger.id }?.labelLetter
|
||||
}
|
||||
.sortedBy { c ->
|
||||
c.charger.labelNumber
|
||||
?: details.siteStatic.chargers.find { it.id == c.charger.id }?.labelNumber
|
||||
}
|
||||
.map { it.availability }
|
||||
if (statusSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
|
||||
// apparently some connectors are missing in Tesla data
|
||||
// If we have just one type of charger, we can still match
|
||||
val numMissing =
|
||||
scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - statusSorted.size
|
||||
if (scV2Connectors.isEmpty() || scV3Connectors.isEmpty() && numMissing > 0) {
|
||||
statusSorted =
|
||||
statusSorted + List(numMissing) { TeslaGraphQlApi.ChargerAvailability.UNKNOWN }
|
||||
} else {
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
}
|
||||
|
||||
val statusMap = emptyMap<Chargepoint, List<ChargepointStatus>>().toMutableMap()
|
||||
var i = 0
|
||||
for (connector in scV2Connectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
if (scV2CCSConnectors.isNotEmpty()) {
|
||||
i = 0
|
||||
for (connector in scV2CCSConnectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
}
|
||||
for (connector in scV3Connectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
|
||||
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
|
||||
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
|
||||
val data = cph.data.toMutableList()
|
||||
Collections.rotate(data, -index)
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
congestionHistogram = congestionHistogram,
|
||||
extraData = details.pricing
|
||||
)
|
||||
}
|
||||
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initApi(): TeslaGraphQlApi {
|
||||
|
||||
return api ?: run {
|
||||
val newApi = TeslaGraphQlApi.create(client, baseUrl) {
|
||||
val now = Instant.now().epochSecond
|
||||
val token =
|
||||
tokenStore.teslaAccessToken.takeIf { tokenStore.teslaAccessTokenExpiry > now }
|
||||
?: run {
|
||||
val refreshToken = tokenStore.teslaRefreshToken
|
||||
?: throw IOException("not signed in")
|
||||
val response =
|
||||
authApi.getToken(
|
||||
TeslaAuthenticationApi.RefreshTokenRequest(
|
||||
refreshToken
|
||||
)
|
||||
)
|
||||
tokenStore.teslaAccessToken = response.accessToken
|
||||
tokenStore.teslaAccessTokenExpiry = now + response.expiresIn
|
||||
response.accessToken
|
||||
}
|
||||
token
|
||||
}
|
||||
api = newApi
|
||||
newApi
|
||||
}
|
||||
}
|
||||
|
||||
fun isSignedIn() = tokenStore.teslaRefreshToken != null
|
||||
|
||||
}
|
||||
@@ -12,16 +12,41 @@ import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.*
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.ChargepointList
|
||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.mapPower
|
||||
import net.vonforst.evmap.api.mapPowerInverse
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.powerSteps
|
||||
import net.vonforst.evmap.model.BooleanFilter
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.Filter
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterValues
|
||||
import net.vonforst.evmap.model.MultipleChoiceFilter
|
||||
import net.vonforst.evmap.model.MultipleChoiceFilterValue
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.model.SliderFilter
|
||||
import net.vonforst.evmap.model.getBooleanValue
|
||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.model.getSliderValue
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.getClusterDistance
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.*
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
|
||||
@@ -122,6 +147,8 @@ interface GoingElectricApi {
|
||||
}
|
||||
}
|
||||
|
||||
private const val STATUS_OK = "ok"
|
||||
|
||||
class GoingElectricApiWrapper(
|
||||
val apikey: String,
|
||||
baseurl: String = "https://api.goingelectric.de",
|
||||
@@ -210,15 +237,17 @@ class GoingElectricApiWrapper(
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
if (!response.isSuccessful || response.body()!!.status != STATUS_OK) {
|
||||
return Resource.error(response.message(), null)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
data.addAll(body.chargelocations!!)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
@@ -305,15 +334,17 @@ class GoingElectricApiWrapper(
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
if (!response.isSuccessful || response.body()!!.status != STATUS_OK) {
|
||||
return Resource.error(response.message(), null)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
data.addAll(body.chargelocations!!)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
@@ -388,9 +419,9 @@ class GoingElectricApiWrapper(
|
||||
): Resource<ChargeLocation> {
|
||||
try {
|
||||
val response = api.getChargepointDetail(id)
|
||||
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
|
||||
return if (response.isSuccessful && response.body()!!.status == STATUS_OK && response.body()!!.chargelocations!!.size == 1) {
|
||||
Resource.success(
|
||||
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
|
||||
(response.body()!!.chargelocations!![0] as GEChargeLocation).convert(
|
||||
apikey, true
|
||||
)
|
||||
)
|
||||
@@ -399,6 +430,8 @@ class GoingElectricApiWrapper(
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,19 +449,24 @@ class GoingElectricApiWrapper(
|
||||
|
||||
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
|
||||
|
||||
if (responses.map { it.isSuccessful }.all { it }) {
|
||||
if (responses.map { it.isSuccessful }.all { it }
|
||||
&& plugsResponse.body()!!.status == STATUS_OK
|
||||
&& chargeCardsResponse.body()!!.status == STATUS_OK
|
||||
&& networksResponse.body()!!.status == STATUS_OK) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result,
|
||||
networksResponse.body()!!.result,
|
||||
chargeCardsResponse.body()!!.result
|
||||
plugsResponse.body()!!.result!!,
|
||||
networksResponse.body()!!.result!!,
|
||||
chargeCardsResponse.body()!!.result!!
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
|
||||
Resource.error(responses.find { !it.isSuccessful }?.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,20 +27,20 @@ import java.time.LocalTime
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GEChargepointList(
|
||||
val status: String,
|
||||
val chargelocations: List<GEChargepointListItem>,
|
||||
val chargelocations: List<GEChargepointListItem>?,
|
||||
@JsonObjectOrFalse val startkey: Int?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GEStringList(
|
||||
val status: String,
|
||||
val result: List<String>
|
||||
val result: List<String>?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GEChargeCardList(
|
||||
val status: String,
|
||||
val result: List<GEChargeCard>
|
||||
val result: List<GEChargeCard>?
|
||||
)
|
||||
|
||||
sealed class GEChargepointListItem {
|
||||
|
||||
@@ -8,11 +8,30 @@ import com.squareup.moshi.Moshi
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.*
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.ChargepointList
|
||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.mapPower
|
||||
import net.vonforst.evmap.api.mapPowerInverse
|
||||
import net.vonforst.evmap.api.powerSteps
|
||||
import net.vonforst.evmap.model.BooleanFilter
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.Filter
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterValues
|
||||
import net.vonforst.evmap.model.MultipleChoiceFilter
|
||||
import net.vonforst.evmap.model.MultipleChoiceFilterValue
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.model.SliderFilter
|
||||
import net.vonforst.evmap.model.getBooleanValue
|
||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.model.getSliderValue
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
@@ -168,6 +187,8 @@ class OpenChargeMapApiWrapper(
|
||||
return Resource.success(ChargepointList(result, data.size < maxResults))
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +280,8 @@ class OpenChargeMapApiWrapper(
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +295,8 @@ class OpenChargeMapApiWrapper(
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,9 +408,7 @@ class OpenChargeMapApiWrapper(
|
||||
}
|
||||
|
||||
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
|
||||
val operators = filters.getMultipleChoiceValue("operators")
|
||||
return (operators != null && !operators.all)
|
||||
// TODO: it would be possible to implement this without requiring details if we extended the data structure to also save the operator ID in the DB
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,7 +63,7 @@ data class OCMChargepoint(
|
||||
Coordinate(addressInfo.latitude, addressInfo.longitude),
|
||||
addressInfo.toAddress(refData),
|
||||
connections.map { it.convert(refData) },
|
||||
operatorInfo?.title,
|
||||
operatorInfo?.title ?: refData.operators.find { it.id == operatorId }?.title,
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
convertFaultReport(),
|
||||
@@ -159,7 +159,9 @@ data class OCMConnection(
|
||||
fun convert(refData: OCMReferenceData) = Chargepoint(
|
||||
convertConnectionTypeFromOCM(connectionTypeId, refData),
|
||||
power,
|
||||
quantity ?: 1
|
||||
quantity ?: 1,
|
||||
voltage?.toDouble(),
|
||||
amps?.toDouble()
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -28,6 +28,7 @@ import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import net.vonforst.evmap.ui.time
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -303,6 +304,15 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.chargeprice_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} catch (e: NoVehicleSelectedException) {
|
||||
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
|
||||
invalidate()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.*
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.text.SpannableString
|
||||
@@ -11,8 +15,17 @@ import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
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.ForegroundCarColorSpan
|
||||
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.core.graphics.drawable.IconCompat
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.text.HtmlCompat
|
||||
@@ -23,12 +36,17 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.EXTRA_CHARGER_ID
|
||||
import net.vonforst.evmap.EXTRA_LAT
|
||||
import net.vonforst.evmap.EXTRA_LON
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.formatTeslaParkingFee
|
||||
import net.vonforst.evmap.adapter.formatTeslaPricing
|
||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
|
||||
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
|
||||
@@ -41,6 +59,7 @@ import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Cost
|
||||
import net.vonforst.evmap.model.FaultReport
|
||||
import net.vonforst.evmap.model.Favorite
|
||||
import net.vonforst.evmap.plus
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
@@ -52,7 +71,6 @@ import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -132,7 +150,16 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.auto_prices))
|
||||
.setOnClickListener {
|
||||
screenManager.push(ChargepriceScreen(carContext, charger))
|
||||
if (prefs.chargepriceNativeIntegration) {
|
||||
screenManager.push(ChargepriceScreen(carContext, charger))
|
||||
} else {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
carContext.startActivity(intent)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
}
|
||||
@@ -330,7 +357,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
}.build())
|
||||
}
|
||||
if (rows.count() < maxRows && teslaSupported) {
|
||||
val teslaPricing = availability?.extraData as? TeslaGraphQlApi.Pricing
|
||||
val teslaPricing = availability?.extraData as? Pricing
|
||||
rows.add(3, Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.cost))
|
||||
teslaPricing?.let {
|
||||
@@ -543,7 +570,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
val img =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
(carContext.imageLoader.execute(request).drawable as? BitmapDrawable)?.bitmap ?: return@let
|
||||
|
||||
// draw icon on top of image
|
||||
val icon = iconGen.getBitmap(
|
||||
|
||||
@@ -45,6 +45,7 @@ import net.vonforst.evmap.utils.headingDiff
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
import net.vonforst.evmap.viewmodel.filtersWithValue
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@@ -502,6 +503,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
} catch (e: IOException) {
|
||||
loadingError = true
|
||||
invalidate()
|
||||
} catch (e: HttpException) {
|
||||
loadingError = true
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.R
|
||||
import okio.IOException
|
||||
import retrofit2.HttpException
|
||||
|
||||
abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
|
||||
SearchTemplate.SearchCallback {
|
||||
@@ -30,14 +31,9 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
|
||||
filterList()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.generic_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
screenManager.pop()
|
||||
}
|
||||
showLoadingError()
|
||||
} catch (e: HttpException) {
|
||||
showLoadingError()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +72,17 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
|
||||
}.build()
|
||||
}
|
||||
|
||||
private suspend fun showLoadingError() {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.generic_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
screenManager.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterList() {
|
||||
currentList = fullList?.let {
|
||||
it.sortedBy { getLabel(it).lowercase() }
|
||||
|
||||
@@ -8,31 +8,44 @@ import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.IInterface
|
||||
import android.text.Html
|
||||
import androidx.annotation.StringRes
|
||||
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.model.*
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.GridItem
|
||||
import androidx.car.app.model.GridTemplate
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
import androidx.car.app.model.ParkedOnlyOnClickListener
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.SectionedItemList
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.Toggle
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.TeslaAuthenticationApi
|
||||
import net.vonforst.evmap.api.availability.TeslaOwnerApi
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.EXTRA_DONATE
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
||||
import net.vonforst.evmap.getPackageInfoCompat
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
@@ -412,12 +425,21 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
setTitle(carContext.getString(R.string.settings_chargeprice))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_native_integration))
|
||||
addText(carContext.getString(if (prefs.chargepriceNativeIntegration) R.string.pref_chargeprice_native_integration_on else R.string.pref_chargeprice_native_integration_off))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceNativeIntegration = it
|
||||
invalidate()
|
||||
}.setChecked(prefs.chargepriceNativeIntegration).build())
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_my_vehicle))
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectVehiclesScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_my_tariffs))
|
||||
@@ -441,6 +463,7 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
}
|
||||
)
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
|
||||
@@ -458,6 +481,7 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectChargingRangeScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_currency))
|
||||
@@ -473,27 +497,31 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectCurrencyScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_no_base_fee))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceNoBaseFee = it
|
||||
}.setChecked(prefs.chargepriceNoBaseFee).build())
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs))
|
||||
addText(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs_summary))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceShowProviderCustomerTariffs = it
|
||||
}.setChecked(prefs.chargepriceShowProviderCustomerTariffs).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
if (maxRows > 6) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs))
|
||||
addText(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs_summary))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceShowProviderCustomerTariffs = it
|
||||
}.setChecked(prefs.chargepriceShowProviderCustomerTariffs).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load))
|
||||
addText(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load_summary))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceAllowUnbalancedLoad = it
|
||||
}.setChecked(prefs.chargepriceAllowUnbalancedLoad).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
}
|
||||
}.build())
|
||||
|
||||
@@ -62,7 +62,7 @@ class ChargepriceFragment : Fragment() {
|
||||
if (savedInstanceState == null) {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
prefs.chargepriceCounter += 1
|
||||
if ((prefs.chargepriceCounter - 30).mod(50) == 0) {
|
||||
if ((prefs.chargepriceCounter).mod(30) == 0) {
|
||||
showDonationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.ConnectorDetailsAdapter
|
||||
import net.vonforst.evmap.adapter.SingleViewAdapter
|
||||
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(
|
||||
val binding: DialogConnectorDetailsBinding,
|
||||
context: Context,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
private val headerBinding: DialogConnectorDetailsHeaderBinding
|
||||
private val detailsAdapter = ConnectorDetailsAdapter()
|
||||
|
||||
init {
|
||||
binding.list.apply {
|
||||
itemAnimator = null
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
}
|
||||
headerBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.dialog_connector_details_header, binding.list, false
|
||||
)
|
||||
binding.list.adapter = ConcatAdapter(
|
||||
SingleViewAdapter(headerBinding.root),
|
||||
detailsAdapter
|
||||
)
|
||||
binding.btnClose.setOnClickListener {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
fun setData(cp: Chargepoint, status: ChargeLocationStatus?) {
|
||||
val cpStatus = status?.status?.get(cp)
|
||||
val items = if (status != null) {
|
||||
List(cp.count) { i ->
|
||||
ConnectorDetailsAdapter.ConnectorDetails(
|
||||
cpStatus?.get(i),
|
||||
status.evseIds?.get(cp)?.get(i),
|
||||
status.labels?.get(cp)?.get(i),
|
||||
status.lastChange?.get(cp)?.get(i)
|
||||
)
|
||||
}.sortedBy { it.evseId ?: it.label }
|
||||
} else emptyList()
|
||||
detailsAdapter.submitList(items)
|
||||
|
||||
headerBinding.divider.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
||||
headerBinding.item = ConnectorAdapter.ChargepointWithAvailability(cp, cpStatus)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
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.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.databinding.DataBindingUtil
|
||||
@@ -108,31 +113,19 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveProfile(error: Boolean = false) {
|
||||
showEditTextDialog(requireContext()) { dialog, input ->
|
||||
private fun saveProfile() {
|
||||
showEditTextDialog(requireContext(), { dialog, input ->
|
||||
vm.filterProfile.value?.let { profile ->
|
||||
input.setText(profile.name)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
input.error = getString(R.string.required)
|
||||
}
|
||||
|
||||
dialog.setTitle(R.string.save_as_profile)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (input.text.isBlank()) {
|
||||
saveProfile(true)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
vm.saveAsProfile(input.text.toString())
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
}, {
|
||||
lifecycleScope.launch {
|
||||
vm.saveAsProfile(it)
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -183,20 +183,16 @@ class FilterProfilesFragment : Fragment() {
|
||||
adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
|
||||
delete(fp)
|
||||
}, onRename = { fp ->
|
||||
showEditTextDialog(requireContext()) { dialog, input ->
|
||||
showEditTextDialog(requireContext(), { dialog, input ->
|
||||
input.setText(fp.name)
|
||||
|
||||
dialog.setTitle(R.string.rename)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
vm.update(fp.copy(name = input.text.toString()))
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
}, {
|
||||
lifecycleScope.launch {
|
||||
vm.update(fp.copy(name = it))
|
||||
}
|
||||
})
|
||||
})
|
||||
binding.filterProfilesList.apply {
|
||||
this.adapter = this@FilterProfilesFragment.adapter
|
||||
|
||||
@@ -11,7 +11,14 @@ import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.KeyListener
|
||||
import android.view.*
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ImageView
|
||||
@@ -23,7 +30,13 @@ import androidx.annotation.RequiresPermission
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.view.*
|
||||
import androidx.core.view.MenuCompat
|
||||
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
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -31,7 +44,6 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
@@ -53,10 +65,16 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
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.*
|
||||
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
|
||||
@@ -71,6 +89,7 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.bold
|
||||
@@ -78,18 +97,38 @@ import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.location.Priority
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocationCluster
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
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.navigation.safeNavigate
|
||||
import net.vonforst.evmap.shouldUseImperialUnits
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.*
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||
import net.vonforst.evmap.ui.HideOnScrollFabBehavior
|
||||
import net.vonforst.evmap.ui.MarkerAnimator
|
||||
import net.vonforst.evmap.ui.chargerZ
|
||||
import net.vonforst.evmap.ui.clusterZ
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.ui.placeSearchZ
|
||||
import net.vonforst.evmap.ui.setTouchModal
|
||||
import net.vonforst.evmap.utils.boundingBox
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
import net.vonforst.evmap.utils.checkFineLocationPermission
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import net.vonforst.evmap.viewmodel.GalleryViewModel
|
||||
import net.vonforst.evmap.viewmodel.MapPosition
|
||||
import net.vonforst.evmap.viewmodel.MapViewModel
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.contains
|
||||
@@ -107,6 +146,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var requestingLocationUpdates = false
|
||||
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()
|
||||
private var clusterMarkers: List<Marker> = emptyList()
|
||||
@@ -115,6 +155,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
private var previousChargepointIds: Set<Long>? = null
|
||||
private var mapTopPadding: Int = 0
|
||||
private var popupMenu: PopupMenu? = null
|
||||
|
||||
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
@@ -128,6 +169,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
return
|
||||
}
|
||||
|
||||
if (vm.selectedChargepoint.value != null) {
|
||||
closeConnectorDetailsDialog()
|
||||
vm.selectedChargepoint.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.search.hasFocus()) {
|
||||
removeSearchFocus()
|
||||
}
|
||||
@@ -266,8 +313,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
binding.detailView.topPart.doOnNextLayout {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
|
||||
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
|
||||
}
|
||||
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
|
||||
binding.detailView.connectorDetails
|
||||
|
||||
setupObservers()
|
||||
setupClickListeners()
|
||||
@@ -279,7 +328,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
if (prefs.appStartCounter > 5 && !prefs.opensourceDonationsDialogShown) {
|
||||
if (prefs.appStartCounter > 5 && Duration.between(
|
||||
prefs.opensourceDonationsDialogLastShown,
|
||||
Instant.now()
|
||||
) > Duration.ofDays(30)
|
||||
) {
|
||||
try {
|
||||
findNavController().safeNavigate(MapFragmentDirections.actionMapToOpensourceDonations())
|
||||
} catch (ignored: IllegalArgumentException) {
|
||||
@@ -321,6 +374,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.appLogo.root.visibility = View.GONE
|
||||
binding.search.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
detailsDialog =
|
||||
ConnectorDetailsDialog(binding.detailView.connectorDetails, requireContext()) {
|
||||
closeConnectorDetailsDialog()
|
||||
vm.selectedChargepoint.value = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -377,12 +436,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
val extras =
|
||||
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
|
||||
findNavController().safeNavigate(
|
||||
MapFragmentDirections.actionMapToChargepriceFragment(charger),
|
||||
extras
|
||||
)
|
||||
if (prefs.chargepriceNativeIntegration) {
|
||||
val extras =
|
||||
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
|
||||
findNavController().safeNavigate(
|
||||
MapFragmentDirections.actionMapToChargepriceFragment(charger),
|
||||
extras
|
||||
)
|
||||
} else {
|
||||
(activity as? MapsActivity)?.openUrl(ChargepriceApi.getPoiUrl(charger), false)
|
||||
}
|
||||
}
|
||||
binding.detailView.btnChargerWebsite.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
@@ -405,6 +468,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.detailView.topPart.setOnClickListener {
|
||||
bottomSheetBehavior.state = STATE_ANCHOR_POINT
|
||||
}
|
||||
binding.detailView.topPart.setOnLongClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnLongClickListener false
|
||||
copyToClipboard(ClipData.newPlainText(getString(R.string.charger_name), charger.name))
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
setupSearchAutocomplete()
|
||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
||||
if (bottomSheetCollapsible) {
|
||||
@@ -601,6 +669,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
) {
|
||||
closeLayersMenu()
|
||||
}
|
||||
|
||||
if (vm.selectedChargepoint.value != null && newState in listOf(
|
||||
STATE_ANCHOR_POINT, STATE_COLLAPSED
|
||||
)
|
||||
) {
|
||||
closeConnectorDetailsDialog()
|
||||
vm.selectedChargepoint.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
vm.chargerSparse.observe(viewLifecycleOwner) {
|
||||
@@ -654,6 +730,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
displaySearchResult(place, moveCamera = true)
|
||||
}
|
||||
vm.layersMenuOpen.observe(viewLifecycleOwner) { open ->
|
||||
HideOnScrollFabBehavior.from(binding.fabLayers)?.hidden = open
|
||||
binding.fabLayers.visibility = if (open) View.INVISIBLE else View.VISIBLE
|
||||
binding.layersSheet.visibility = if (open) View.VISIBLE else View.INVISIBLE
|
||||
updateBackPressedCallback()
|
||||
@@ -664,6 +741,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.mapTrafficEnabled.observe(viewLifecycleOwner) {
|
||||
map?.setTrafficEnabled(it)
|
||||
}
|
||||
vm.selectedChargepoint.observe(viewLifecycleOwner) {
|
||||
binding.detailView.connectorDetailsCard.visibility =
|
||||
if (it != null) View.VISIBLE else View.INVISIBLE
|
||||
if (it != null) {
|
||||
detailsDialog.setData(it, vm.availability.value?.data)
|
||||
}
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
@@ -708,6 +793,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|| vm.searchResult.value != null
|
||||
|| (vm.layersMenuOpen.value ?: false)
|
||||
|| binding.search.hasFocus()
|
||||
|| vm.selectedChargepoint.value != null
|
||||
}
|
||||
|
||||
private fun unhighlightAllMarkers() {
|
||||
@@ -808,7 +894,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
binding.detailView.connectors.apply {
|
||||
adapter = ConnectorAdapter()
|
||||
adapter = ConnectorAdapter().apply {
|
||||
onClickListener = {
|
||||
vm.selectedChargepoint.value = it.chargepoint
|
||||
openConnectorDetailsDialog()
|
||||
}
|
||||
}
|
||||
itemAnimator = null
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
@@ -839,50 +930,26 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
onLongClickListener = {
|
||||
val charger = vm.chargerDetails.value?.data
|
||||
val clipboardManager =
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
if (charger != null) {
|
||||
when (it.icon) {
|
||||
R.drawable.ic_address -> {
|
||||
if (charger.address != null) {
|
||||
val clip = ClipData.newPlainText(
|
||||
copyToClipboard(ClipData.newPlainText(
|
||||
getString(R.string.address),
|
||||
charger.address.toString()
|
||||
)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Snackbar.make(
|
||||
requireView(),
|
||||
R.string.copied,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
R.drawable.ic_location -> {
|
||||
val clip = ClipData.newPlainText(
|
||||
copyToClipboard(ClipData.newPlainText(
|
||||
getString(R.string.coordinates),
|
||||
charger.coordinates.formatDecimal()
|
||||
)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Snackbar.make(
|
||||
requireView(),
|
||||
R.string.copied,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
))
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
@@ -902,6 +969,59 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(clip: ClipData) {
|
||||
val clipboardManager =
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Snackbar.make(
|
||||
requireView(),
|
||||
R.string.copied,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConnectorDetailsDialog() {
|
||||
val chargepoints = vm.chargerDetails.value?.data?.chargepointsMerged ?: return
|
||||
val chargepoint = vm.selectedChargepoint.value ?: return
|
||||
val index = chargepoints.indexOf(chargepoint).takeIf { it >= 0 } ?: return
|
||||
val vh = binding.detailView.connectors.findViewHolderForAdapterPosition(index) ?: return
|
||||
|
||||
val materialTransform = MaterialContainerTransform().apply {
|
||||
startView = vh.itemView
|
||||
endView = binding.detailView.connectorDetailsCard
|
||||
setPathMotion(MaterialArcMotion())
|
||||
duration = 250
|
||||
scrimColor = Color.TRANSPARENT
|
||||
addTarget(binding.detailView.connectorDetailsCard)
|
||||
isElevationShadowEnabled = false
|
||||
fadeMode = FADE_MODE_CROSS
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(binding.root, materialTransform)
|
||||
}
|
||||
|
||||
private fun closeConnectorDetailsDialog() {
|
||||
val chargepoints = vm.chargerDetails.value?.data?.chargepointsMerged ?: return
|
||||
val chargepoint = vm.selectedChargepoint.value ?: return
|
||||
val index = chargepoints.indexOf(chargepoint).takeIf { it >= 0 } ?: return
|
||||
val vh = binding.detailView.connectors.findViewHolderForAdapterPosition(index) ?: return
|
||||
|
||||
val materialTransform = MaterialContainerTransform().apply {
|
||||
startView = binding.detailView.connectorDetailsCard
|
||||
endView = vh.itemView
|
||||
setPathMotion(MaterialArcMotion())
|
||||
duration = 200
|
||||
scrimColor = Color.TRANSPARENT
|
||||
addTarget(vh.itemView)
|
||||
isElevationShadowEnabled = false
|
||||
fadeMode = FADE_MODE_CROSS
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(binding.root, materialTransform)
|
||||
}
|
||||
|
||||
private fun showPaymentMethodsDialog(charger: ChargeLocation) {
|
||||
val activity = activity ?: return
|
||||
val chargecardData = vm.chargeCardMap.value ?: return
|
||||
@@ -1281,14 +1401,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
MenuCompat.setGroupDividerEnabled(popup.menu, true)
|
||||
popup.setForceShowIcon(true)
|
||||
popup.setOnMenuItemClickListener {
|
||||
val navController = requireView().findNavController()
|
||||
when (it.itemId) {
|
||||
R.id.menu_edit_filters -> {
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
|
||||
lifecycleScope.launch {
|
||||
vm.copyFiltersToCustom()
|
||||
navController.safeNavigate(
|
||||
findNavController().safeNavigate(
|
||||
MapFragmentDirections.actionMapToFilterFragment()
|
||||
)
|
||||
}
|
||||
@@ -1298,7 +1417,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
R.id.menu_manage_filter_profiles -> {
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
|
||||
navController.safeNavigate(
|
||||
findNavController().safeNavigate(
|
||||
MapFragmentDirections.actionMapToFilterProfilesFragment()
|
||||
)
|
||||
true
|
||||
@@ -1378,6 +1497,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
popup.setTouchModal(false)
|
||||
popupMenu = popup
|
||||
popup.show()
|
||||
}
|
||||
|
||||
@@ -1461,5 +1581,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.text.Spanned
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
@@ -28,9 +29,11 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
|
||||
private lateinit var myVehiclePreference: MultiSelectDialogPreference
|
||||
private lateinit var myTariffsPreference: MultiSelectDialogPreference
|
||||
private lateinit var nativeIntegrationPreference: CheckBoxPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
nativeIntegrationPreference = findPreference("chargeprice_native_integration")!!
|
||||
|
||||
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
|
||||
myVehiclePreference.isEnabled = false
|
||||
@@ -48,7 +51,7 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
)
|
||||
}
|
||||
}.toTypedArray()
|
||||
myVehiclePreference.isEnabled = true
|
||||
myVehiclePreference.isEnabled = nativeIntegrationPreference.isChecked
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
}
|
||||
@@ -65,10 +68,28 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
it.name
|
||||
}
|
||||
}.toTypedArray()
|
||||
myTariffsPreference.isEnabled = true
|
||||
myTariffsPreference.isEnabled = nativeIntegrationPreference.isChecked
|
||||
updateMyTariffsSummary()
|
||||
}
|
||||
}
|
||||
updateNativeIntegrationState()
|
||||
}
|
||||
|
||||
private fun updateNativeIntegrationState() {
|
||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||
val pref = preferenceScreen.getPreference(i)
|
||||
if (pref == nativeIntegrationPreference) {
|
||||
continue
|
||||
} else if (pref == myTariffsPreference) {
|
||||
pref.isEnabled =
|
||||
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
|
||||
} else if (pref == myVehiclePreference) {
|
||||
pref.isEnabled =
|
||||
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
|
||||
} else {
|
||||
pref.isEnabled = nativeIntegrationPreference.isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyTariffsSummary() {
|
||||
@@ -110,6 +131,10 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
"chargeprice_my_tariffs" -> {
|
||||
updateMyTariffsSummary()
|
||||
}
|
||||
|
||||
"chargeprice_native_integration" -> {
|
||||
updateNativeIntegrationState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.availability.TeslaAuthenticationApi
|
||||
import net.vonforst.evmap.api.availability.TeslaOwnerApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
@@ -6,9 +6,11 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.isAppInstalled
|
||||
import net.vonforst.evmap.ui.getAppLocale
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
@@ -16,6 +18,7 @@ import net.vonforst.evmap.ui.updateNightMode
|
||||
class UiSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
lateinit var langPref: ListPreference
|
||||
lateinit var immediateNavPref: CheckBoxPreference
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_ui, rootKey)
|
||||
@@ -28,11 +31,18 @@ class UiSettingsFragment : BaseSettingsFragment() {
|
||||
|
||||
val appLinkPref = findPreference<Preference>("applink_associate")!!
|
||||
appLinkPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
immediateNavPref = findPreference("navigate_use_maps")!!
|
||||
immediateNavPref.isVisible = isGoogleMapsInstalled()
|
||||
}
|
||||
|
||||
private fun isGoogleMapsInstalled() =
|
||||
requireContext().packageManager.isAppInstalled("com.google.android.apps.maps")
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
langPref.value = getAppLocale()
|
||||
langPref.value = getAppLocale(requireContext())
|
||||
immediateNavPref.isVisible = isGoogleMapsInstalled()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import net.vonforst.evmap.databinding.DialogOpensourceDonationsBinding
|
||||
import net.vonforst.evmap.navigation.safeNavigate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.MaterialDialogFragment
|
||||
import java.time.Instant
|
||||
|
||||
class OpensourceDonationsDialogFragment : MaterialDialogFragment() {
|
||||
private lateinit var binding: DialogOpensourceDonationsBinding
|
||||
@@ -26,15 +27,15 @@ class OpensourceDonationsDialogFragment : MaterialDialogFragment() {
|
||||
override fun initView(view: View, savedInstanceState: Bundle?) {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
binding.btnOk.setOnClickListener {
|
||||
prefs.opensourceDonationsDialogShown = true
|
||||
prefs.opensourceDonationsDialogLastShown = Instant.now()
|
||||
dismiss()
|
||||
}
|
||||
binding.btnDonate.setOnClickListener {
|
||||
prefs.opensourceDonationsDialogShown = true
|
||||
prefs.opensourceDonationsDialogLastShown = Instant.now()
|
||||
findNavController().safeNavigate(OpensourceDonationsDialogFragmentDirections.actionOpensourceDonationsToDonate())
|
||||
}
|
||||
binding.btnGithubSponsors.setOnClickListener {
|
||||
prefs.opensourceDonationsDialogShown = true
|
||||
prefs.opensourceDonationsDialogLastShown = Instant.now()
|
||||
findNavController().safeNavigate(OpensourceDonationsDialogFragmentDirections.actionOpensourceDonationsToGithubSponsors())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@@ -127,10 +127,13 @@ data class ChargeLocation(
|
||||
get() {
|
||||
val variants = chargepoints.distinctBy { it.power to it.type }
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
val filtered = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumOf { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
val count = filtered.sumOf { it.count }
|
||||
Chargepoint(variant.type, variant.power, count,
|
||||
filtered.map { it.voltage }.distinct().singleOrNull(),
|
||||
filtered.map { it.current }.distinct().singleOrNull()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,24 +393,29 @@ data class Address(
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Chargepoint(
|
||||
// The chargepoint type (use one of the constants in the companion object)
|
||||
// The connector type (use one of the constants in the companion object if applicable)
|
||||
val type: String,
|
||||
// Power in kW (or null if unknown)
|
||||
val power: Double?,
|
||||
// How many instances of this plug/socket are available?
|
||||
val count: Int,
|
||||
// Max current in A (or null if unknown)
|
||||
val current: Double? = null,
|
||||
// Max voltage in V (or null if unknown).
|
||||
// note that for DC chargers: current * voltage may be larger than power
|
||||
// (each of the three can be separately limited)
|
||||
val voltage: Double? = null
|
||||
) : Equatable, Parcelable {
|
||||
fun hasKnownPower(): Boolean = power != null
|
||||
fun hasKnownVoltageAndCurrent(): Boolean = voltage != null && current != null
|
||||
|
||||
/**
|
||||
* If chargepoint power is defined, format it into a string.
|
||||
* Otherwise, return null.
|
||||
*/
|
||||
fun formatPower(): String? {
|
||||
if (power == null) {
|
||||
return null
|
||||
}
|
||||
val powerFmt = if (power - power.toInt() == 0.0) {
|
||||
if (power == null) return null
|
||||
val powerFmt = if (abs(power - power.toInt()) < 0.1) {
|
||||
"%.0f".format(power)
|
||||
} else {
|
||||
"%.1f".format(power)
|
||||
@@ -415,6 +423,12 @@ data class Chargepoint(
|
||||
return "$powerFmt kW"
|
||||
}
|
||||
|
||||
fun formatVoltageAndCurrent(): String? {
|
||||
if (current == null || voltage == null) return null
|
||||
|
||||
return "%.0f V · %.0f A".format(voltage, current)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_1 = "Type 1"
|
||||
const val TYPE_2_UNKNOWN = "Type 2 (either plug or socket)"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package net.vonforst.evmap.navigation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import android.widget.Toast
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -52,7 +54,15 @@ class CustomNavigator(
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
intent.launchUrl(context, Uri.parse(url))
|
||||
try {
|
||||
intent.launchUrl(context, Uri.parse(url))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.no_browser_app_found,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popBackStack() = true // Managed by Chrome Custom Tabs
|
||||
|
||||
@@ -3,13 +3,13 @@ package net.vonforst.evmap.storage
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import net.vonforst.evmap.api.availability.TeslaAvailabilityDetector
|
||||
import net.vonforst.evmap.api.availability.TeslaOwnerAvailabilityDetector
|
||||
|
||||
/**
|
||||
* Encrypted data storage for sensitive data such as API access tokens.
|
||||
* This will not be included in backups.
|
||||
*/
|
||||
class EncryptedPreferenceDataStore(context: Context) : TeslaAvailabilityDetector.TokenStore {
|
||||
class EncryptedPreferenceDataStore(context: Context) : TeslaOwnerAvailabilityDetector.TokenStore {
|
||||
val sp = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"encrypted_prefs",
|
||||
|
||||
@@ -108,11 +108,14 @@ class PreferenceDataSource(val context: Context) {
|
||||
val darkmode: String
|
||||
get() = sp.getString("darkmode", "default")!!
|
||||
|
||||
val mapProvider: String
|
||||
var mapProvider: String
|
||||
get() = sp.getString(
|
||||
"map_provider",
|
||||
context.getString(R.string.pref_map_provider_default)
|
||||
)!!
|
||||
set(value) {
|
||||
sp.edit().putString("map_provider", value).apply()
|
||||
}
|
||||
|
||||
var searchProvider: String
|
||||
get() = sp.getString(
|
||||
@@ -147,6 +150,12 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceNativeIntegration: Boolean
|
||||
get() = sp.getBoolean("chargeprice_native_integration", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_native_integration", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyVehicles: Set<String>
|
||||
get() = try {
|
||||
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
|
||||
@@ -237,10 +246,11 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putLong("chargeprice_counter", value).apply()
|
||||
}
|
||||
|
||||
var opensourceDonationsDialogShown: Boolean
|
||||
get() = sp.getBoolean("opensource_donations_dialog_shown", false)
|
||||
var opensourceDonationsDialogLastShown: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("opensource_donations_dialog_last_shown", 0L))
|
||||
set(value) {
|
||||
sp.edit().putBoolean("opensource_donations_dialog_shown", value).apply()
|
||||
sp.edit().putLong("opensource_donations_dialog_last_shown", value.toEpochMilli())
|
||||
.apply()
|
||||
}
|
||||
|
||||
var placeSearchResultAndroidAuto: LatLng?
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.ImageView
|
||||
@@ -25,9 +26,14 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import coil.load
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
import net.vonforst.evmap.isDarkMode
|
||||
import net.vonforst.evmap.kmPerMile
|
||||
import net.vonforst.evmap.meterPerFt
|
||||
import net.vonforst.evmap.shouldUseImperialUnits
|
||||
import java.time.Instant
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
@@ -67,7 +73,7 @@ fun invisibleUnlessAnimated(view: View, oldValue: Boolean, newValue: Boolean) {
|
||||
if (oldValue == newValue) {
|
||||
if (!newValue && view.visibility == View.VISIBLE && view.alpha == 1f) {
|
||||
// view is initially invisible
|
||||
view.visibility = View.GONE
|
||||
view.visibility = View.INVISIBLE
|
||||
} else {
|
||||
return
|
||||
}
|
||||
@@ -137,8 +143,8 @@ fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
|
||||
}
|
||||
|
||||
@BindingAdapter("connectorIcon")
|
||||
fun getConnectorItem(view: ImageView, type: String) {
|
||||
view.setImageResource(iconForPlugType(type))
|
||||
fun getConnectorItem(view: ImageView, type: String?) {
|
||||
view.setImageResource(type?.let { iconForPlugType(it) } ?: 0)
|
||||
}
|
||||
|
||||
@BindingAdapter("srcCompat")
|
||||
@@ -156,11 +162,21 @@ fun setImageTintAvailability(view: ImageView, available: List<ChargepointStatus>
|
||||
view.imageTintList = ColorStateList.valueOf(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
@BindingAdapter("tintAvailability")
|
||||
fun setImageTintAvailability(view: ImageView, available: ChargepointStatus?) {
|
||||
view.imageTintList = ColorStateList.valueOf(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
@BindingAdapter("textColorAvailability")
|
||||
fun setTextColorAvailability(view: TextView, available: List<ChargepointStatus>?) {
|
||||
view.setTextColor(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
@BindingAdapter("textColorAvailability")
|
||||
fun setTextColorAvailability(view: TextView, available: ChargepointStatus?) {
|
||||
view.setTextColor(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
@BindingAdapter("backgroundTintAvailability")
|
||||
fun setBackgroundTintAvailability(view: View, available: List<ChargepointStatus>?) {
|
||||
view.backgroundTintList = ColorStateList.valueOf(availabilityColor(available, view.context))
|
||||
@@ -269,6 +285,25 @@ private fun availabilityColor(
|
||||
ta.getColor(0, 0)
|
||||
}
|
||||
|
||||
private fun availabilityColor(
|
||||
status: ChargepointStatus?,
|
||||
context: Context
|
||||
): Int = when (status) {
|
||||
ChargepointStatus.UNKNOWN -> ContextCompat.getColor(context, R.color.unknown)
|
||||
ChargepointStatus.AVAILABLE -> ContextCompat.getColor(context, R.color.available)
|
||||
ChargepointStatus.FAULTED -> ContextCompat.getColor(context, R.color.unavailable)
|
||||
ChargepointStatus.OCCUPIED, ChargepointStatus.CHARGING -> ContextCompat.getColor(
|
||||
context,
|
||||
R.color.charging
|
||||
)
|
||||
|
||||
null -> {
|
||||
val ta =
|
||||
context.theme.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.colorControlNormal))
|
||||
ta.getColor(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun availabilityText(status: List<ChargepointStatus>?): String? {
|
||||
if (status == null) return null
|
||||
|
||||
@@ -281,6 +316,29 @@ fun availabilityText(status: List<ChargepointStatus>?): String? {
|
||||
} else available.toString()
|
||||
}
|
||||
|
||||
fun availabilityText(status: ChargepointStatus?, lastChange: Instant?, context: Context): String? {
|
||||
if (status == null) return null
|
||||
|
||||
val statusText = when (status) {
|
||||
ChargepointStatus.UNKNOWN -> context.getString(R.string.status_unknown)
|
||||
ChargepointStatus.AVAILABLE -> context.getString(R.string.status_available)
|
||||
ChargepointStatus.CHARGING -> context.getString(R.string.status_charging)
|
||||
ChargepointStatus.OCCUPIED -> context.getString(R.string.status_occupied)
|
||||
ChargepointStatus.FAULTED -> context.getString(R.string.status_faulted)
|
||||
}
|
||||
|
||||
return if (lastChange != null) {
|
||||
val relativeTime = DateUtils.getRelativeTimeSpanString(
|
||||
lastChange.toEpochMilli(),
|
||||
Instant.now().toEpochMilli(),
|
||||
0
|
||||
).toString()
|
||||
return context.getString(R.string.status_since, statusText, relativeTime)
|
||||
} else {
|
||||
statusText
|
||||
}
|
||||
}
|
||||
|
||||
fun flatten(it: Iterable<Iterable<ChargepointStatus>>?): List<ChargepointStatus>? {
|
||||
return it?.flatten()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (prefs.darkmode) {
|
||||
@@ -25,13 +27,14 @@ fun updateAppLocale(language: String) {
|
||||
)
|
||||
}
|
||||
|
||||
fun getAppLocale(): String? {
|
||||
fun getAppLocale(context: Context): String? {
|
||||
val locales = AppCompatDelegate.getApplicationLocales()
|
||||
return if (locales.isEmpty) {
|
||||
"default"
|
||||
} else {
|
||||
val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() }
|
||||
LocaleListCompat.forLanguageTags(BuildConfig.supportedLocales).getFirstMatch(arr)
|
||||
?.toLanguageTag()
|
||||
val choices =
|
||||
context.resources.getStringArray(R.array.pref_language_values).joinToString(",")
|
||||
LocaleListCompat.forLanguageTags(choices).getFirstMatch(arr)?.toLanguageTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,50 +10,60 @@ import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import net.vonforst.evmap.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private fun dialogEditText(ctx: Context): Pair<View, EditText> {
|
||||
val container = FrameLayout(ctx)
|
||||
container.setPadding(
|
||||
(16 * ctx.resources.displayMetrics.density).toInt(), 0,
|
||||
(16 * ctx.resources.displayMetrics.density).toInt(), 0
|
||||
)
|
||||
val input = EditText(ctx)
|
||||
input.isSingleLine = true
|
||||
container.addView(input)
|
||||
return container to input
|
||||
private fun dialogEditText(ctx: Context): Pair<TextInputLayout, EditText> {
|
||||
val view = LayoutInflater.from(ctx).inflate(R.layout.dialog_textinput, null)
|
||||
return view as TextInputLayout to view.findViewById(R.id.input)
|
||||
}
|
||||
|
||||
fun showEditTextDialog(
|
||||
ctx: Context,
|
||||
customize: (MaterialAlertDialogBuilder, EditText) -> Unit
|
||||
customize: (MaterialAlertDialogBuilder, EditText) -> Unit,
|
||||
okAction: (String) -> Unit
|
||||
): AlertDialog {
|
||||
val (container, input) = dialogEditText(ctx)
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(ctx)
|
||||
.setView(container)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
|
||||
customize(dialogBuilder, input)
|
||||
|
||||
val dialog = dialogBuilder.show()
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
|
||||
val okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
|
||||
// focus and show keyboard
|
||||
input.requestFocus()
|
||||
input.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
val text = input.text
|
||||
val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
if (text != null && button != null) {
|
||||
button.performClick()
|
||||
if (text != null && okButton != null) {
|
||||
okButton.performClick()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
okButton?.setOnClickListener {
|
||||
if (input.text.isBlank()) {
|
||||
container.isErrorEnabled = true
|
||||
container.error = ctx.getString(R.string.required)
|
||||
} else {
|
||||
container.isErrorEnabled = false
|
||||
okAction(input.text.toString())
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
|
||||
class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
|
||||
FloatingActionButton.Behavior(context, attrs) {
|
||||
var hidden: Boolean = false
|
||||
|
||||
companion object {
|
||||
fun from(view: View): HideOnScrollFabBehavior? {
|
||||
return ((view.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? HideOnScrollFabBehavior)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartNestedScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
@@ -61,13 +68,13 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
|
||||
child: FloatingActionButton,
|
||||
dependency: View
|
||||
): Boolean {
|
||||
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
|
||||
val behavior = BottomSheetBehaviorGoogleMapsLike.from(dependency)
|
||||
when (behavior.state) {
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> {
|
||||
|
||||
}
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
|
||||
child.show()
|
||||
if (!hidden) child.show()
|
||||
}
|
||||
else -> {
|
||||
child.hide()
|
||||
@@ -103,7 +110,7 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
|
||||
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()
|
||||
if (!hidden) child.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import jsonapi.Relationships
|
||||
import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.EspressoIdlingResource
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
@@ -48,6 +49,7 @@ class ChargepriceViewModel(
|
||||
} else {
|
||||
value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
EspressoIdlingResource.increment()
|
||||
value = try {
|
||||
val result = api.getVehicles()
|
||||
Resource.success(result.filter {
|
||||
@@ -55,6 +57,10 @@ class ChargepriceViewModel(
|
||||
})
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
Resource.error(e.message, null)
|
||||
} finally {
|
||||
EspressoIdlingResource.decrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,6 +257,7 @@ class ChargepriceViewModel(
|
||||
|
||||
loadPricesJob?.cancel()
|
||||
loadPricesJob = viewModelScope.launch {
|
||||
EspressoIdlingResource.increment()
|
||||
try {
|
||||
val result = api.getChargePrices(
|
||||
ChargepriceRequest(
|
||||
@@ -293,6 +300,8 @@ class ChargepriceViewModel(
|
||||
} catch (e: HttpException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
} finally {
|
||||
EspressoIdlingResource.decrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,25 +9,17 @@ import com.car2go.maps.Projection
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
|
||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
|
||||
import net.vonforst.evmap.api.availability.tesla.Pricing
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.api.fronyx.FronyxApi
|
||||
import net.vonforst.evmap.api.fronyx.FronyxEvseIdResponse
|
||||
import net.vonforst.evmap.api.fronyx.FronyxStatus
|
||||
import net.vonforst.evmap.api.fronyx.PredictionData
|
||||
import net.vonforst.evmap.api.fronyx.PredictionRepository
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargepoint
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.openchargemap.OCMConnection
|
||||
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
@@ -40,12 +32,6 @@ import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Parcelize
|
||||
@@ -162,9 +148,9 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
MutableLiveData<Set<Long>>()
|
||||
}
|
||||
|
||||
val chargerSparse: MutableLiveData<ChargeLocation?> by lazy {
|
||||
state.getLiveData("chargerSparse")
|
||||
}
|
||||
val chargerSparse: MutableLiveData<ChargeLocation?> =
|
||||
state.getLiveData<ChargeLocation?>("chargerSparse")
|
||||
|
||||
private val triggerChargerDetailsRefresh = MutableLiveData(false)
|
||||
val chargerDetails: LiveData<Resource<ChargeLocation>> = chargerSparse.switchMap { charger ->
|
||||
triggerChargerDetailsRefresh.value = false
|
||||
@@ -180,6 +166,15 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
val selectedChargepoint: MutableLiveData<Chargepoint?> =
|
||||
state.getLiveData("selectedChargepoint")
|
||||
|
||||
init {
|
||||
chargerSparse.observeForever {
|
||||
selectedChargepoint.value = null
|
||||
}
|
||||
}
|
||||
|
||||
val charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocation>>().apply {
|
||||
addSource(chargerDetails) {
|
||||
@@ -249,7 +244,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
|
||||
val teslaPricing = availability.map {
|
||||
it.data?.extraData as? TeslaGraphQlApi.Pricing
|
||||
it.data?.extraData as? Pricing
|
||||
}
|
||||
|
||||
private val predictionRepository = PredictionRepository(application)
|
||||
|
||||
@@ -11,6 +11,7 @@ import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class SettingsViewModel(
|
||||
@@ -58,6 +59,8 @@ class SettingsViewModel(
|
||||
vehicles.value = Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +72,8 @@ class SettingsViewModel(
|
||||
tariffs.value = Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
tariffs.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
tariffs.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
app/src/main/res/drawable/circle.xml
Normal file
11
app/src/main/res/drawable/circle.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#FFFFFFFF" />
|
||||
<size
|
||||
android:height="12dp"
|
||||
android:width="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<variable
|
||||
name="teslaPricing"
|
||||
type="net.vonforst.evmap.api.availability.TeslaGraphQlApi.Pricing" />
|
||||
type="net.vonforst.evmap.api.availability.tesla.Pricing" />
|
||||
|
||||
<variable
|
||||
name="chargeCards"
|
||||
@@ -158,7 +158,6 @@
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
|
||||
app:invisibleUnless="@{filteredAvailability.data != null && !expanded}"
|
||||
app:invisibleUnlessAnimated="@{filteredAvailability.data != null && !expanded}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toTopOf="@+id/txtName"
|
||||
tools:backgroundTint="@color/available"
|
||||
@@ -560,6 +559,24 @@
|
||||
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>
|
||||
|
||||
|
||||
35
app/src/main/res/layout/dialog_connector_details.xml
Normal file
35
app/src/main/res/layout/dialog_connector_details.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/dialog_connector_details_preview" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnClose"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/close"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_close"
|
||||
app:tint="?colorControlNormal" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
115
app/src/main/res/layout/dialog_connector_details_header.xml
Normal file
115
app/src/main/res/layout/dialog_connector_details_header.xml
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.UtilsKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargepointWithAvailability" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:layout_marginStart="?attr/dialogPreferredPadding"
|
||||
android:layout_marginEnd="?attr/dialogPreferredPadding">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:contentDescription="@{item.chargepoint.type}"
|
||||
android:tintMode="src_in"
|
||||
app:connectorIcon="@{item.chargepoint.type}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/divider"
|
||||
app:tintAvailability="@{item.status}"
|
||||
tools:srcCompat="@drawable/ic_connector_typ2"
|
||||
tools:tint="@color/available" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="38dp"
|
||||
android:layout_marginTop="38dp"
|
||||
android:text="@{String.format("\u00D7 %d", item.chargepoint.count)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{item.status == null}"
|
||||
app:layout_constraintStart_toStartOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView"
|
||||
tools:text="×99"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{item.status}"
|
||||
app:goneUnless="@{item.status != null}"
|
||||
app:layout_constraintStart_toStartOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView"
|
||||
tools:backgroundTint="@color/available"
|
||||
tools:text="80/99" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView6"
|
||||
android:layout_width="0dp"
|
||||
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) + " · " + item.chargepoint.formatPower() : null}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
app:goneUnless="@{item.chargepoint.hasKnownPower()}"
|
||||
app:layout_constraintBottom_toTopOf="@id/textView8"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="@id/imageView"
|
||||
tools:text="CCS · 350 kW" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{item.chargepoint.formatVoltageAndCurrent()}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{item.chargepoint.hasKnownVoltageAndCurrent()}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView6"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView6"
|
||||
tools:text="1000 V · 500 A" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
70
app/src/main/res/layout/dialog_connector_details_item.xml
Normal file
70
app/src/main/res/layout/dialog_connector_details_item.xml
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.adapter.ConnectorDetailsAdapter.ConnectorDetails" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
<import type="java.time.Instant" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ConnectorDetails" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:layout_marginStart="?attr/dialogPreferredPadding"
|
||||
android:layout_marginEnd="?attr/dialogPreferredPadding">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@{BindingAdaptersKt.availabilityText(item.status, (Instant) null, context)}"
|
||||
android:scaleType="center"
|
||||
android:tintMode="src_in"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tintAvailability="@{item.status}"
|
||||
app:srcCompat="@drawable/circle"
|
||||
tools:tint="@color/available" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtEvseid"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:text="@{(item.label != null && item.evseId != null) ? item.label + " · " + item.evseId : (item.label ?? item.evseId)}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="SK*IOY*E222901" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtStatus"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@{BindingAdaptersKt.availabilityText(item.status, item.lastChange, context)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{item.status != null}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtEvseid"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtEvseid"
|
||||
tools:text="Available" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
18
app/src/main/res/layout/dialog_connector_details_preview.xml
Normal file
18
app/src/main/res/layout/dialog_connector_details_preview.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dialog_connector_details_header" />
|
||||
|
||||
<include layout="@layout/dialog_connector_details_item" />
|
||||
|
||||
<include layout="@layout/dialog_connector_details_item" />
|
||||
|
||||
<include layout="@layout/dialog_connector_details_item" />
|
||||
|
||||
<include layout="@layout/dialog_connector_details_item" />
|
||||
|
||||
<include layout="@layout/dialog_connector_details_item" />
|
||||
</LinearLayout>
|
||||
14
app/src/main/res/layout/dialog_textinput.xml
Normal file
14
app/src/main/res/layout/dialog_textinput.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -1,29 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
android:id="@+id/linearLayout7"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical">
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView20"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/referrals"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary" />
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textView21"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView21"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/referrals_info" />
|
||||
android:text="@string/referrals_info"
|
||||
app:layout_constraintBottom_toTopOf="@+id/referral_tesla"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView20"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView20" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/referral_tesla"
|
||||
@@ -32,4 +54,39 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/referral_tesla"
|
||||
app:icon="@drawable/ic_tesla" />
|
||||
</LinearLayout>
|
||||
|
||||
<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>
|
||||
@@ -238,7 +238,8 @@
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginTop="@dimen/layers_fab_top_padding"
|
||||
app:tint="?android:colorControlNormal"
|
||||
app:tint="?colorControlNormal"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:borderWidth="0dp"
|
||||
app:srcCompat="@drawable/ic_layers"
|
||||
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
|
||||
@@ -261,4 +262,4 @@
|
||||
app:vm="@{vm}" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
</layout>
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
|
||||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
@@ -0,0 +1 @@
|
||||
unqualifiedResLocale=en-US
|
||||
386
app/src/main/res/values-cs/strings.xml
Normal file
386
app/src/main/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,386 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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"><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"><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>
|
||||
<string name="parking_paid">Placené</string>
|
||||
<string name="amenities">Vybavení</string>
|
||||
<string name="general_info">Obecné informace</string>
|
||||
<string name="realtime_data_unavailable">Aktuální stav není dostupný</string>
|
||||
<string name="realtime_data_login_needed">Pro data v reálném čase je vyžadován účet Tesla</string>
|
||||
<string name="realtime_data_loading">Kontrola aktuálního stavu…</string>
|
||||
<string name="realtime_data_source">Zdroj aktuálního stavu (beta): %s</string>
|
||||
<string name="source">Zdroj: %s</string>
|
||||
<string name="menu_map">Mapa</string>
|
||||
<string name="menu_favs">Oblíbené</string>
|
||||
<string name="menu_filter">Filtr</string>
|
||||
<string name="about">O aplikaci</string>
|
||||
<string name="version">Verze</string>
|
||||
<string name="github_link_title">Zdrojový kód</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="settings_ui">Rozhraní</string>
|
||||
<string name="settings_map">Mapa</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="other">Ostatní</string>
|
||||
<string name="privacy">Soukromí</string>
|
||||
<string name="fav_add">Uložit jako oblíbené</string>
|
||||
<string name="connectors">Konektory</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigační tlačítko spustí navigaci pomocí Map Google</string>
|
||||
<string name="coordinates">Souřadnice</string>
|
||||
<string name="share">Sdílet</string>
|
||||
<string name="filter_min_power">Minimální výkon</string>
|
||||
<string name="filter_free_parking">Pouze nabíječky s bezplatným parkováním</string>
|
||||
<string name="filter_min_connectors">Minimální počet konektorů</string>
|
||||
<string name="filter_connectors">Konektory</string>
|
||||
<string name="plug_type_1">Typ 1</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
<string name="plug_supercharger">Tesla Supercharger</string>
|
||||
<string name="plug_cee_blau">CEE modrý</string>
|
||||
<string name="plug_cee_rot">CEE červený</string>
|
||||
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
|
||||
<string name="all">všechny</string>
|
||||
<string name="show_more">více…</string>
|
||||
<string name="show_less">méně…</string>
|
||||
<string name="favorites_empty_state">Tady se zobrazí uložené nabíječky</string>
|
||||
<string name="donate">Přispět</string>
|
||||
<string name="donation_successful">Děkujeme ❤️</string>
|
||||
<string name="donation_failed">Něco se pokazilo 😕</string>
|
||||
<string name="map_type_normal">Výchozí</string>
|
||||
<string name="map_type_satellite">Satelitní</string>
|
||||
<string name="map_details">Podrobnosti mapy</string>
|
||||
<string name="map_traffic">Doprava</string>
|
||||
<string name="faq">Často kladené dotazy</string>
|
||||
<string name="filters_activated">Filtry aktivovány</string>
|
||||
<string name="menu_manage_filter_profiles">Správa profilů filtrů</string>
|
||||
<string name="go_to_chargeprice">Porovnat ceny</string>
|
||||
<string name="fault_report">Zpráva o závadě</string>
|
||||
<string name="fault_report_date">Zpráva o závadě (poslední aktualizace: %s)</string>
|
||||
<string name="filter_networks">Sítě</string>
|
||||
<string name="filter_operators">Operátoři</string>
|
||||
<string name="filter_chargecards">Platební metody</string>
|
||||
<string name="all_selected">Vybrány všechny</string>
|
||||
<string name="number_selected">Vybráno %d</string>
|
||||
<string name="edit">upravit</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Jazyk aplikace</string>
|
||||
<string name="pref_darkmode">Tmavý režim</string>
|
||||
<string name="filter_barrierfree">Použitelné bez registrace</string>
|
||||
<string name="filter_exclude_faults">Vyloučit nabíječky se zprávou o závadě</string>
|
||||
<string name="charge_cards">Platební metody</string>
|
||||
<string name="goingelectric_forum">Vlákno na fóru GoingElectric.de</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="menu_report_new_charger">Nová nabíječka</string>
|
||||
<string name="edit_at_datasource">Upravit na %s</string>
|
||||
<string name="categories">Kategorie</string>
|
||||
<string name="category_car_dealership">Prodejna automobilů</string>
|
||||
<string name="category_service_on_motorway">Služební oblast (na dálnici)</string>
|
||||
<string name="category_service_off_motorway">Služební oblast (mimo dálnici)</string>
|
||||
<string name="category_railway_station">Železniční stanice</string>
|
||||
<string name="category_public_authorities">Orgány veřejné správy</string>
|
||||
<string name="category_camping">Kemp</string>
|
||||
<string name="category_shopping_mall">Nákupní středisko</string>
|
||||
<string name="category_church">Kostel</string>
|
||||
<string name="category_hospital">Nemocnice</string>
|
||||
<string name="category_museum">Muzeum</string>
|
||||
<string name="category_parking_multi">Parkovací garáž</string>
|
||||
<string name="category_parking">Parkoviště</string>
|
||||
<string name="category_private_charger">Soukromá nabíječka</string>
|
||||
<string name="category_rest_area">Odpočívadlo</string>
|
||||
<string name="category_restaurant">Restaurace</string>
|
||||
<string name="category_swimming_pool">Bazén</string>
|
||||
<string name="category_supermarket">Supermarket</string>
|
||||
<string name="category_petrol_station">Čerpací stanice</string>
|
||||
<string name="category_parking_underground">Podzemní parkoviště</string>
|
||||
<string name="category_zoo">Zoo</string>
|
||||
<string name="category_caravan_site">Kemp pro karavany</string>
|
||||
<string name="menu_apply">Použít filtry</string>
|
||||
<string name="menu_save_profile">Uložit jako profil</string>
|
||||
<string name="menu_reset">Obnovit nastavení filtrů</string>
|
||||
<string name="no_filters">Žádné filtry</string>
|
||||
<string name="filter_custom">Upravený filtr</string>
|
||||
<string name="filter_favorites">Oblíbené</string>
|
||||
<string name="reorder">změnit pořadí</string>
|
||||
<string name="delete">Odstranit</string>
|
||||
<string name="save_profile_enter_name">Zadejte název profilu filtrů:</string>
|
||||
<string name="filterprofiles_empty_state">Nemáte uložené žádné profily filtrů</string>
|
||||
<string name="welcome_to_evmap">Vítejte v aplikaci EVMap</string>
|
||||
<string name="welcome_1">Najděte nabíječky elektromobilů v okolí</string>
|
||||
<string name="welcome_2_title">Potřebujete dobít baterky?</string>
|
||||
<string name="welcome_2_detail">Tyto informace naleznete také na stránce „O aplikaci“ → „Často kladené dotazy“</string>
|
||||
<string name="chargeprice_donation_dialog_title">Máte rádi výhodné nabídky!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Hodně využiváte možnost porovnávání cen. Pomozte nám pokrýt náklady na tato data podpořením EVMap peněžním darem.</string>
|
||||
<string name="deleted_item">Odstraněno „%s“</string>
|
||||
<string name="undo">Vrátit zpět</string>
|
||||
<string name="rename">Přejmenovat</string>
|
||||
<string name="charging_barrierfree">Použitelné bez registrace</string>
|
||||
<string name="navigate">Navigace</string>
|
||||
<string name="charge_price_format">%1$.2f %2$s</string>
|
||||
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
|
||||
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
|
||||
<string name="charge_price_minute_format">%1$.2f %2$s/min</string>
|
||||
<string name="chargeprice_select_connector">Zvolte konektor</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Pouze pro přihlášené zákazníky</string>
|
||||
<string name="edit_on_goingelectric_info">Pokud je tato stránka prázdná, přihlaste se prosím na GoingElectric.de</string>
|
||||
<string name="percent_format">%.0f%%</string>
|
||||
<string name="chargeprice_session_fee">poplatek za využití</string>
|
||||
<string name="chargeprice_per_kwh">za kWh</string>
|
||||
<string name="chargeprice_per_minute">za min</string>
|
||||
<string name="chargeprice_no_tariffs_found">Tato nabíječka nemá ve službě Chargeprice.app žádné nabíjecí plány</string>
|
||||
<string name="powered_by_chargeprice">používá službu Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Základní poplatek: %1$.2f %2$s/měsíc</string>
|
||||
<string name="chargeprice_min_spend">Minimální útrata: %1$.2f %2$s/měsíc</string>
|
||||
<string name="settings_chargeprice">Porovnání cen</string>
|
||||
<string name="pref_my_vehicle">Moje vozidla</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Vyloučit plány s měsíčními poplatky</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Zahrnout zákaznické plány</string>
|
||||
<string name="chargeprice_select_car_first">Nejprve prosím v nastavení vyberte váš model auta</string>
|
||||
<string name="chargeprice_battery_range">Nabíjení od %1$.0f%% do %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Nabíjení od</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, cca. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_vehicle">Vozidlo</string>
|
||||
<string name="chargeprice_price_not_available">Cena není dostupná</string>
|
||||
<string name="close">Zavřít</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Tato nabíjecí stanice nemá žádné kompatibilní konektory</string>
|
||||
<string name="chargeprice_blocking_fee">Blokovací poplatek>%s</string>
|
||||
<string name="pref_my_tariffs">Mé nabíjecí plány</string>
|
||||
<string name="chargeprice_all_tariffs_selected">vybrány všechny plány</string>
|
||||
<string name="license">Licence</string>
|
||||
<string name="settings_charger_data">Nabíjecí stanice</string>
|
||||
<string name="pref_data_source">Zdroj dat</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">Vybrán %d plán</item>
|
||||
<item quantity="few">Vybrány %d plány</item>
|
||||
<item quantity="other">Vybráno %d plánů</item>
|
||||
</plurals>
|
||||
<string name="data_sources_description">Vyberte prosím zdroj dat pro nabíjecí stanice. Můžete jej později změnit v nastavení aplikace.</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Funguje dobře v německy mluvících zemích. Popisy jsou v němčině. Spravováno komunitou.</string>
|
||||
<string name="next">další</string>
|
||||
<string name="get_started">Začínáme</string>
|
||||
<string name="got_it">Chápu</string>
|
||||
<string name="lets_go">Jdeme na to</string>
|
||||
<string name="crash_report_text">Aplikace EVMap havarovala. Odešlete prosím hlášení o pádu vývojáři.</string>
|
||||
<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">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>
|
||||
<string name="unnamed_filter_profile">Nepojmenovaný profil filtrů</string>
|
||||
<string name="faq_link">https://ev-map.app/faq/</string>
|
||||
<string name="required">vyžadováno</string>
|
||||
<string name="edit_filter_profile">Upravit filtr „%s“</string>
|
||||
<string name="pref_search_delete_recent">Smazat nedávné výsledky vyhledávání</string>
|
||||
<string name="deleted_recent_search_results">Nedávné výsledky vyhledávání byly smazány</string>
|
||||
<string name="settings_data_sources">Zdroje dat</string>
|
||||
<string name="help">Nápověda</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Povolit nevyvážené zatížení</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Povolit jednofázové nabíjení střídavým proudem o výkonu vyšším než 4,5 kW</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Otočení mapy</string>
|
||||
<string name="pref_map_rotate_gestures_on">Použijte dva prsty pro otočení mapy</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotace vypnuta (sever vždy nahoře)</string>
|
||||
<string name="refresh_live_data">stav obnovení v reálném čase</string>
|
||||
<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>
|
||||
<string name="about_contributors_text">Děkujeme všem přispěvatelům za jejich výpomoc s kódem a překlady aplikace EVMap:</string>
|
||||
<string name="utilization_prediction">Předpověď využití</string>
|
||||
<string name="powered_by_fronyx">používá službu fronyx</string>
|
||||
<string name="prediction_help">Předpověď je založena na faktorech, jako je den v týdnu, denní doba a předchozí využití, takže se můžete vyhnout přeplněným nabíječkám. Bez záruky.</string>
|
||||
<string name="prediction_time_colon">%s:</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">Dostupná %1$d/%2$d</item>
|
||||
<item quantity="few">Dostupné %1$d/%2$d</item>
|
||||
<item quantity="other">Dostupných %1$d/%2$d</item>
|
||||
</plurals>
|
||||
<string name="pref_prediction_enabled">Zobrazit předpověď využití</string>
|
||||
<string name="prediction_only">(pouze %s)</string>
|
||||
<string name="pref_prediction_enabled_summary">pro podporované nabíječky
|
||||
\n(v současné době pouze stejnosměrné v Německu)</string>
|
||||
<string name="prediction_dc_plugs_only">Zástrčky se stejnosměrným proudem</string>
|
||||
<string name="data_source_switched_to">Zdroj dat změněn na %s</string>
|
||||
<string name="pref_applink_associate">Otevírat podporované odkazy</string>
|
||||
<string name="pref_applink_associate_summary">z goingelectric.de a openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">Moje plány</string>
|
||||
<string name="developer_mode_enabled">Vývojářský režim aktivován</string>
|
||||
<string name="developer_options">Vývojářské možnosti</string>
|
||||
<string name="disable_developer_mode">Zakázat vývojářský režim</string>
|
||||
<string name="developer_mode_disabled">Vývojářský režim deaktivován</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="location_status">Stav poskytovatele polohy</string>
|
||||
<string name="pref_tesla_account">Účet Tesla</string>
|
||||
<string name="pref_tesla_account_enabled">Přihlášeni jako %s</string>
|
||||
<string name="pref_tesla_account_disabled">Přihlaste se pro zobrazení dat pro Tesla Superchargers v reálném čase. Není potřeba vozidlo Tesla</string>
|
||||
<string name="logging_in">Přihlašování…</string>
|
||||
<string name="log_out">Odhlásit se</string>
|
||||
<string name="logged_out">Odhlášeni</string>
|
||||
<string name="login">Přihlásit se</string>
|
||||
<string name="login_error">Přihlášení se nezdařilo</string>
|
||||
<string name="tesla_pricing_owners">Pouze vozidla Tesla:</string>
|
||||
<string name="tesla_pricing_others">Ostatní zákazníci:</string>
|
||||
<string name="tesla_pricing_other_times">Ostatní časy:</string>
|
||||
<string name="tesla_pricing_blocking_fee">Blokovací poplatek: %s</string>
|
||||
<string name="average_utilization">Průměrné využití</string>
|
||||
<string name="website">Webové stránky</string>
|
||||
<string name="pref_map_scale">Zobrazit ovládání přiblížení mapy</string>
|
||||
<string name="pref_map_scale_meters_and_miles">Míle a metry na ukazateli měřítka</string>
|
||||
<string name="pref_units">Jednotky</string>
|
||||
<string name="pref_units_metric">Metrické</string>
|
||||
<string name="pref_units_imperial">Imperiální</string>
|
||||
<string name="data_retrieved_at">Data obdržena %s</string>
|
||||
<string name="settings_caching">Mezipaměť</string>
|
||||
<string name="settings_cache_count">Velikost mezipaměti</string>
|
||||
<string name="settings_cache_clear">Vymazat mezipaměť</string>
|
||||
<string name="settings_cache_clear_summary">Vymaže všechny nabíječky v mezipaměti kromě oblíbených</string>
|
||||
<string name="settings_cache_count_summary">%1$d nabíječek v mezipaměti, %2$.1f MB</string>
|
||||
<string name="auto_no_chargers_found">V okolí nebyly nalezeny žádné nabíječky</string>
|
||||
<string name="auto_no_favorites_found">Nebyly nalezeny žádné oblíbené</string>
|
||||
<string name="opened_on_phone">Otevřeno na telefonu</string>
|
||||
<string name="grant_on_phone">Udělit na telefonu</string>
|
||||
<string name="auto_chargers_closeby">Nabíječky v okolí</string>
|
||||
<string name="auto_favorites">Oblíbené</string>
|
||||
<string name="auto_chargers_near_location">Poblíž %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Zpráva o závadě (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Další aktualizace nejsou možné. Vraťte se prosím zpět a restartujte aplikaci.</string>
|
||||
<string name="auto_prices">Ceník</string>
|
||||
<string name="auto_vehicle_data">Data vozidla</string>
|
||||
<string name="auto_charging_level">Úroveň nabití</string>
|
||||
<string name="auto_no_data">Nedostupné</string>
|
||||
<string name="auto_range">Rozsah</string>
|
||||
<string name="auto_speed">Rychlost</string>
|
||||
<string name="auto_heading">Hlavička</string>
|
||||
<string name="auto_settings">Nastavení</string>
|
||||
<string name="welcome_android_auto">Podpora Android Auto</string>
|
||||
<string name="welcome_android_auto_detail">EVMap můžete používat i z vašeho vozidla Android Auto u podporovaných aut. Stačí vybrat aplikaci EVMap v nabídce Android Auto.</string>
|
||||
<string name="sounds_cool">To zní dobře</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">Aplikace EVMap nedokázala zjistit model vašeho vozidla.</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Žádné z vybraných vozidel v aplikaci se neshoduje s tímto vozidlem (%1$s %2$s).</string>
|
||||
<string name="auto_chargers_ahead">Pouze nabíječky v okolí směru jízdy</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Rozsah nabíjení pro porovnání cen</string>
|
||||
<string name="selecting_all">vybrány všechny položky</string>
|
||||
<string name="selecting_none">zrušit výběr všech položek</string>
|
||||
<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="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>
|
||||
<string name="copied">Zkopírováno do schránky</string>
|
||||
<string name="charger_name">Název nabíječky</string>
|
||||
<string name="closed_unfmt">Zavřeno</string>
|
||||
<string name="holiday">Svátek</string>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="search">Vyhledávání</string>
|
||||
<string name="fav_remove">Odebrat z oblíbených</string>
|
||||
<string name="pref_navigate_use_maps">Okamžitá navigace</string>
|
||||
<string name="plug_type_2">Typ 2</string>
|
||||
<string name="connection_error">Nepodařilo se načíst nabíjecí stanice</string>
|
||||
<string name="filter_open_247">Dostupné 24/7</string>
|
||||
<string name="and_n_others">a %d dalších</string>
|
||||
<string name="no_maps_app_found">Nejprve si nainstalujte navigační aplikaci</string>
|
||||
<string name="operator">Operátor</string>
|
||||
<string name="network">Síť</string>
|
||||
<string name="not_implemented">zatím není implementováno</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigační tlačítko otevře aplikaci map s umístěním nabíječky</string>
|
||||
<string name="plug_type_3">Typ 3A</string>
|
||||
<string name="map_type_terrain">Terén</string>
|
||||
<string name="map_type">Typ mapy</string>
|
||||
<string name="location_error">Nepodařilo se zjistit polohu. Zkontrolujte prosím nastavení systému</string>
|
||||
<string name="pref_map_provider">Poskytovatel mapy</string>
|
||||
<string name="oss_licenses">Licence</string>
|
||||
<string name="filter_free">Pouze bezplatné nabíječky</string>
|
||||
<string name="none">žádné</string>
|
||||
<string name="menu_filters_active">Aktivní filtry</string>
|
||||
<string name="retry">Zkusit znovu</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="filters_deactivated">Filtry deaktivovány</string>
|
||||
<string name="menu_edit_filters">Upravit filtry</string>
|
||||
<string name="category_holiday_home">Rekreační dům</string>
|
||||
<string name="category_airport">Letiště</string>
|
||||
<string name="category_amusement_park">Zábavní park</string>
|
||||
<string name="category_cinema">Kino</string>
|
||||
<string name="category_hotel">Hotel</string>
|
||||
<string name="welcome_2">Barva každé nabíječky odpovídá jejímu maximálnímu nabíjecímu výkonu</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d kompatibilní platební metoda</item>
|
||||
<item quantity="few">%d kompatibilní platební metody</item>
|
||||
<item quantity="other">%d kompatibilních platebních metod</item>
|
||||
</plurals>
|
||||
<string name="verified">ověřeno</string>
|
||||
<string name="verified_desc">Funkčnost nabíječky byla potvrzena členem komunity %s</string>
|
||||
<string name="donation_dialog_title">Děkujeme, že používáte EVMap</string>
|
||||
<string name="save_as_profile">Uložit jako profil</string>
|
||||
<string name="donation_dialog_detail">EVMap je bezplatná open-source aplikace. Příspěvky do kódu na GitHubu jsou velmi vítány. Abyste pomohli pokrýt provozní náklady na přístup k datům, zvažte prosím možnost darovat vývojáři částku dle vlastního výběru.</string>
|
||||
<string name="pref_chargeprice_currency">Měna</string>
|
||||
<string name="chargeprice_battery_range_to">do</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
<item quantity="one">(bude zvýrazněno v porovnání cen)</item>
|
||||
<item quantity="few">(budou zvýrazněna v porovnání cen)</item>
|
||||
<item quantity="other">(bude zvýrazněno v porovnání cen)</item>
|
||||
</plurals>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Společnosti poskytující veřejné služby někdy nabízejí speciální plány pro své zákazníky</string>
|
||||
<string name="chargeprice_title">Ceny</string>
|
||||
<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">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>
|
||||
<string name="tesla_pricing_members">Vozidla Tesla a členové:</string>
|
||||
<string name="pricing_up_to">až %s</string>
|
||||
<string name="pref_units_default">Podle zařízení</string>
|
||||
<string name="auto_location_service">EVMap běží na Android Auto a používá vaši polohu.</string>
|
||||
<string name="open_in_app">Otevřít v aplikaci</string>
|
||||
<string name="auto_location_permission_needed">Pro spuštění aplikace EVMap na Android Auto musíte udělit přístup ke své poloze.</string>
|
||||
<string name="auto_vehicle_data_permission_needed">Pro použití této funkce potřebuje aplikace EVMap přístup k datům vašeho vozidla.</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Několik vozidel vybraných v aplikaci se shoduje s tímto vozidlem (%1$s %2$s).</string>
|
||||
<string name="auto_multipage">(%1$d/%2$d)</string>
|
||||
<string name="referral_tesla">Tesla</string>
|
||||
<string name="status_available">Dostupná</string>
|
||||
<string name="status_occupied">Obsazená</string>
|
||||
<string name="status_charging">Nabíjení</string>
|
||||
<string name="status_faulted">Mimo provoz</string>
|
||||
<string name="status_unknown">Stav neznámý</string>
|
||||
<string name="status_since">%1$s od %2$s</string>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -368,4 +368,14 @@
|
||||
<string name="referral_tesla">Tesla</string>
|
||||
<string name="generic_connection_error">Daten konnten nicht geladen werden</string>
|
||||
<string name="copied">In Zwischenablage kopiert</string>
|
||||
<string name="status_available">Verfügbar</string>
|
||||
<string name="status_occupied">Besetzt</string>
|
||||
<string name="status_charging">Lädt</string>
|
||||
<string name="status_faulted">Defekt</string>
|
||||
<string name="status_unknown">Status unbekannt</string>
|
||||
<string name="status_since">%1$s seit %2$s</string>
|
||||
<string name="charger_name">Ladestationsname</string>
|
||||
<string name="pref_chargeprice_native_integration">Preisvergleich in EVMap</string>
|
||||
<string name="pref_chargeprice_native_integration_on">Preise werden direkt in EVMap angezeigt</string>
|
||||
<string name="pref_chargeprice_native_integration_off">Preisvergleich verlinkt auf die App oder Website von Chargeprice</string>
|
||||
</resources>
|
||||
@@ -331,4 +331,46 @@
|
||||
<string name="loading">Chargement…</string>
|
||||
<string name="auto_multipage_goto">Page %d</string>
|
||||
<string name="auto_multipage">(%1$d/%2$d)</string>
|
||||
<string name="tesla_pricing_other_times">Autres horaires :</string>
|
||||
<string name="settings_cache_clear_summary">Supprime tous les chargeurs mis en cache, sauf les favoris</string>
|
||||
<string name="tesla_pricing_owners">Véhicules Tesla uniquement :</string>
|
||||
<string name="settings_cache_clear">Effacer le cache</string>
|
||||
<string name="charge_price_minute_format">%1$.2f %2$s/min</string>
|
||||
<string name="settings_cache_count_summary">%1$d chargeurs mis en cache, %2$.1f MB</string>
|
||||
<string name="average_utilization">Utilisation moyenne</string>
|
||||
<string name="website">Site web</string>
|
||||
<string name="pref_tesla_account_disabled">Connectez-vous pour voir les données en temps réel pour les Superchargeurs Tesla. Pas de véhicule Tesla nécessaire</string>
|
||||
<string name="pref_tesla_account_enabled">Connecté en tant que %s</string>
|
||||
<string name="login_error">Connexion échouée</string>
|
||||
<string name="tesla_pricing_blocking_fee">Frais de blocage : %s</string>
|
||||
<string name="pref_map_scale">Afficher l\'échelle de la carte</string>
|
||||
<string name="data_retrieved_at">Données récupérées %s</string>
|
||||
<string name="prediction_only">(%s seulement)</string>
|
||||
<string name="pref_units">Unités</string>
|
||||
<string name="referrals_info">Vous pouvez également utiliser l\'un des liens d\'affiliation ci-dessous pour soutenir le développeur avec votre achat.</string>
|
||||
<string name="powered_by_fronyx">propulsé par fronyx</string>
|
||||
<string name="log_out">Déconnexion</string>
|
||||
<string name="logging_in">Connexion…</string>
|
||||
<string name="location_status">Statut du fournisseur de localisation</string>
|
||||
<string name="pref_units_imperial">Impérial</string>
|
||||
<string name="copied">Copié dans le presse-papiers</string>
|
||||
<string name="settings_caching">Caching</string>
|
||||
<string name="realtime_data_login_needed">Compte Tesla nécessaire pour les données en temps réel</string>
|
||||
<string name="generic_connection_error">Impossible de charger les données</string>
|
||||
<string name="logged_out">Déconnecté</string>
|
||||
<string name="tesla_pricing_members">Véhicules Tesla & membres :</string>
|
||||
<string name="pref_units_metric">Métrique</string>
|
||||
<string name="pricing_up_to">jusqu\'à %s</string>
|
||||
<string name="pref_tesla_account">Compte Tesla</string>
|
||||
<string name="reload">Actualiser</string>
|
||||
<string name="charger_website">Site web</string>
|
||||
<string name="tesla_pricing_others">Autres clients :</string>
|
||||
<string name="referral_tesla">Tesla</string>
|
||||
<string name="pref_units_default">Réglage par défaut de l\'appareil</string>
|
||||
<string name="pref_map_scale_meters_and_miles">Milles et mètres sur l\'échelle de la carte</string>
|
||||
<string name="login">Se connecter</string>
|
||||
<string name="auto_chargers_ahead">Uniquement les stations de recharge dans le sens de la marche</string>
|
||||
<string name="referrals">Liens d\'affiliation</string>
|
||||
<string name="settings_cache_count">Taille du cache</string>
|
||||
<string name="accept_privacy"><![CDATA[J\'ai lu et accepté la politique de confidentialité <a href=\"%s\"> d\'EVMap</a>.]]></string>
|
||||
</resources>
|
||||
@@ -360,7 +360,7 @@
|
||||
<string name="auto_chargeprice_vehicle_unknown">Nenhum dos veículos selecionados na app corresponde a este veículo (%1$s %2$s).</string>
|
||||
<string name="auto_chargers_ahead">Apenas carregadores na direção do destino</string>
|
||||
<string name="sounds_cool">Continuar</string>
|
||||
<string name="reload">Recarregar</string>
|
||||
<string name="reload">Atualizar informação</string>
|
||||
<string name="accept_privacy"><![CDATA[Li e aceito a <a href=\"%s\">política de privacidade</a> do EVMap.]]></string>
|
||||
<string name="referrals">Links de afiliado</string>
|
||||
<string name="referral_tesla">Tesla</string>
|
||||
@@ -372,4 +372,15 @@
|
||||
<string name="referrals_info">Também pode usar um dos seguintes links de afiliado para apoiar o desenvolvedor da app com a sua compra.</string>
|
||||
<string name="generic_connection_error">Não foi possível carregar a informação</string>
|
||||
<string name="powered_by_fronyx">previsão feita por fronyx</string>
|
||||
<string name="copied">Informação copiada</string>
|
||||
<string name="charger_name">Nome do carregador</string>
|
||||
<string name="status_available">Disponível</string>
|
||||
<string name="status_occupied">Ocupado</string>
|
||||
<string name="status_charging">Carregando</string>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -4,6 +4,7 @@
|
||||
<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>
|
||||
@@ -14,6 +15,7 @@
|
||||
<item>default</item>
|
||||
<item>en</item>
|
||||
<item>de</item>
|
||||
<item>cs</item>
|
||||
<item>fr</item>
|
||||
<item>nb-NO</item>
|
||||
<item>nl</item>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<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
|
||||
@@ -35,6 +36,16 @@
|
||||
<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="copyright_summary">©2020–2023 Johan von Forstner and contributors</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">©2020–2024 Johan von Forstner and contributors</string>
|
||||
<string name="acra_backend_url" translatable="false">https://acra.muc.vonforst.net/report</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -368,4 +368,14 @@
|
||||
<string name="referral_tesla">Tesla</string>
|
||||
<string name="generic_connection_error">Could not load data</string>
|
||||
<string name="copied">Copied to clipboard</string>
|
||||
<string name="status_available">Available</string>
|
||||
<string name="status_occupied">Occupied</string>
|
||||
<string name="status_charging">Charging</string>
|
||||
<string name="status_faulted">Out of order</string>
|
||||
<string name="status_unknown">Status Unknown</string>
|
||||
<string name="status_since">%1$s since %2$s</string>
|
||||
<string name="charger_name">Charger name</string>
|
||||
<string name="pref_chargeprice_native_integration">Price comparison within EVMap</string>
|
||||
<string name="pref_chargeprice_native_integration_on">Pricing data will be shown directly in EVMap</string>
|
||||
<string name="pref_chargeprice_native_integration_off">Price comparison button will refer to the Chargeprice app or website</string>
|
||||
</resources>
|
||||
@@ -15,6 +15,7 @@
|
||||
<item name="preferenceTheme">@style/AppTheme.Preference</item>
|
||||
<item name="alertDialogTheme">@style/AppTheme.AlertDialog</item>
|
||||
<item name="materialAlertDialogTheme">@style/AppTheme.AlertDialog</item>
|
||||
<item name="snackbarButtonStyle">@style/Button.TextButton.Snackbar.App</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Preference" parent="@style/PreferenceThemeOverlay">
|
||||
@@ -82,6 +83,10 @@
|
||||
<item name="backgroundInsetBottom">24dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Button.TextButton.Snackbar.App" parent="Widget.Material3.Button.TextButton.Snackbar">
|
||||
<item name="android:textColor">@color/colorPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="CarAppTheme">
|
||||
<item name="carColorPrimary">@color/colorPrimary</item>
|
||||
<item name="carColorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<CheckBoxPreference
|
||||
android:key="chargeprice_native_integration"
|
||||
android:title="@string/pref_chargeprice_native_integration"
|
||||
android:summaryOn="@string/pref_chargeprice_native_integration_on"
|
||||
android:summaryOff="@string/pref_chargeprice_native_integration_off"
|
||||
app:defaultValue="true" />
|
||||
<net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
android:key="chargeprice_my_vehicle"
|
||||
android:title="@string/pref_my_vehicle"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(e.g. in the debug version). -->
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetPackage="${applicationId}"
|
||||
android:targetPackage="net.vonforst.evmap"
|
||||
android:targetClass="net.vonforst.evmap.MapsActivity">
|
||||
<extra
|
||||
android:name="favorites"
|
||||
|
||||
@@ -7,4 +7,10 @@ fun addDebugInterceptors(context: Context) {
|
||||
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder = this
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder = this
|
||||
|
||||
object EspressoIdlingResource {
|
||||
fun increment() {}
|
||||
|
||||
fun decrement() {}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ class NewMotionAvailabilityDetectorTest {
|
||||
fun apiTest() {
|
||||
for (chargepoint in listOf(2105L, 18284L)) {
|
||||
val charger = runBlocking { api.getChargepointDetail(chargepoint).body()!! }
|
||||
.chargelocations[0].convert("", true) as ChargeLocation
|
||||
.chargelocations!![0].convert("", true) as ChargeLocation
|
||||
println(charger)
|
||||
|
||||
runBlocking {
|
||||
|
||||
@@ -60,7 +60,7 @@ class ChargepriceApiTest {
|
||||
fun apiTest() {
|
||||
for (chargepoint in listOf(2105L, 18284L)) {
|
||||
val charger = runBlocking { ge.getChargepointDetail(chargepoint).body()!! }
|
||||
.chargelocations[0].convert("", true) as ChargeLocation
|
||||
.chargelocations!![0].convert("", true) as ChargeLocation
|
||||
println(charger)
|
||||
|
||||
runBlocking {
|
||||
|
||||
@@ -63,8 +63,8 @@ class GoingElectricApiTest {
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(null, body.startkey)
|
||||
assertEquals(1, body.chargelocations.size)
|
||||
val charger = body.chargelocations[0] as GEChargeLocation
|
||||
assertEquals(1, body.chargelocations!!.size)
|
||||
val charger = body.chargelocations!![0] as GEChargeLocation
|
||||
assertEquals(2105, charger.id)
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ class GoingElectricApiTest {
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(null, body.startkey)
|
||||
assertEquals(1, body.chargelocations.size)
|
||||
val charger = body.chargelocations[0] as GEChargeLocation
|
||||
assertEquals(1, body.chargelocations!!.size)
|
||||
val charger = body.chargelocations!![0] as GEChargeLocation
|
||||
assertEquals(34210, charger.id)
|
||||
assertEquals(LocalTime.MIN, charger.openinghours!!.days!!.monday.start)
|
||||
assertEquals(LocalTime.MAX, charger.openinghours!!.days!!.monday.end)
|
||||
@@ -92,8 +92,8 @@ class GoingElectricApiTest {
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(null, body.startkey)
|
||||
assertEquals(2, body.chargelocations.size)
|
||||
val charger = body.chargelocations[0] as GEChargeLocation
|
||||
assertEquals(2, body.chargelocations!!.size)
|
||||
val charger = body.chargelocations!![0] as GEChargeLocation
|
||||
assertEquals(41161, charger.id)
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class GoingElectricApiTest {
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(null, body.startkey)
|
||||
assertEquals(0, body.chargelocations.size)
|
||||
assertEquals(0, body.chargelocations!!.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -118,8 +118,8 @@ class GoingElectricApiTest {
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(2, body.startkey)
|
||||
assertEquals(2, body.chargelocations.size)
|
||||
val charger = body.chargelocations[0] as GEChargeLocation
|
||||
assertEquals(2, body.chargelocations!!.size)
|
||||
val charger = body.chargelocations!![0] as GEChargeLocation
|
||||
assertEquals(41161, charger.id)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import org.robolectric.annotation.internal.DoNotInstrument
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@DoNotInstrument
|
||||
@Config(sdk = [33]) // Robolectric does not yet support SDK 34
|
||||
class CarAppTest {
|
||||
private val testCarContext =
|
||||
TestCarContext.createCarContext(ApplicationProvider.getApplicationContext()).apply {
|
||||
@@ -10,11 +10,10 @@ buildscript {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.1.2")
|
||||
classpath("com.android.tools.build:gradle:8.2.2")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$aboutLibsVersion")
|
||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
|
||||
classpath("pt.jcosta.resourceplaceholders:plugin:0.7")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
@@ -27,6 +26,9 @@ allprojects {
|
||||
mavenCentral()
|
||||
//noinspection JcenterRepositoryObsolete
|
||||
maven { setUrl("https://jitpack.io") }
|
||||
maven {
|
||||
setUrl("https://raw.githubusercontent.com/ev-map/mapbox-gl-native-android/mvn")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ Not all API keys are strictly required if you only want to work on certain parts
|
||||
example, you can choose only one of the map providers and one of the charging station databases. The
|
||||
Chargeprice API key is also only required if you want to test the price comparison feature.
|
||||
|
||||
All APIs can be used for free, at least for testing. Some APIs require payment above a certain usage
|
||||
limit or to get access to the full dataset, but the free tiers should be plenty for local testing
|
||||
and development.
|
||||
Most APIs can be used for free, at least for testing. Some APIs require payment above a certain
|
||||
usage limit or to get access to the full dataset, but the free tiers should be plenty for local
|
||||
testing and development.
|
||||
|
||||
Below you find a list of all the services and how to obtain the API keys.
|
||||
|
||||
@@ -185,7 +185,7 @@ Availability data providers
|
||||
<details>
|
||||
<summary>How to obtain an API key</summary>
|
||||
|
||||
The API is not publically available, contact [fronyx](https://fronyx.io/contact-us/) to get an API
|
||||
The API is not publicly available, contact [fronyx](https://fronyx.io/contact-us/) to get an API
|
||||
key and documentation.
|
||||
|
||||
If you don't want to test this functionality, simply leave the API key blank.
|
||||
|
||||
18
fastlane/metadata/android/cs-CZ/full_description.txt
Normal file
18
fastlane/metadata/android/cs-CZ/full_description.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
Pomocí aplikace EVMap můžete pohodlně najít nabíječky elektromobilů s vaším Android mobilem. Poskytuje mobilní přístup do komunitních databází GoingElectric.de a Open Charge Map, které obsahují informace o nabíjecích místech po celém světě. U mnoha nabíjecích míst v Evropě si můžete zobrazit informace o jejich stavu v reálném čase.
|
||||
|
||||
Funkce:
|
||||
- Material Design
|
||||
- Zobrazuje všechny nabíjecí stanice z komunitou spravovaných databází GoingElectric.de a Open Charge Map.
|
||||
- Informace o dostupnosti v reálném čase (pouze v Evropě)
|
||||
- Integrované srovnání cen pomocí Chargeprice.app (pouze v Evropě)
|
||||
- Mapové podklady z OpenStreetMap (Mapbox)
|
||||
- Vyhledávání míst
|
||||
- Pokročilé možnosti filtrování, včetně uložených profilů filtrů
|
||||
- Seznam oblíbených, také s informacemi o dostupnosti
|
||||
- Žádné reklamy, plně otevřený zdrojový kód
|
||||
|
||||
EVMap je projekt s otevřeným zdrojovým kódem a najdete jej na adrese https://github.com/ev-map/EVMap.
|
||||
|
||||
Tato aplikace není oficiálním produktem GoingElectric.de ani Open Charge Map, využívá pouze jejich veřejná API.
|
||||
|
||||
Seznam potřebných oprávnění s vysvětlivkami je k dispozici zde: https://ev-map.app/faq/#permissions
|
||||
1
fastlane/metadata/android/cs-CZ/short_description.txt
Normal file
1
fastlane/metadata/android/cs-CZ/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Najděte nabíjecí stanice pro elektromobily
|
||||
1
fastlane/metadata/android/cs-CZ/title.txt
Normal file
1
fastlane/metadata/android/cs-CZ/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
EVMap - nabíjení elektromobilů
|
||||
13
fastlane/metadata/android/de-DE/changelogs/208.txt
Normal file
13
fastlane/metadata/android/de-DE/changelogs/208.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Neue Funktionen:
|
||||
- Tippe auf einen Anschluss um Details zur Belegung zu sehen
|
||||
- Name der Ladestation gedrückt halten um ihn zu kopieren
|
||||
- Neue Übersetzung: Tschechisch
|
||||
|
||||
Verbesserungen:
|
||||
- Echtzeitdaten für Tesla Supercharger in einigen Ländern nun auch ohne Login verfügbar
|
||||
- Links zu map.openchargemap.io können in der App geöffnet werden
|
||||
- Mapbox: Verkehrsdaten für beliebige Kartenstile verfügbar
|
||||
|
||||
Fehler behoben:
|
||||
- Anzeigefehler behoben
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/210.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/210.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
6
fastlane/metadata/android/de-DE/changelogs/212.txt
Normal file
6
fastlane/metadata/android/de-DE/changelogs/212.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Verbesserungen:
|
||||
- Option "Sofort navigieren" wird nur angezeigt, wenn kompatible Navigationsapp installiert ist
|
||||
|
||||
Fehler behoben:
|
||||
- OpenChargeMap: Korrektur des Verhaltens der Kartenmarker bei Filter nach Betreiber
|
||||
- Abstürze behoben
|
||||
13
fastlane/metadata/android/en-US/changelogs/208.txt
Normal file
13
fastlane/metadata/android/en-US/changelogs/208.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
New features:
|
||||
- Tap on a connector type to see more details about its status
|
||||
- Copy charger name using long press
|
||||
- New translation: Czech
|
||||
|
||||
Improvements:
|
||||
- Realtime data for Tesla Superchargers also available without login in some countries
|
||||
- Links to map.openchargemap.io can be opened in the app
|
||||
- Mapbox: Traffic data available for all map styles
|
||||
|
||||
Bugfixes:
|
||||
- Fixed display errors
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/210.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/210.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user