mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 08:07:46 -05:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea906ec969 | ||
|
|
ec2b6d4f28 | ||
|
|
e7c2683ee2 | ||
|
|
d76051ec3a | ||
|
|
975ba2bcce | ||
|
|
dc067fd86b | ||
|
|
226ca3a60e | ||
|
|
af63ee350b | ||
|
|
21d4060ac9 | ||
|
|
3b9efa0302 | ||
|
|
95d93af0d6 | ||
|
|
17a6a253d4 | ||
|
|
f73545c01e | ||
|
|
e4fa1f2c78 | ||
|
|
b2b5cc63e8 | ||
|
|
84ba62f755 | ||
|
|
b29653049a | ||
|
|
4159491589 | ||
|
|
4e67f434cd | ||
|
|
5e58d52a0d | ||
|
|
eddc1f9b61 | ||
|
|
b5054b4dc9 | ||
|
|
926799bb1d | ||
|
|
f038138620 | ||
|
|
1c44e5ae3d | ||
|
|
c58543fe3f | ||
|
|
a5db42322f | ||
|
|
bb0d2e35d4 | ||
|
|
38c8c5510f | ||
|
|
8d1d15ad68 | ||
|
|
954203bf18 | ||
|
|
524e9fcfc0 | ||
|
|
ae2041d26b | ||
|
|
698c832518 | ||
|
|
17c1a11675 | ||
|
|
d04661e925 | ||
|
|
02316fceb9 | ||
|
|
9bf7a90302 | ||
|
|
2697389b49 | ||
|
|
cd0e381707 | ||
|
|
e5ed5eeafe | ||
|
|
b25c61fbea | ||
|
|
d472be1676 | ||
|
|
24fa85929e | ||
|
|
4a67ffd956 | ||
|
|
fab66d1f84 | ||
|
|
0783c6c272 | ||
|
|
c5714c8592 | ||
|
|
cb4b571721 | ||
|
|
0bfa80bbe0 | ||
|
|
d77f13682d | ||
|
|
0c19eb5833 | ||
|
|
a5abedae55 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
- name: Decrypt keystore
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 Johan von Forstner
|
||||
Copyright (c) 2020-2023 Johan von Forstner and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
<svg id="Ebene_5" data-name="Ebene 5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style>.cls-1,.cls-2{fill:none;}.cls-2{stroke:#000;stroke-miterlimit:10;stroke-width:2px;}
|
||||
</style>
|
||||
</defs>
|
||||
<title>connector_supercharger</title>
|
||||
<path class="cls-1" d="M12,12H36V36H12Z" />
|
||||
<path class="cls-2"
|
||||
d="M13.45,17.08a8.24,8.24,0,0,1-3.11.6,8.34,8.34,0,0,1-6-14.18H16.3a8.35,8.35,0,0,1,1.07,10.33" />
|
||||
<circle cx="10.34" cy="9.34" r="1.67" />
|
||||
<circle cx="15.35" cy="9.34" r="1.67" />
|
||||
<circle cx="12.84" cy="13.51" r="1.67" />
|
||||
<circle cx="7.84" cy="13.51" r="1.67" />
|
||||
<circle cx="5.34" cy="9.34" r="1.67" />
|
||||
<circle cx="7.84" cy="5.59" r="1" />
|
||||
<circle cx="12.84" cy="5.59" r="1.04" />
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_5" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24"
|
||||
style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;fill:none;}
|
||||
.st1{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<path class="st0" d="M12,12h24v24H12V12z" />
|
||||
<path class="st1"
|
||||
d="M6.2,13.8C4.1,10.6,4.6,6.3,7.3,3.5h12c1.5,1.6,2.4,3.7,2.4,5.9c0,4.6-3.8,8.3-8.4,8.3c-1.1,0-2.1-0.2-3.1-0.6" />
|
||||
<circle cx="13.3" cy="9.3" r="1.7" />
|
||||
<circle cx="8.3" cy="9.3" r="1.7" />
|
||||
<circle cx="10.8" cy="13.5" r="1.7" />
|
||||
<circle cx="15.8" cy="13.5" r="1.7" />
|
||||
<circle cx="18.3" cy="9.3" r="1.7" />
|
||||
<circle cx="15.8" cy="5.6" r="1" />
|
||||
<circle cx="10.8" cy="5.6" r="1" />
|
||||
<g id="T">
|
||||
<path id="path35"
|
||||
d="M18.18,22.23l1-5.48c.93,0,1.22.1,1.27.52a2.15,2.15,0,0,0,.93-.7,6.91,6.91,0,0,0-2.46-.6l-.71.88h0L17.46,16a7,7,0,0,0-2.46.6,2.22,2.22,0,0,0,.94.7c0-.42.33-.52,1.26-.52l1,5.48" />
|
||||
<path id="path37"
|
||||
d="M18.18,15.72a7.9,7.9,0,0,1,3.28.66,2.65,2.65,0,0,0,.2-.4,9.24,9.24,0,0,0-7,0,2.61,2.61,0,0,0,.19.4,7.94,7.94,0,0,1,3.29-.66h0" />
|
||||
</g>
|
||||
</svg>
|
||||
<path id="path35" d="M5.4,22.3l1-5.5c0.9,0,1.3,0.1,1.3,0.5c0.4-0.1,0.7-0.4,0.9-0.7C7.8,16.3,7,16,6.1,16l-0.8,0.8l0,0L4.7,16
|
||||
c-0.8,0-1.7,0.3-2.5,0.6c0.2,0.3,0.6,0.6,0.9,0.7c0.1-0.4,0.3-0.5,1.3-0.5L5.4,22.3" />
|
||||
<path id="path37" d="M5.5,15.7L5.5,15.7c1.1,0,2.3,0.2,3.3,0.7c0.1-0.1,0.1-0.3,0.2-0.4c-2.2-0.9-4.8-0.9-7,0
|
||||
c0.1,0.1,0.1,0.3,0.2,0.4C3.2,15.9,4.3,15.7,5.5,15.7" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -9,7 +9,7 @@ apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
|
||||
def supportedLocales = "en,de,fr,nb-rNO,nl"
|
||||
def supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
@@ -20,8 +20,8 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 166
|
||||
versionName "1.4.9"
|
||||
versionCode 172
|
||||
versionName "1.5.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs supportedLocales.split(',')
|
||||
@@ -90,6 +90,12 @@ android {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs).configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding true
|
||||
@@ -156,24 +162,25 @@ configurations {
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.6.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.5"
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation "androidx.activity:activity-ktx:1.7.2"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.7"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'androidx.browser:browser:1.5.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation 'com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.13.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.11.0'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.15.0'
|
||||
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
|
||||
implementation 'io.coil-kt:coil:1.1.0'
|
||||
implementation 'com.github.ev-map:StfalconImageViewer:5082ebd392'
|
||||
@@ -208,7 +215,7 @@ dependencies {
|
||||
fossImplementation 'com.github.ev-map:mapbox-events-android:a21c324501'
|
||||
|
||||
// Google Places
|
||||
googleImplementation 'com.google.android.libraries.places:places:3.0.0'
|
||||
googleImplementation 'com.google.android.libraries.places:places:3.1.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
|
||||
|
||||
// Mapbox Geocoding
|
||||
@@ -219,18 +226,18 @@ dependencies {
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// viewmodel library
|
||||
def lifecycle_version = "2.5.1"
|
||||
def lifecycle_version = "2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.5.0"
|
||||
def room_version = "2.5.1"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// billing library
|
||||
def billing_version = "5.1.0"
|
||||
def billing_version = "6.0.0"
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
@@ -241,26 +248,27 @@ dependencies {
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
|
||||
debugImplementation 'com.facebook.flipper:flipper:0.190.0'
|
||||
debugImplementation 'com.facebook.soloader:soloader:0.10.5'
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.190.0'
|
||||
|
||||
// testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.11.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
|
||||
// testing for car app
|
||||
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.9'
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.9.2'
|
||||
testGoogleImplementation 'androidx.test:core:1.5.0'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.15.0"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
}
|
||||
|
||||
private static String decode(String s, String key) {
|
||||
|
||||
9
app/src/debug/AndroidManifest.xml
Normal file
9
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||
android:exported="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
42
app/src/debug/java/net/vonforst/evmap/DebugInits.kt
Normal file
42
app/src/debug/java/net/vonforst/evmap/DebugInits.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
private val networkFlipperPlugin = NetworkFlipperPlugin()
|
||||
|
||||
fun addDebugInterceptors(context: Context) {
|
||||
if (Build.FINGERPRINT == "robolectric") return
|
||||
|
||||
SoLoader.init(context, false)
|
||||
val client = AndroidFlipperClient.getInstance(context)
|
||||
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
|
||||
client.addPlugin(networkFlipperPlugin)
|
||||
client.addPlugin(DatabasesFlipperPlugin(context))
|
||||
client.addPlugin(SharedPreferencesFlipperPlugin(context))
|
||||
client.start()
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
||||
// Flipper does not work during unit tests - so check whether we are running tests first
|
||||
var isRunningTest = true
|
||||
try {
|
||||
Class.forName("org.junit.Test")
|
||||
} catch (e: ClassNotFoundException) {
|
||||
isRunningTest = false
|
||||
}
|
||||
|
||||
if (!isRunningTest) {
|
||||
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
|
||||
}
|
||||
return this
|
||||
}
|
||||
6
app/src/foss/res/values-pt/strings.xml
Normal file
6
app/src/foss/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap (Mapbox).</string>
|
||||
<string name="donate_paypal">Doar com o PayPal</string>
|
||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
|
||||
</resources>
|
||||
6
app/src/foss/res/values-ro/strings.xml
Normal file
6
app/src/foss/res/values-ro/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Crezi ca EVMap este util? Sprijina dezvoltarea printr-o donatie pentru dezvoltator.</string>
|
||||
<string name="donate_paypal">Doneaza cu PayPal</string>
|
||||
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
@@ -275,7 +275,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||
meta = metaMapped.chargePoints.maxByOrNull { it.power }
|
||||
|
||||
prices = result.data!!.map { cp ->
|
||||
prices = result.data!!.mapNotNull { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == chargepoint.plug && it.power == chargepoint.power
|
||||
@@ -287,7 +287,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
chargepointPrices = filteredPrices
|
||||
)
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
|
||||
@@ -23,8 +23,8 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
@@ -57,6 +57,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
||||
|
||||
private val imageSize = 128 // images should be 128dp according to docs
|
||||
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
|
||||
@@ -465,7 +466,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
|
||||
invalidate()
|
||||
|
||||
availability = getAvailability(charger).data
|
||||
availability = availabilityRepo.getAvailability(charger).data
|
||||
|
||||
invalidate()
|
||||
} else {
|
||||
|
||||
@@ -24,8 +24,8 @@ import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
@@ -79,6 +79,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
@@ -325,7 +326,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
|
||||
// power
|
||||
val power = charger.maxPower;
|
||||
val power = charger.maxPower
|
||||
if (power != null) {
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${power.roundToInt()} kW")
|
||||
@@ -577,7 +578,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
|
||||
override fun onItemVisibilityChanged(startIndex: Int, endIndex: Int) {
|
||||
// when the list is scrolled, load corresponding availabilities
|
||||
if (startIndex == visibleStart && endIndex == visibleEnd && !availabilities.isEmpty()) return
|
||||
if (startIndex == visibleStart && endIndex == visibleEnd && availabilities.isNotEmpty()) return
|
||||
if (startIndex == -1 || endIndex == -1) return
|
||||
if (availabilityUpdateCoroutine != null) return
|
||||
|
||||
@@ -606,7 +607,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
lifecycleScope.async {
|
||||
val availability = getAvailability(it).data
|
||||
val availability = availabilityRepo.getAvailability(it).data
|
||||
val date = ZonedDateTime.now()
|
||||
availabilities[it.id] = date to availability
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
): List<AutocompletePlace> {
|
||||
val request = FindAutocompletePredictionsRequest.builder().apply {
|
||||
if (location != null) {
|
||||
setLocationBias(calcLocationBias(location))
|
||||
setOrigin(LatLng(location.latitude, location.longitude))
|
||||
locationBias = calcLocationBias(location)
|
||||
origin = LatLng(location.latitude, location.longitude)
|
||||
}
|
||||
setSessionToken(token)
|
||||
sessionToken = token
|
||||
setQuery(query)
|
||||
}.build()
|
||||
try {
|
||||
@@ -92,10 +92,11 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAttributionString(): Int = R.string.places_powered_by_google
|
||||
override fun getAttributionString(): Int =
|
||||
com.google.android.libraries.places.R.string.places_powered_by_google
|
||||
|
||||
override fun getAttributionImage(dark: Boolean): Int =
|
||||
if (dark) R.drawable.places_powered_by_google_dark else R.drawable.places_powered_by_google_light
|
||||
if (dark) com.google.android.libraries.places.R.drawable.places_powered_by_google_dark else com.google.android.libraries.places.R.drawable.places_powered_by_google_light
|
||||
|
||||
private fun calcLocationBias(location: com.car2go.maps.model.LatLng): RectangularBounds {
|
||||
val radius = 100e3 // meters
|
||||
|
||||
@@ -3,12 +3,8 @@ package net.vonforst.evmap.fragment.preference
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.RangeSliderPreference
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
import java.text.NumberFormat
|
||||
|
||||
class AndroidAutoSettingsFragment : BaseSettingsFragment() {
|
||||
|
||||
41
app/src/google/res/values-pt/strings.xml
Normal file
41
app/src/google/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="auto_no_chargers_found">Não foram encontrados carregadores próximo de si</string>
|
||||
<string name="auto_no_favorites_found">Nenhum favorito encontrado</string>
|
||||
<string name="opened_on_phone">Aberto no telefone</string>
|
||||
<string name="auto_location_permission_needed">Para usar o EVMap no Android Auto, permita o acesso à sua localização.</string>
|
||||
<string name="open_in_app">Abrir na app</string>
|
||||
<string name="auto_vehicle_data_permission_needed">Para esta funcionalidade, o EVMap precisa de aceder aos dados do seu veículo.</string>
|
||||
<string name="auto_chargers_closeby">Carregadores próximos</string>
|
||||
<string name="grant_on_phone">Conceda permissões no telefone</string>
|
||||
<string name="auto_chargers_near_location">Perto de %s</string>
|
||||
<string name="auto_favorites">Favoritos</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Vários veículos selecionados na app correspondem a este veículo (%1$s %2$s).</string>
|
||||
<string name="selecting_none">todos os items desmarcados</string>
|
||||
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap (Mapbox) nas definições da app.</string>
|
||||
<string name="selecting_all">todos os items selecionados</string>
|
||||
<string name="loading">Carregando…</string>
|
||||
<string name="auto_multipage_goto">Página %d</string>
|
||||
<string name="auto_multipage">(%d/%d)</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Escala de carregamento para comparação de preços</string>
|
||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.
|
||||
\n
|
||||
\nA Google cobra 15% de cada doação.</string>
|
||||
<string name="auto_location_service">O EVMap está a funcionar no Android Auto e usando a sua localização.</string>
|
||||
<string name="auto_fault_report_date">⚠️ Problemas (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Não é possível atualizar. Por favor volte atrás e reinicie.</string>
|
||||
<string name="auto_prices">Preços</string>
|
||||
<string name="auto_vehicle_data">Dados do veículo</string>
|
||||
<string name="auto_charging_level">Nível de carregamento</string>
|
||||
<string name="auto_no_data">Não disponível</string>
|
||||
<string name="auto_speed">Velocidade</string>
|
||||
<string name="auto_heading">Direção</string>
|
||||
<string name="auto_settings">Definições</string>
|
||||
<string name="welcome_android_auto">Suporte para Android Auto</string>
|
||||
<string name="auto_range">Alcance</string>
|
||||
<string name="welcome_android_auto_detail">Também pode usar o EVMap no Android Auto em carros compatíveis. Basta selecionar a app EVMap no menu do Android Auto.</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">O EVMap não pôde determinar o modelo do seu veículo.</string>
|
||||
<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>
|
||||
</resources>
|
||||
2
app/src/google/res/values-ro/strings.xml
Normal file
2
app/src/google/res/values-ro/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
5
app/src/googleAutomotive/res/values-pt/strings.xml
Normal file
5
app/src/googleAutomotive/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Permitir</string>
|
||||
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
|
||||
</resources>
|
||||
2
app/src/googleAutomotive/res/values-ro/strings.xml
Normal file
2
app/src/googleAutomotive/res/values-ro/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -23,6 +23,8 @@
|
||||
<application
|
||||
android:name=".EvMapApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:dataExtractionRules="@xml/backup_rules_api31"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.stetho.Stetho
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
@@ -24,8 +23,8 @@ class EvMapApplication : Application() {
|
||||
prefs.language = null
|
||||
}
|
||||
|
||||
Stetho.initializeWithDefaults(this);
|
||||
init(applicationContext)
|
||||
addDebugInterceptors(applicationContext)
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
initAcra {
|
||||
|
||||
@@ -55,8 +55,8 @@ class MapsActivity : AppCompatActivity(),
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val splashScreen = installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_maps)
|
||||
|
||||
@@ -207,7 +207,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
|
||||
intent.`package` = "com.google.android.apps.maps"
|
||||
if (prefs.navigateUseMaps && intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent);
|
||||
startActivity(intent)
|
||||
} else {
|
||||
// fallback: generic geo intent
|
||||
showLocation(charger)
|
||||
@@ -223,7 +223,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
})"
|
||||
)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent);
|
||||
startActivity(intent)
|
||||
} else {
|
||||
val cb = fragmentCallback ?: return
|
||||
Snackbar.make(
|
||||
@@ -262,7 +262,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
|
||||
fun shareUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
setType("text/plain")
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
}
|
||||
startActivity(intent)
|
||||
|
||||
@@ -90,7 +90,7 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
class ChargepriceAdapter() :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
|
||||
val viewPool = RecyclerView.RecycledViewPool();
|
||||
val viewPool = RecyclerView.RecycledViewPool()
|
||||
var meta: ChargepriceChargepointMeta? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.joinToSpannedString
|
||||
import net.vonforst.evmap.model.ChargeCard
|
||||
@@ -10,6 +15,7 @@ import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.OpeningHoursDays
|
||||
import net.vonforst.evmap.plus
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import net.vonforst.evmap.utils.formatDMS
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import java.time.ZoneId
|
||||
@@ -41,11 +47,18 @@ fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
teslaPricing: TeslaGraphQlApi.Pricing?,
|
||||
ctx: Context
|
||||
): List<DetailsAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
|
||||
return listOfNotNull(
|
||||
if (teslaPricing != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_tesla,
|
||||
R.string.cost,
|
||||
formatTeslaPricing(teslaPricing, ctx),
|
||||
formatTeslaParkingFee(teslaPricing, ctx)
|
||||
) else null,
|
||||
if (loc.address != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_address,
|
||||
R.string.address,
|
||||
@@ -61,7 +74,8 @@ fun buildDetails(
|
||||
if (loc.network != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_network,
|
||||
R.string.network,
|
||||
loc.network
|
||||
loc.network,
|
||||
clickable = loc.networkUrl != null
|
||||
) else null,
|
||||
if (loc.faultReport != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_fault_report,
|
||||
@@ -125,6 +139,128 @@ fun buildDetails(
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
teslaPricing.memberRates?.activePricebook?.parking?.let { parkingFee ->
|
||||
ctx.getString(
|
||||
R.string.tesla_pricing_blocking_fee,
|
||||
formatTeslaPricingRate(parkingFee.rates, parkingFee.currencyCode, parkingFee.uom, ctx)
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
buildSpannedString {
|
||||
teslaPricing.memberRates?.let { memberRates ->
|
||||
append(
|
||||
ctx.getString(if (teslaPricing.userRates != null) R.string.tesla_pricing_members else R.string.tesla_pricing_owners),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(formatTeslaPricingRates(memberRates, ctx))
|
||||
}
|
||||
teslaPricing.userRates?.let { userRates ->
|
||||
append("\n\n")
|
||||
append(
|
||||
ctx.getString(R.string.tesla_pricing_others),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(formatTeslaPricingRates(userRates, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTeslaPricingRates(rates: TeslaGraphQlApi.Rates, ctx: Context) =
|
||||
buildSpannedString {
|
||||
val timeFmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
if (rates.activePricebook.charging.touRates.enabled) {
|
||||
// time-of-day-based rates
|
||||
val ratesByTime = rates.activePricebook.charging.touRates.activeRatesByTime
|
||||
val distinctRates =
|
||||
ratesByTime.map { it.rates }.distinct().sortedByDescending { it.max() }
|
||||
if (distinctRates.size == 2) {
|
||||
// special case: only list periods with higher price
|
||||
val highPriceTimes = ratesByTime.filter { it.rates == distinctRates[0] }
|
||||
append("\n")
|
||||
append(highPriceTimes.joinToString(", ") {
|
||||
timeFmt.format(it.startTime) + " - " + timeFmt.format(it.endTime)
|
||||
} + ": ", StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
distinctRates[0],
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
append("\n")
|
||||
append(
|
||||
ctx.getString(R.string.tesla_pricing_other_times),
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(" ")
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
distinctRates[1],
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// general case
|
||||
ratesByTime.forEach { rate ->
|
||||
append("\n")
|
||||
append(
|
||||
timeFmt.format(rate.startTime) + " - " + timeFmt.format(rate.endTime) + ": ",
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
rate.rates,
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fixed rates
|
||||
append(" ")
|
||||
append(
|
||||
formatTeslaPricingRate(
|
||||
rates.activePricebook.charging.rates,
|
||||
rates.activePricebook.charging.currencyCode,
|
||||
rates.activePricebook.charging.uom,
|
||||
ctx
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTeslaPricingRate(
|
||||
rates: List<Double>,
|
||||
currencyCode: String,
|
||||
uom: String,
|
||||
ctx: Context
|
||||
): String {
|
||||
if (rates.isEmpty()) return ""
|
||||
val rate = rates.max()
|
||||
val value = ctx.getString(
|
||||
when (uom) {
|
||||
"kwh" -> R.string.charge_price_kwh_format
|
||||
"min" -> R.string.charge_price_minute_format
|
||||
else -> return ""
|
||||
}, rate, currency(currencyCode)
|
||||
)
|
||||
return if (rates.size > 1) {
|
||||
ctx.getString(R.string.pricing_up_to, value)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fun formatChargeCards(
|
||||
chargecards: List<ChargeCardId>,
|
||||
chargecardData: Map<Long, ChargeCard>?,
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
|
||||
@@ -70,6 +71,7 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
|
||||
memoryKeys[item.id] = metadata.memoryCacheKey
|
||||
}
|
||||
)
|
||||
allowHardware(!BuildConfig.DEBUG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ class RateLimitInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (request.url.host == "my.newmotion.com") {
|
||||
if (request.url.host == "ui-map.shellrecharge.com") {
|
||||
// limit requests sent to NewMotion to 3 per second
|
||||
rateLimiter.acquire(1)
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.RateLimitInterceptor
|
||||
import net.vonforst.evmap.api.await
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.cartesianProduct
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -133,7 +135,9 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String,
|
||||
val evseIds: Map<Chargepoint, List<String>>? = null
|
||||
val evseIds: Map<Chargepoint, List<String>>? = null,
|
||||
val congestionHistogram: List<Double>? = null,
|
||||
val extraData: Any? = null // API-specific data
|
||||
) {
|
||||
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
|
||||
val statusFiltered = status.filterKeys {
|
||||
@@ -158,38 +162,41 @@ private val cookieManager = CookieManager().apply {
|
||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
}
|
||||
|
||||
private val okhttp = OkHttpClient.Builder()
|
||||
.addInterceptor(RateLimitInterceptor())
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
.build()
|
||||
val availabilityDetectors = listOf(
|
||||
RheinenergieAvailabilityDetector(okhttp),
|
||||
EnBwAvailabilityDetector(okhttp),
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
)
|
||||
class AvailabilityRepository(context: Context) {
|
||||
private val okhttp = OkHttpClient.Builder()
|
||||
.addInterceptor(RateLimitInterceptor())
|
||||
.addDebugInterceptors()
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
.build()
|
||||
private val availabilityDetectors = listOf(
|
||||
RheinenergieAvailabilityDetector(okhttp),
|
||||
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context)),
|
||||
EnBwAvailabilityDetector(okhttp),
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
)
|
||||
|
||||
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
if (!ad.isChargerSupported(charger)) continue
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
if (!ad.isChargerSupported(charger)) continue
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
) < maxDistance
|
||||
}
|
||||
|
||||
var details = markers.filter {
|
||||
val details = markers.filter {
|
||||
// only include stations from same operator
|
||||
it.operator == nearest.operator && it.stationId != null
|
||||
}.map {
|
||||
@@ -203,30 +203,46 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
val country = charger.chargepriceData?.country
|
||||
?: charger.address?.country ?: return false
|
||||
return when (charger.dataSource) {
|
||||
// list of countries as of 2021/06/30, according to
|
||||
// https://www.electrive.net/2021/06/30/enbw-expandiert-mit-ladenetz-in-drei-weitere-laender/
|
||||
// list of countries as of 2023/04/14, according to
|
||||
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
|
||||
"goingelectric" -> country in listOf(
|
||||
"Deutschland",
|
||||
"Österreich",
|
||||
"Schweiz",
|
||||
"Frankreich",
|
||||
"Belgien",
|
||||
"Niederlande",
|
||||
"Luxemburg",
|
||||
"Liechtenstein",
|
||||
"Dänemark",
|
||||
"Frankreich",
|
||||
"Italien",
|
||||
)
|
||||
"Kroatien",
|
||||
"Liechtenstein",
|
||||
"Luxemburg",
|
||||
"Niederlande",
|
||||
"Polen",
|
||||
"Schweden",
|
||||
"Slowakei",
|
||||
"Slowenien",
|
||||
"Spanien",
|
||||
"Tschechien"
|
||||
) && charger.network != "Tesla Supercharger"
|
||||
"openchargemap" -> country in listOf(
|
||||
"DE",
|
||||
"AT",
|
||||
"CH",
|
||||
"FR",
|
||||
"BE",
|
||||
"NE",
|
||||
"LU",
|
||||
"DK",
|
||||
"FR",
|
||||
"IT",
|
||||
"HR",
|
||||
"LI",
|
||||
"IT"
|
||||
)
|
||||
"LU",
|
||||
"NE",
|
||||
"PL",
|
||||
"SE",
|
||||
"SK",
|
||||
"SI",
|
||||
"ES",
|
||||
"CZ"
|
||||
) && charger.chargepriceData?.network !in listOf("23", "3534")
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ private const val coordRange = 0.005 // range of latitude and longitude for loa
|
||||
private const val maxDistance = 40 // max distance between reported positions in meters
|
||||
|
||||
interface NewMotionApi {
|
||||
@GET("markers/{lngMin}/{lngMax}/{latMin}/{latMax}")
|
||||
@GET("markers/{lngMin}/{lngMax}/{latMin}/{latMax}/{zoom}")
|
||||
suspend fun getMarkers(
|
||||
@Path("lngMin") lngMin: Double,
|
||||
@Path("lngMax") lngMax: Double,
|
||||
@Path("latMin") latMin: Double,
|
||||
@Path("latMax") latMax: Double
|
||||
@Path("latMax") latMax: Double,
|
||||
@Path("zoom") zoom: Int = 22
|
||||
): List<NMMarker>
|
||||
|
||||
@GET("locations/{id}")
|
||||
@@ -76,7 +77,7 @@ interface NewMotionApi {
|
||||
companion object {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): NewMotionApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://my.newmotion.com/api/map/v2/")
|
||||
.baseUrl(baseUrl ?: "https://ui-map.shellrecharge.com/api/map/v2/")
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.client(client)
|
||||
.build()
|
||||
@@ -181,7 +182,11 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
// NewMotion is our fallback
|
||||
return true
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network != "Tesla Supercharger"
|
||||
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
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
|
||||
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")
|
||||
suspend fun getToken(@Body request: OAuth2Request): OAuth2Response
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class AuthCodeRequest(
|
||||
val code: String,
|
||||
@Json(name = "code_verifier") val codeVerifier: String,
|
||||
@Json(name = "redirect_uri") val redirectUri: String = "https://auth.tesla.com/void/callback",
|
||||
scope: String = "openid email offline_access",
|
||||
@Json(name = "client_id") clientId: String = "ownerapi"
|
||||
) : OAuth2Request(scope, clientId)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class RefreshTokenRequest(
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
scope: String = "openid email offline_access",
|
||||
@Json(name = "client_id") clientId: String = "ownerapi"
|
||||
) : OAuth2Request(scope, clientId)
|
||||
|
||||
sealed class OAuth2Request(
|
||||
val scope: String,
|
||||
val clientId: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OAuth2Response(
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
@Json(name = "token_type") val tokenType: String,
|
||||
@Json(name = "expires_in") val expiresIn: Long,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): TeslaAuthenticationApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://auth.tesla.com")
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(
|
||||
OAuth2Request::class.java,
|
||||
"grant_type"
|
||||
)
|
||||
.withSubtype(AuthCodeRequest::class.java, "authorization_code")
|
||||
.withSubtype(RefreshTokenRequest::class.java, "refresh_token")
|
||||
.withDefaultValue(null)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(TeslaAuthenticationApi::class.java)
|
||||
}
|
||||
|
||||
fun generateCodeVerifier(): String {
|
||||
val code = ByteArray(64)
|
||||
SecureRandom().nextBytes(code)
|
||||
return Base64.encodeToString(
|
||||
code,
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
|
||||
)
|
||||
}
|
||||
|
||||
fun generateCodeChallenge(codeVerifier: String): String {
|
||||
val bytes = codeVerifier.toByteArray()
|
||||
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
messageDigest.update(bytes, 0, bytes.size)
|
||||
return Base64.encodeToString(
|
||||
messageDigest.digest(),
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TeslaOwnerApi {
|
||||
@GET("/api/1/users/me")
|
||||
suspend fun getUserInfo(): UserInfoResponse
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UserInfoResponse(
|
||||
val response: UserInfo
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UserInfo(
|
||||
val email: String,
|
||||
@Json(name = "full_name") val fullName: String,
|
||||
@Json(name = "profile_image_url") val profileImageUrl: String?
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(client: OkHttpClient, token: String, baseUrl: String? = null): TeslaOwnerApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Authorization", "Bearer $token")
|
||||
.header("User-Agent", "okhttp/4.9.2")
|
||||
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
|
||||
.header("Accept", "*/*")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}.build()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://owner-api.teslamotors.com")
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(TeslaOwnerApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TeslaGraphQlApi {
|
||||
@POST("/graphql")
|
||||
suspend fun getNearbyChargingSites(
|
||||
@Body request: GetNearbyChargingSitesRequest,
|
||||
@Query("operationName") operationName: String = "GetNearbyChargingSites",
|
||||
@Query("deviceLanguage") deviceLanguage: String = "en",
|
||||
@Query("deviceCountry") deviceCountry: String = "US",
|
||||
@Query("ttpLocale") ttpLocale: String = "en_US",
|
||||
@Query("vin") vin: String = "",
|
||||
): GetNearbyChargingSitesResponse
|
||||
|
||||
@POST("/graphql")
|
||||
suspend fun getChargingSiteInformation(
|
||||
@Body request: GetChargingSiteInformationRequest,
|
||||
@Query("operationName") operationName: String = "getChargingSiteInformation",
|
||||
@Query("deviceLanguage") deviceLanguage: String = "en",
|
||||
@Query("deviceCountry") deviceCountry: String = "US",
|
||||
@Query("ttpLocale") ttpLocale: String = "en_US",
|
||||
@Query("vin") vin: String = "",
|
||||
): GetChargingSiteInformationResponse
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesRequest(
|
||||
override val variables: GetNearbyChargingSitesVariables,
|
||||
override val operationName: String = "GetNearbyChargingSites",
|
||||
override val query: String =
|
||||
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n "
|
||||
) : GraphQlRequest()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesVariables(val args: GetNearbyChargingSitesArgs)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesArgs(
|
||||
val userLocation: Coordinate,
|
||||
val northwestCorner: Coordinate,
|
||||
val southeastCorner: Coordinate,
|
||||
val openToNonTeslasFilter: OpenToNonTeslasFilterValue,
|
||||
val languageCode: String = "en",
|
||||
val countryCode: String = "US",
|
||||
//val vin: String = "",
|
||||
//val maxCount: Int = 100
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OpenToNonTeslasFilterValue(val value: Boolean)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Coordinate(val latitude: Double, val longitude: Double)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationRequest(
|
||||
override val variables: GetChargingSiteInformationVariables,
|
||||
override val operationName: String = "getChargingSiteInformation",
|
||||
override val query: String =
|
||||
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isMagicDockSupportedSite\n trtId {\n value\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n }\n waitEstimateBucket\n currentCongestion\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
|
||||
) : GraphQlRequest()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationVariables(
|
||||
val id: ChargingSiteIdentifier,
|
||||
val vehicleMakeType: VehicleMakeType,
|
||||
val deviceLanguage: String = "en",
|
||||
val deviceCountry: String = "US",
|
||||
val ttpLocale: String = "en_US"
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteIdentifier(
|
||||
val id: String,
|
||||
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID
|
||||
)
|
||||
|
||||
enum class ChargingSiteIdentifierType {
|
||||
SITE_ID
|
||||
}
|
||||
|
||||
enum class VehicleMakeType {
|
||||
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)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesResponseData(val charging: GetNearbyChargingSitesResponseDataCharging)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesResponseDataCharging(val nearbySites: GetNearbyChargingSitesResponseDataChargingNearbySites)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesResponseDataChargingNearbySites(val sitesAndDistances: List<ChargingSite>)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSite(
|
||||
val activeOutages: List<Outage>,
|
||||
val availableStalls: Value<Int>?,
|
||||
val centroid: Coordinate,
|
||||
val drivingDistanceMiles: Value<Double>?,
|
||||
val entryPoint: Coordinate,
|
||||
val haversineDistanceMiles: Value<Double>,
|
||||
val id: Text,
|
||||
val localizedSiteName: Value<String>,
|
||||
val maxPowerKw: Value<Int>,
|
||||
val totalStalls: Value<Int>
|
||||
// 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)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationResponseData(val charging: GetChargingSiteInformationResponseDataCharging)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteInformation(
|
||||
val siteDynamic: SiteDynamic,
|
||||
val siteStatic: SiteStatic,
|
||||
val pricing: Pricing,
|
||||
val congestionPriceHistogram: CongestionPriceHistogram,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SiteDynamic(
|
||||
val activeOutages: List<Outage>,
|
||||
val chargerDetails: List<ChargerDetail>,
|
||||
val chargersAvailable: Value<Int>?,
|
||||
val currentCongestion: Double,
|
||||
val id: Text,
|
||||
val waitEstimateBucket: WaitEstimateBucket
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerDetail(
|
||||
val availability: ChargerAvailability,
|
||||
val charger: ChargerId
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerId(
|
||||
val id: Text,
|
||||
val label: Value<String>,
|
||||
val name: String?
|
||||
) {
|
||||
val labelNumber
|
||||
get() = label.value.replace(Regex("""\D"""), "").toInt()
|
||||
val labelLetter
|
||||
get() = label.value.replace(Regex("""\d"""), "")
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SiteStatic(
|
||||
val accessCode: Value<String>?,
|
||||
val centroid: Coordinate,
|
||||
val chargers: List<ChargerId>,
|
||||
val entryPoint: Coordinate,
|
||||
val fastchargeSiteId: Value<Long>,
|
||||
val id: Text,
|
||||
val isMagicDockSupportedSite: Boolean,
|
||||
val localizedSiteName: Value<String>,
|
||||
val maxPowerKw: Value<Int>,
|
||||
val name: String,
|
||||
val openToPublic: Boolean,
|
||||
val publicStallCount: Int
|
||||
// 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>,
|
||||
val dataAttributes: List<CongestionHistogramDataAttributes>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CongestionHistogramDataAttributes(
|
||||
val congestionThreshold: String, // "LEVEL_1"
|
||||
val label: String // "1AM", "2AM", etc.
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
enum class WaitEstimateBucket {
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_NO_WAIT")
|
||||
NO_WAIT,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_LESS_THAN_5_MINUTES")
|
||||
LESS_THAN_5_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_5_MINUTES")
|
||||
APPROXIMATELY_5_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_10_MINUTES")
|
||||
APPROXIMATELY_10_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_15_MINUTES")
|
||||
APPROXIMATELY_15_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_20_MINUTES")
|
||||
APPROXIMATELY_20_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_UNKNOWN")
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null,
|
||||
token: suspend () -> String
|
||||
): TeslaGraphQlApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val t = runBlocking { token() }
|
||||
// add API key to every request
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Authorization", "Bearer $t")
|
||||
.header("User-Agent", "okhttp/4.9.2")
|
||||
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
|
||||
.header("Accept", "*/*")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}.build()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://akamai-apigateway-charging-ownership.tesla.com")
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(LocalTimeAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(TeslaGraphQlApi::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
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
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 statusSorted = details.siteDynamic.chargerDetails.sortedBy { it.charger.labelLetter }
|
||||
.sortedBy { it.charger.labelNumber }
|
||||
|
||||
val statusMap = emptyMap<Chargepoint, List<ChargepointStatus>>().toMutableMap()
|
||||
var i = 0
|
||||
for (connector in scV2Connectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
if (scV2CCSConnectors.isNotEmpty()) {
|
||||
i = 0
|
||||
for (connector in scV2CCSConnectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
}
|
||||
for (connector in scV3Connectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val indexOfMidnight =
|
||||
details.congestionPriceHistogram.dataAttributes.indexOfFirst { it.label == "12AM" }
|
||||
val congestionHistogram = indexOfMidnight.takeIf { it >= 0 }?.let { index ->
|
||||
val data = details.congestionPriceHistogram.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package net.vonforst.evmap.api.chargeprice
|
||||
|
||||
import android.content.Context
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import jsonapi.Document
|
||||
import jsonapi.JsonApiFactory
|
||||
import jsonapi.retrofit.DocumentConverterFactory
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -77,10 +77,10 @@ interface ChargepriceApi {
|
||||
chain.proceed(new)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.getCacheDir(), cacheSize))
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
@@ -127,14 +127,16 @@ interface ChargepriceApi {
|
||||
charger.chargepriceData?.country?.let { isCountrySupported(it, charger.dataSource) }
|
||||
?: false
|
||||
val networkSupported = charger.chargepriceData?.network?.let {
|
||||
if (charger.dataSource == "openchargemap") {
|
||||
it !in listOf(
|
||||
when (charger.dataSource) {
|
||||
"openchargemap" -> it !in listOf(
|
||||
"1", // unknown operator
|
||||
"44", // private residence/individual
|
||||
"45" // business owner at location
|
||||
"45", // business owner at location
|
||||
"23", "3534" // Tesla
|
||||
)
|
||||
} else {
|
||||
true
|
||||
|
||||
"goingelectric" -> it != "Tesla Supercharger"
|
||||
else -> true
|
||||
}
|
||||
} ?: false
|
||||
val powerAvailable = charger.chargepoints.all { it.hasKnownPower() }
|
||||
|
||||
@@ -157,9 +157,9 @@ data class ChargepriceCar(
|
||||
get() = id_!!
|
||||
|
||||
val compatibleEvmapConnectors: List<String>
|
||||
get() = dcChargePorts.map {
|
||||
get() = dcChargePorts.mapNotNull {
|
||||
plugMapping[it]
|
||||
}.filterNotNull().plus(acConnectors)
|
||||
}.plus(acConnectors)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -228,7 +228,7 @@ internal object RelationshipsParceler : Parceler<Relationships?> {
|
||||
if (parcel.readInt() == 0) return null
|
||||
|
||||
val nMembers = parcel.readInt()
|
||||
val members = (0 until nMembers).map { _ ->
|
||||
val members = (0 until nMembers).associate { _ ->
|
||||
val key = parcel.readString()!!
|
||||
val value = if (parcel.readInt() == 0) {
|
||||
val type = parcel.readString()
|
||||
@@ -247,7 +247,7 @@ internal object RelationshipsParceler : Parceler<Relationships?> {
|
||||
Relationship.ToMany(ris)
|
||||
}
|
||||
key to value
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
return Relationships(members)
|
||||
}
|
||||
@@ -299,12 +299,12 @@ data class ChargepointPrice(
|
||||
}
|
||||
|
||||
fun time(value: Int): String {
|
||||
val h = floor(value.toDouble() / 60).toInt();
|
||||
val min = ceil(value.toDouble() % 60).toInt();
|
||||
if (h == 0 && min > 0) return "${min}min";
|
||||
val h = floor(value.toDouble() / 60).toInt()
|
||||
val min = ceil(value.toDouble() % 60).toInt()
|
||||
return if (h == 0 && min > 0) "${min}min";
|
||||
// be slightly sloppy (3:01 is shown as 3h) to save space
|
||||
else if (h > 0 && (min == 0 || min == 1)) return "${h}h";
|
||||
else return "%d:%02dh".format(h, min);
|
||||
else if (h > 0 && (min == 0 || min == 1)) "${h}h";
|
||||
else "%d:%02dh".format(h, min)
|
||||
}
|
||||
|
||||
// based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.vonforst.evmap.api.fronyx
|
||||
|
||||
import android.content.Context
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.Cache
|
||||
@@ -49,10 +49,10 @@ private interface FronyxApiRetrofit {
|
||||
chain.proceed(new)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.getCacheDir(), cacheSize))
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
|
||||
annotations: MutableSet<out Annotation>,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<*>? {
|
||||
if (Types.getRawType(type) == GEChargepointListItem::class.java) {
|
||||
return ChargepointListItemJsonAdapter(
|
||||
return if (Types.getRawType(type) == GEChargepointListItem::class.java) {
|
||||
ChargepointListItemJsonAdapter(
|
||||
moshi
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package net.vonforst.evmap.api.goingelectric
|
||||
import android.content.Context
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -11,6 +10,7 @@ import kotlinx.coroutines.supervisorScope
|
||||
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.ui.cluster
|
||||
@@ -104,10 +104,10 @@ interface GoingElectricApi {
|
||||
chain.proceed(original)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.getCacheDir(), cacheSize))
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
@@ -146,7 +146,7 @@ class GoingElectricApiWrapper(
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
|
||||
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
@@ -217,7 +217,7 @@ class GoingElectricApiWrapper(
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
var result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom)
|
||||
val result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom)
|
||||
|
||||
return Resource.success(result)
|
||||
}
|
||||
@@ -240,7 +240,7 @@ class GoingElectricApiWrapper(
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
|
||||
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
@@ -404,11 +404,11 @@ class GoingElectricApiWrapper(
|
||||
val networks = refData.networks
|
||||
val chargeCards = refData.chargecards
|
||||
|
||||
val plugMap = plugs.map { plug ->
|
||||
plug to nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
|
||||
}.toMap()
|
||||
val networkMap = networks.map { it to it }.toMap()
|
||||
val chargecardMap = chargeCards.map { it.id.toString() to it.name }.toMap()
|
||||
val plugMap = plugs.associateWith { plug ->
|
||||
nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
|
||||
}
|
||||
val networkMap = networks.associateWith { it }
|
||||
val chargecardMap = chargeCards.associate { it.id.toString() to it.name }
|
||||
val categoryMap = mapOf(
|
||||
"Autohaus" to sp.getString(R.string.category_car_dealership),
|
||||
"Autobahnraststätte" to sp.getString(R.string.category_service_on_motorway),
|
||||
|
||||
@@ -77,6 +77,8 @@ data class GEChargeLocation(
|
||||
cost?.convert(),
|
||||
null,
|
||||
ChargepriceData(address.country, network, chargepoints.map { it.type }),
|
||||
null,
|
||||
null,
|
||||
Instant.now(),
|
||||
isDetailed
|
||||
)
|
||||
@@ -209,6 +211,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
|
||||
"Typ1" -> Chargepoint.TYPE_1
|
||||
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"Typ3" -> Chargepoint.TYPE_3
|
||||
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
|
||||
"CCS" -> Chargepoint.CCS_UNKNOWN
|
||||
"Schuko" -> Chargepoint.SCHUKO
|
||||
"CHAdeMO" -> Chargepoint.CHADEMO
|
||||
|
||||
@@ -3,11 +3,11 @@ package net.vonforst.evmap.api.openchargemap
|
||||
import android.content.Context
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.ui.cluster
|
||||
@@ -83,7 +83,7 @@ interface OpenChargeMapApi {
|
||||
chain.proceed(new)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
@@ -148,18 +148,18 @@ class OpenChargeMapApiWrapper(
|
||||
),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
operators = operators
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
var result = postprocessResult(
|
||||
val result = postprocessResult(
|
||||
response.body()!!,
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
excludeFaults,
|
||||
refData,
|
||||
zoom
|
||||
)
|
||||
@@ -202,8 +202,7 @@ class OpenChargeMapApiWrapper(
|
||||
radius.toDouble(),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
operators = operators
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
@@ -214,6 +213,7 @@ class OpenChargeMapApiWrapper(
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
excludeFaults,
|
||||
refData,
|
||||
zoom
|
||||
)
|
||||
@@ -228,6 +228,7 @@ class OpenChargeMapApiWrapper(
|
||||
minPower: Double?,
|
||||
connectorsVal: MultipleChoiceFilterValue?,
|
||||
minConnectors: Int?,
|
||||
excludeFaults: Boolean?,
|
||||
referenceData: OCMReferenceData,
|
||||
zoom: Float
|
||||
): List<ChargepointListItem> {
|
||||
@@ -237,6 +238,8 @@ class OpenChargeMapApiWrapper(
|
||||
.filter { it.power == null || it.power >= (minPower ?: 0.0) }
|
||||
.filter { if (connectorsVal != null && !connectorsVal.all) it.connectionTypeId in connectorsVal.values.map { it.toLong() } else true }
|
||||
.sumOf { it.quantity ?: 1 } >= (minConnectors ?: 0)
|
||||
}.filter {
|
||||
it.statusTypeId == null || (it.statusTypeId !in removedStatuses && if (excludeFaults == true) it.statusTypeId !in faultStatuses else true)
|
||||
}.map { it.convert(referenceData, false) }.distinct() as List<ChargepointListItem>
|
||||
|
||||
// apply clustering
|
||||
@@ -286,8 +289,8 @@ class OpenChargeMapApiWrapper(
|
||||
): List<Filter<FilterValue>> {
|
||||
val refData = referenceData as OCMReferenceData
|
||||
|
||||
val operatorsMap = refData.operators.map { it.id.toString() to it.title }.toMap()
|
||||
val plugMap = refData.connectionTypes.map { it.id.toString() to it.title }.toMap()
|
||||
val operatorsMap = refData.operators.associate { it.id.toString() to it.title }
|
||||
val plugMap = refData.connectionTypes.associate { it.id.toString() to it.title }
|
||||
|
||||
return listOf(
|
||||
// supported by OCM API
|
||||
|
||||
@@ -11,12 +11,15 @@ import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
// Unknown, Currently Available, Currently In Use, Operational
|
||||
val noFaultStatuses = listOf(0, 10, 20, 50)
|
||||
val noFaultStatuses = listOf(0L, 10L, 20L, 50L)
|
||||
|
||||
// Temporarily Unavailable, Partly Operational, Not Operational, Planned For Future Date, Removed (Decommissioned)
|
||||
val faultStatuses = listOf(30L, 75L, 100L, 150L, 200L)
|
||||
// Temporarily Unavailable, Partly Operational, Not Operational, Planned For Future Date
|
||||
val faultStatuses = listOf(30L, 75L, 100L, 150L)
|
||||
val faultReportCommentType = 1000L
|
||||
|
||||
// Removed (Decommissioned), Removed (Duplicate Listing)
|
||||
val removedStatuses = listOf(200L, 210L)
|
||||
|
||||
data class OCMBoundingBox(
|
||||
val sw_lat: Double, val sw_lng: Double,
|
||||
val ne_lat: Double, val ne_lng: Double
|
||||
@@ -71,10 +74,16 @@ data class OCMChargepoint(
|
||||
addressInfo.countryISOCode(refData),
|
||||
operatorId?.toString(),
|
||||
connections.map { "${it.connectionTypeId},${it.currentTypeId}" }),
|
||||
operatorInfo?.websiteUrl,
|
||||
if (operatorInfo?.websiteUrl?.withoutTrailingSlash() != addressInfo.relatedUrl?.withoutTrailingSlash()) addressInfo.relatedUrl else null,
|
||||
Instant.now(),
|
||||
isDetailed
|
||||
)
|
||||
|
||||
private fun String.withoutTrailingSlash(): String {
|
||||
return this.replace(Regex("/$"), "")
|
||||
}
|
||||
|
||||
private fun convertFaultReport(): FaultReport? {
|
||||
if (statusTypeId in faultStatuses || connections.any { it.statusTypeId in faultStatuses }) {
|
||||
if (userComments != null) {
|
||||
|
||||
@@ -105,6 +105,8 @@ data class OSMChargingStation(
|
||||
getCost(),
|
||||
"© OpenStreetMap contributors",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
dataFetchTimestamp,
|
||||
true,
|
||||
)
|
||||
@@ -118,7 +120,7 @@ data class OSMChargingStation(
|
||||
// If that is missing as well, use a generic "Charging Station" string.
|
||||
return tags["name"]
|
||||
?: tags["operator"]
|
||||
?: "Charging Station";
|
||||
?: "Charging Station"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,7 +193,7 @@ data class OSMChargingStation(
|
||||
*/
|
||||
fun parseOutputPower(rawOutput: String?): Double? {
|
||||
if (rawOutput == null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
val pattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
|
||||
val matchResult = pattern.matchEntire(rawOutput) ?: return null
|
||||
|
||||
@@ -176,10 +176,10 @@ enum class AutocompletePlaceType {
|
||||
|
||||
companion object {
|
||||
fun valueOfOrNull(value: String): AutocompletePlaceType? {
|
||||
try {
|
||||
return valueOf(value)
|
||||
return try {
|
||||
valueOf(value)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
override fun getAttributionString(): Int = R.string.powered_by_mapbox
|
||||
|
||||
override fun getAttributionImage(dark: Boolean): Int =
|
||||
if (dark) R.drawable.mapbox_logo_icon else R.drawable.mapbox_logo
|
||||
if (dark) com.mapbox.mapboxsdk.R.drawable.mapbox_logo_icon else R.drawable.mapbox_logo
|
||||
}
|
||||
|
||||
private fun BoundingBox.toLatLngBounds(): LatLngBounds {
|
||||
|
||||
@@ -123,12 +123,12 @@ class ChargepriceFragment : Fragment() {
|
||||
val charger = fragmentArgs.charger
|
||||
vm.charger.value = charger
|
||||
if (vm.chargepoint.value == null) {
|
||||
vm.chargepoint.value = charger.chargepointsMerged.get(0)
|
||||
vm.chargepoint.value = charger.chargepointsMerged[0]
|
||||
}
|
||||
|
||||
val vehicleAdapter = CheckableChargepriceCarAdapter()
|
||||
headerBinding.vehicleSelection.adapter = vehicleAdapter
|
||||
val vehicleObserver: Observer<ChargepriceCar> = Observer {
|
||||
val vehicleObserver: Observer<ChargepriceCar?> = Observer {
|
||||
vehicleAdapter.setCheckedItem(it)
|
||||
}
|
||||
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
|
||||
|
||||
@@ -80,8 +80,8 @@ class FilterProfilesFragment : Fragment() {
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val fromPos = viewHolder.bindingAdapterPosition;
|
||||
val toPos = target.bindingAdapterPosition;
|
||||
val fromPos = viewHolder.bindingAdapterPosition
|
||||
val toPos = target.bindingAdapterPosition
|
||||
|
||||
val list = vm.filterProfiles.value?.toMutableList()
|
||||
if (list != null) {
|
||||
|
||||
@@ -40,8 +40,6 @@ import androidx.transition.TransitionInflater
|
||||
import androidx.transition.TransitionManager
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.MapFragment
|
||||
import com.car2go.maps.OnMapReadyCallback
|
||||
@@ -75,6 +73,7 @@ import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.fragment.preference.DataSettingsFragmentArgs
|
||||
import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.location.Priority
|
||||
@@ -130,16 +129,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
val state = bottomSheetBehavior.state
|
||||
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
|
||||
if (bottomSheetCollapsible) {
|
||||
when (state) {
|
||||
STATE_COLLAPSED -> vm.chargerSparse.value = null
|
||||
STATE_HIDDEN -> vm.searchResult.value = null
|
||||
else -> if (bottomSheetCollapsible) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
} else {
|
||||
vm.chargerSparse.value = null
|
||||
}
|
||||
} else if (state == STATE_COLLAPSED) {
|
||||
vm.chargerSparse.value = null
|
||||
} else if (state == STATE_HIDDEN) {
|
||||
vm.searchResult.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +161,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false)
|
||||
println(binding.detailView.sourceButton)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
@@ -248,7 +246,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
mapFragment!!.getMapAsync(this)
|
||||
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
|
||||
bottomSheetBehavior = from(binding.bottomSheet)
|
||||
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
|
||||
|
||||
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail)
|
||||
@@ -377,6 +375,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
null, extras
|
||||
)
|
||||
}
|
||||
binding.detailView.btnChargerWebsite.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
charger.chargerUrl?.let { (activity as? MapsActivity)?.openUrl(it) }
|
||||
}
|
||||
binding.detailView.btnLogin.setOnClickListener {
|
||||
findNavController().navigate(
|
||||
R.id.settings_data,
|
||||
DataSettingsFragmentArgs(true).toBundle()
|
||||
)
|
||||
}
|
||||
binding.detailView.imgPredictionSource.setOnClickListener {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.fronyx_url))
|
||||
}
|
||||
@@ -387,7 +395,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
.show()
|
||||
}
|
||||
binding.detailView.topPart.setOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
|
||||
bottomSheetBehavior.state = STATE_ANCHOR_POINT
|
||||
}
|
||||
setupSearchAutocomplete()
|
||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
||||
@@ -552,7 +560,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
private fun setupObservers() {
|
||||
bottomSheetBehavior.addBottomSheetCallback(object :
|
||||
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
|
||||
BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
if (bottomSheetBehavior.state == STATE_HIDDEN) {
|
||||
map?.setPadding(0, mapTopPadding, 0, 0)
|
||||
@@ -581,9 +589,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
})
|
||||
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
|
||||
vm.chargerSparse.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
|
||||
if (vm.bottomSheetState.value != STATE_ANCHOR_POINT) {
|
||||
bottomSheetBehavior.state =
|
||||
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT
|
||||
}
|
||||
@@ -596,7 +604,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
bottomSheetBehavior.state = STATE_HIDDEN
|
||||
unhighlightAllMarkers()
|
||||
}
|
||||
})
|
||||
}
|
||||
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
|
||||
when (res.status) {
|
||||
Status.ERROR -> {
|
||||
@@ -626,23 +634,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.useMiniMarkers.observe(viewLifecycleOwner) {
|
||||
vm.chargepoints.value?.data?.let { updateMap(it) }
|
||||
}
|
||||
vm.favorites.observe(viewLifecycleOwner, Observer {
|
||||
vm.favorites.observe(viewLifecycleOwner) {
|
||||
updateFavoriteToggle()
|
||||
})
|
||||
vm.searchResult.observe(viewLifecycleOwner, Observer { place ->
|
||||
}
|
||||
vm.searchResult.observe(viewLifecycleOwner) { place ->
|
||||
displaySearchResult(place, moveCamera = true)
|
||||
})
|
||||
vm.layersMenuOpen.observe(viewLifecycleOwner, Observer { open ->
|
||||
}
|
||||
vm.layersMenuOpen.observe(viewLifecycleOwner) { open ->
|
||||
binding.fabLayers.visibility = if (open) View.INVISIBLE else View.VISIBLE
|
||||
binding.layersSheet.visibility = if (open) View.VISIBLE else View.INVISIBLE
|
||||
updateBackPressedCallback()
|
||||
})
|
||||
vm.mapType.observe(viewLifecycleOwner, Observer {
|
||||
}
|
||||
vm.mapType.observe(viewLifecycleOwner) {
|
||||
map?.setMapType(it)
|
||||
})
|
||||
vm.mapTrafficEnabled.observe(viewLifecycleOwner, Observer {
|
||||
}
|
||||
vm.mapTrafficEnabled.observe(viewLifecycleOwner) {
|
||||
map?.setTrafficEnabled(it)
|
||||
})
|
||||
}
|
||||
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
@@ -697,7 +705,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = c.isMulti(vm.filteredConnectors.value),
|
||||
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
|
||||
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
@@ -713,7 +721,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
highlight = true,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
|
||||
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
@@ -728,7 +736,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = c.isMulti(vm.filteredConnectors.value),
|
||||
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
|
||||
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
@@ -808,6 +816,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
R.drawable.ic_payment -> {
|
||||
showPaymentMethodsDialog(charger)
|
||||
}
|
||||
R.drawable.ic_network -> {
|
||||
charger.networkUrl?.let { (activity as? MapsActivity)?.openUrl(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -957,10 +968,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.chargerSparse.observe(
|
||||
viewLifecycleOwner,
|
||||
object : Observer<ChargeLocation> {
|
||||
override fun onChanged(item: ChargeLocation?) {
|
||||
if (item?.id == chargerId) {
|
||||
override fun onChanged(value: ChargeLocation) {
|
||||
if (value.id == chargerId) {
|
||||
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(
|
||||
LatLng(item.coordinates.lat, item.coordinates.lng), 16f
|
||||
LatLng(value.coordinates.lat, value.coordinates.lng), 16f
|
||||
)
|
||||
map.moveCamera(cameraUpdate)
|
||||
vm.chargerSparse.removeObserver(this)
|
||||
@@ -979,9 +990,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.chargepoints.observe(
|
||||
viewLifecycleOwner,
|
||||
object : Observer<Resource<List<ChargepointListItem>>> {
|
||||
override fun onChanged(res: Resource<List<ChargepointListItem>>) {
|
||||
if (res.data == null) return
|
||||
for (item in res.data) {
|
||||
override fun onChanged(value: Resource<List<ChargepointListItem>>) {
|
||||
if (value.data == null) return
|
||||
for (item in value.data) {
|
||||
if (item is ChargeLocation && item.id == chargerId) {
|
||||
vm.chargerSparse.value = item
|
||||
vm.chargepoints.removeObserver(this)
|
||||
@@ -1089,7 +1100,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
highlight = highlight,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
|
||||
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
@@ -1110,7 +1121,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val fault = charger.faultReport != null
|
||||
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||
val fav =
|
||||
charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
|
||||
charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList())
|
||||
animator.animateMarkerDisappear(
|
||||
marker, tint, highlight, fault, multi, fav,
|
||||
vm.useMiniMarkers.value == true
|
||||
@@ -1129,7 +1140,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
val fault = charger.faultReport != null
|
||||
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||
val fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
|
||||
val fav =
|
||||
charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList())
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
@@ -1183,13 +1195,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val filterBadge = filterView?.findViewById<TextView>(R.id.filter_badge)
|
||||
if (filterBadge != null) {
|
||||
// set up badge showing number of active filters
|
||||
vm.filtersCount.observe(viewLifecycleOwner, Observer {
|
||||
vm.filtersCount.observe(viewLifecycleOwner) {
|
||||
filterBadge.visibility = if (it > 0) View.VISIBLE else View.GONE
|
||||
filterBadge.text = it.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
filterView?.setOnClickListener {
|
||||
var profilesMap: MutableBiMap<Long, MenuItem> = HashBiMap()
|
||||
val profilesMap: MutableBiMap<Long, MenuItem> = HashBiMap()
|
||||
|
||||
val popup = PopupMenu(
|
||||
ContextThemeWrapper(requireContext(), R.style.RoundedPopup),
|
||||
@@ -1230,7 +1242,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
vm.filterProfiles.observe(viewLifecycleOwner, { profiles ->
|
||||
vm.filterProfiles.observe(viewLifecycleOwner) { profiles ->
|
||||
popup.menu.removeGroup(R.id.menu_group_filter_profiles)
|
||||
|
||||
val noFiltersItem = popup.menu.add(
|
||||
@@ -1260,25 +1272,28 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
profilesMap[FILTERS_CUSTOM] = customItem
|
||||
profilesMap[FILTERS_FAVORITES] = favoritesItem
|
||||
|
||||
popup.menu.setGroupCheckable(R.id.menu_group_filter_profiles, true, true);
|
||||
popup.menu.setGroupCheckable(R.id.menu_group_filter_profiles, true, true)
|
||||
|
||||
val manageFiltersItem = popup.menu.findItem(R.id.menu_manage_filter_profiles)
|
||||
manageFiltersItem.isVisible = profiles.isNotEmpty()
|
||||
|
||||
vm.filterStatus.observe(viewLifecycleOwner, Observer { id ->
|
||||
vm.filterStatus.observe(viewLifecycleOwner) { id ->
|
||||
when (id) {
|
||||
FILTERS_DISABLED -> {
|
||||
customItem.isVisible = false
|
||||
noFiltersItem.isChecked = true
|
||||
}
|
||||
|
||||
FILTERS_CUSTOM -> {
|
||||
customItem.isVisible = true
|
||||
customItem.isChecked = true
|
||||
}
|
||||
|
||||
FILTERS_FAVORITES -> {
|
||||
customItem.isVisible = false
|
||||
favoritesItem.isChecked = true
|
||||
}
|
||||
|
||||
else -> {
|
||||
customItem.isVisible = false
|
||||
val item = profilesMap[id]
|
||||
@@ -1288,8 +1303,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
// else unknown ID -> wait for filterProfiles to update
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
popup.setTouchModal(false)
|
||||
popup.show()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.vonforst.evmap.fragment.oauth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
class OAuthLoginFragment : Fragment() {
|
||||
private lateinit var webView: WebView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = inflater.inflate(R.layout.fragment_oauth_login, container, false)
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
|
||||
val uri = Uri.parse(args.url)
|
||||
|
||||
webView = view.findViewById(R.id.webView)
|
||||
|
||||
args.color?.let { webView.setBackgroundColor(Color.parseColor(it)) }
|
||||
val progress = view.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
|
||||
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
val url = request.url
|
||||
|
||||
if (url.toString().startsWith(args.resultUrlPrefix)) {
|
||||
val result = Bundle()
|
||||
result.putString("url", url.toString())
|
||||
setFragmentResult(args.url, result)
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
return url.host != uri.host
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
progress.show()
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
progress.hide()
|
||||
webView.background = null
|
||||
}
|
||||
}
|
||||
webView.settings.javaScriptEnabled = true
|
||||
webView.loadUrl(args.url)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
@@ -15,9 +16,13 @@ import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
|
||||
class AboutFragment : PreferenceFragmentCompat() {
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
private var developerOptionsCounter = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialFadeThrough()
|
||||
@@ -33,6 +38,8 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
|
||||
}
|
||||
@@ -45,6 +52,21 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
"version" -> {
|
||||
if (!prefs.developerModeEnabled) {
|
||||
developerOptionsCounter += 1
|
||||
if (developerOptionsCounter >= 7) {
|
||||
prefs.developerModeEnabled = true
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
getString(R.string.developer_mode_enabled),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
"contributors" -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.about_contributors)
|
||||
@@ -53,6 +75,7 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
"github_link" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.github_link))
|
||||
true
|
||||
|
||||
@@ -12,14 +12,18 @@ import com.google.android.material.transition.MaterialFadeThrough
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
protected lateinit var prefs: PreferenceDataSource
|
||||
protected lateinit var encryptedPrefs: EncryptedPreferenceDataStore
|
||||
protected abstract val isTopLevel: Boolean
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
encryptedPrefs = EncryptedPreferenceDataStore(requireContext())
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (isTopLevel) {
|
||||
@@ -40,8 +44,6 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.fragment.app.viewModels
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
@@ -92,9 +90,9 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
private fun updateMyVehiclesSummary() {
|
||||
vm.vehicles.value?.data?.let { cars ->
|
||||
val vehicles = cars.filter { it.id in prefs.chargepriceMyVehicles }
|
||||
val summary = vehicles.map {
|
||||
val summary = vehicles.joinToString(", ") {
|
||||
"${it.brand} ${it.name}"
|
||||
}.joinToString(", ")
|
||||
}
|
||||
myVehiclePreference.summary = summary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
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.fragment.oauth.OAuthLoginFragmentArgs
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.IOException
|
||||
import java.time.Instant
|
||||
|
||||
class DataSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
@@ -23,8 +37,38 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
}
|
||||
})
|
||||
|
||||
private lateinit var teslaAccountPreference: Preference
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_data, rootKey)
|
||||
teslaAccountPreference = findPreference<Preference>("tesla_account")!!
|
||||
refreshTeslaAccountStatus()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
arguments?.let {
|
||||
val args = DataSettingsFragmentArgs.fromBundle(it)
|
||||
if (args.startTeslaLogin) {
|
||||
teslaLogin()
|
||||
arguments = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshTeslaAccountStatus()
|
||||
}
|
||||
|
||||
private fun refreshTeslaAccountStatus() {
|
||||
teslaAccountPreference.summary =
|
||||
if (encryptedPrefs.teslaRefreshToken != null) {
|
||||
getString(R.string.pref_tesla_account_enabled, encryptedPrefs.teslaEmail)
|
||||
} else {
|
||||
getString(R.string.pref_tesla_account_disabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
@@ -60,7 +104,86 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
vm.deleteRecentSearchResults()
|
||||
true
|
||||
}
|
||||
|
||||
"tesla_account" -> {
|
||||
if (encryptedPrefs.teslaRefreshToken != null) {
|
||||
teslaLogout()
|
||||
} else {
|
||||
teslaLogin()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun teslaLogin() {
|
||||
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
|
||||
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
|
||||
val uri = Uri.parse("https://auth.tesla.com/oauth2/v3/authorize").buildUpon()
|
||||
.appendQueryParameter("client_id", "ownerapi")
|
||||
.appendQueryParameter("code_challenge", codeChallenge)
|
||||
.appendQueryParameter("code_challenge_method", "S256")
|
||||
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("scope", "openid email offline_access")
|
||||
.appendQueryParameter("state", "123").build()
|
||||
|
||||
val args = OAuthLoginFragmentArgs(
|
||||
uri.toString(),
|
||||
"https://auth.tesla.com/void/callback",
|
||||
"#000000"
|
||||
).toBundle()
|
||||
|
||||
setFragmentResultListener(uri.toString()) { _, result ->
|
||||
teslaGetAccessToken(result, codeVerifier)
|
||||
}
|
||||
|
||||
findNavController().navigate(R.id.oauth_login, args)
|
||||
}
|
||||
|
||||
private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) {
|
||||
teslaAccountPreference.summary = getString(R.string.logging_in)
|
||||
|
||||
val url = Uri.parse(result.getString("url"))
|
||||
val code = url.getQueryParameter("code")!!
|
||||
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
|
||||
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val time = Instant.now().epochSecond
|
||||
val response =
|
||||
TeslaAuthenticationApi.create(okhttp).getToken(request)
|
||||
val userResponse =
|
||||
TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo()
|
||||
|
||||
encryptedPrefs.teslaEmail = userResponse.response.email
|
||||
encryptedPrefs.teslaAccessToken = response.accessToken
|
||||
encryptedPrefs.teslaAccessTokenExpiry = time + response.expiresIn
|
||||
encryptedPrefs.teslaRefreshToken = response.refreshToken
|
||||
} catch (e: IOException) {
|
||||
view?.let {
|
||||
Snackbar.make(it, R.string.connection_error, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
refreshTeslaAccountStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private fun teslaLogout() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(getString(R.string.pref_tesla_account_enabled, encryptedPrefs.teslaEmail))
|
||||
.setPositiveButton(R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(R.string.log_out) { _, _ ->
|
||||
// sign out
|
||||
encryptedPrefs.teslaRefreshToken = null
|
||||
encryptedPrefs.teslaAccessToken = null
|
||||
encryptedPrefs.teslaAccessTokenExpiry = -1
|
||||
encryptedPrefs.teslaEmail = null
|
||||
view?.let { Snackbar.make(it, R.string.logged_out, Snackbar.LENGTH_SHORT).show() }
|
||||
refreshTeslaAccountStatus()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.Preference
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
class DeveloperSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
private val locationManager: LocationManager by lazy {
|
||||
requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_developer, rootKey)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val locationPref = findPreference<Preference>("location_status")!!
|
||||
val coarseGranted = ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
val fineGranted = ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
locationPref.summary = buildString {
|
||||
append("Coarse location permission: ")
|
||||
appendLine(if (coarseGranted) "granted" else "not granted")
|
||||
append("Fine location permission: ")
|
||||
appendLine(if (fineGranted) "granted" else "not granted")
|
||||
appendLine()
|
||||
|
||||
if (coarseGranted) {
|
||||
append("Last network location: ")
|
||||
appendLine(printLocation(locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)))
|
||||
}
|
||||
if (fineGranted) {
|
||||
append("Last GPS location: ")
|
||||
appendLine(printLocation(locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.allProviders.contains(
|
||||
LocationManager.FUSED_PROVIDER
|
||||
)
|
||||
) {
|
||||
append("Last fused location: ")
|
||||
append(printLocation(locationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER)))
|
||||
} else {
|
||||
append("System's fused location provider not available")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
"disable_developer_mode" -> {
|
||||
prefs.developerModeEnabled = false
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
getString(R.string.developer_mode_disabled),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
findNavController().popBackStack()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun printLocation(location: Location?): String {
|
||||
if (location == null) return "not available"
|
||||
|
||||
return buildString {
|
||||
append("%.4f".format(location.latitude))
|
||||
append(",")
|
||||
append("%.4f".format(location.longitude))
|
||||
append(" (")
|
||||
append(DateUtils.getRelativeTimeSpanString(location.time))
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,22 @@ package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
|
||||
class SettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = true
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.settings)
|
||||
addPreferencesFromResource(R.xml.settings_variantspecific)
|
||||
findPreference<Preference>("developer_options")?.isVisible = prefs.developerModeEnabled
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
findPreference<Preference>("developer_options")?.isVisible = prefs.developerModeEnabled
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
|
||||
@@ -31,6 +31,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
|
||||
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private var gpsLocation: Location? = null
|
||||
private var networkLocation: Location? = null
|
||||
private var fusedLocation: Location? = null
|
||||
|
||||
private val supportsSystemFusedProvider: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.allProviders.contains(
|
||||
@@ -101,7 +102,6 @@ class FusionEngine(context: Context) : LocationEngine(context),
|
||||
try {
|
||||
enableFused(gpsInterval)
|
||||
checkLastKnownFused()
|
||||
return
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Permissions not granted for fused provider", e)
|
||||
}
|
||||
@@ -143,6 +143,9 @@ class FusionEngine(context: Context) : LocationEngine(context),
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
override fun disable() {
|
||||
locationManager.removeUpdates(this)
|
||||
gpsLocation = null
|
||||
networkLocation = null
|
||||
fusedLocation = null
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
@@ -235,15 +238,16 @@ class FusionEngine(context: Context) : LocationEngine(context),
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
if (LocationManager.FUSED_PROVIDER == location.provider) {
|
||||
fusedLocation = location
|
||||
requests.forEach { it.listener.onLocationChanged(location) }
|
||||
} else if (LocationManager.GPS_PROVIDER == location.provider) {
|
||||
gpsLocation = location
|
||||
if (gpsLocation.isBetterThan(networkLocation)) {
|
||||
if (gpsLocation.isBetterThan(networkLocation) && fusedLocation == null) {
|
||||
requests.forEach { it.listener.onLocationChanged(location) }
|
||||
}
|
||||
} else if (LocationManager.NETWORK_PROVIDER == location.provider) {
|
||||
networkLocation = location
|
||||
if (networkLocation.isBetterThan(gpsLocation)) {
|
||||
if (networkLocation.isBetterThan(gpsLocation) && fusedLocation == null) {
|
||||
requests.forEach { it.listener.onLocationChanged(location) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ data class ChargeLocation(
|
||||
@Embedded val address: Address?,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
val network: String?,
|
||||
val url: String,
|
||||
val editUrl: String?,
|
||||
val url: String, // URL of this charger at the data source
|
||||
val editUrl: String?, // URL to edit this charger at the data source
|
||||
@Embedded(prefix = "fault_report_") val faultReport: FaultReport?,
|
||||
val verified: Boolean,
|
||||
val barrierFree: Boolean?,
|
||||
@@ -78,6 +78,8 @@ data class ChargeLocation(
|
||||
@Embedded val cost: Cost?,
|
||||
val license: String?,
|
||||
@Embedded(prefix = "chargeprice") val chargepriceData: ChargepriceData?,
|
||||
val networkUrl: String?, // Website of the network
|
||||
val chargerUrl: String?, // Website for this specific charging site. Might be an ad-hoc payment page.
|
||||
val timeRetrieved: Instant,
|
||||
val isDetailed: Boolean
|
||||
) : ChargepointListItem(), Equatable, Parcelable {
|
||||
@@ -136,9 +138,9 @@ data class ChargeLocation(
|
||||
get() = chargepoints.sumOf { it.count }
|
||||
|
||||
fun formatChargepoints(sp: StringProvider): String {
|
||||
return chargepointsMerged.map {
|
||||
return chargepointsMerged.joinToString(" · ") {
|
||||
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower()}"
|
||||
}.joinToString(" · ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,6 @@ data class Favorite(
|
||||
)
|
||||
|
||||
data class FavoriteWithDetail(
|
||||
@Embedded() val favorite: Favorite,
|
||||
@Embedded val favorite: Favorite,
|
||||
@Embedded val charger: ChargeLocation
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ import net.vonforst.evmap.model.*
|
||||
OCMConnectionType::class,
|
||||
OCMCountry::class,
|
||||
OCMOperator::class
|
||||
], version = 18
|
||||
], version = 19
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -56,7 +56,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
|
||||
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
|
||||
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
|
||||
MIGRATION_17, MIGRATION_18
|
||||
MIGRATION_17, MIGRATION_18, MIGRATION_19
|
||||
)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
@@ -78,7 +78,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
// SQL for creating tables copied from build/generated/source/kapt/debug/net/vonforst/evmap/storage/AppDatbase_Impl
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `BooleanFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `MultipleChoiceFilterValue` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, PRIMARY KEY(`key`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))");
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
// recreate ChargeLocation table to make postcode nullable
|
||||
db.beginTransaction()
|
||||
try {
|
||||
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT NOT NULL, `country` TEXT NOT NULL, `postcode` TEXT, `street` TEXT NOT NULL, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
|
||||
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT NOT NULL, `country` TEXT NOT NULL, `postcode` TEXT, `street` TEXT NOT NULL, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))")
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`")
|
||||
db.execSQL("DROP TABLE `ChargeLocation`")
|
||||
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
|
||||
db.setTransactionSuccessful()
|
||||
@@ -109,8 +109,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
// recreate ChargeLocation table to make other address fields nullable
|
||||
db.beginTransaction()
|
||||
try {
|
||||
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
|
||||
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))")
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`")
|
||||
db.execSQL("DROP TABLE `ChargeLocation`")
|
||||
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
|
||||
db.setTransactionSuccessful()
|
||||
@@ -160,7 +160,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
// add profile column to existing filtervalue tables
|
||||
db.execSQL("CREATE TABLE `BooleanFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE `MultipleChoiceFilterValueNew` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
|
||||
for (table in listOf(
|
||||
"BooleanFilterValue",
|
||||
@@ -202,7 +202,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
//////////////////////////////////////////
|
||||
db.execSQL("CREATE TABLE `OCMConnectionType` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))")
|
||||
db.execSQL("CREATE TABLE `OCMCountry` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||
db.execSQL("CREATE TABLE `OCMOperator` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))");
|
||||
db.execSQL("CREATE TABLE `OCMOperator` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))")
|
||||
|
||||
//////////////////////////////////////////
|
||||
// rename GoingElectric-specific tables //
|
||||
@@ -295,7 +295,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
// update ChargeLocation table to change primary key
|
||||
db.execSQL(
|
||||
"CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))"
|
||||
);
|
||||
)
|
||||
val columnList =
|
||||
"`id`,`dataSource`,`name`,`chargepoints`,`network`,`url`,`editUrl`,`verified`,`barrierFree`,`operator`,`generalInformation`,`amenities`,`locationDescription`,`photos`,`chargecards`,`license`,`lat`,`lng`,`city`,`country`,`postcode`,`street`,`fault_report_created`,`fault_report_description`,`twentyfourSeven`,`description`,`mostart`,`moend`,`tustart`,`tuend`,`westart`,`weend`,`thstart`,`thend`,`frstart`,`frend`,`sastart`,`saend`,`sustart`,`suend`,`hostart`,`hoend`,`freecharging`,`freeparking`,`descriptionShort`,`descriptionLong`,`chargepricecountry`,`chargepricenetwork`,`chargepriceplugTypes`"
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew`($columnList) SELECT $columnList FROM `ChargeLocation`")
|
||||
@@ -311,7 +311,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
private val MIGRATION_14 = object : Migration(13, 14) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `RecentAutocompletePlace` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))");
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `RecentAutocompletePlace` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -321,7 +321,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
try {
|
||||
db.beginTransaction()
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Favorite` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE RESTRICT )");
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Favorite` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE RESTRICT )")
|
||||
|
||||
val cursor = db.query("SELECT * FROM `ChargeLocation`")
|
||||
while (cursor.moveToNext()) {
|
||||
@@ -361,7 +361,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
try {
|
||||
db.beginTransaction()
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `FavoriteNew` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )");
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `FavoriteNew` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )")
|
||||
val columnList =
|
||||
"`favoriteId`,`chargerId`,`chargerDataSource`"
|
||||
db.execSQL("INSERT INTO `FavoriteNew`($columnList) SELECT $columnList FROM `Favorite`")
|
||||
@@ -376,5 +376,12 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val MIGRATION_19 = object : Migration(18, 19) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `networkUrl` TEXT")
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargerUrl` TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* Encrypted data storage for sensitive data such as API access tokens.
|
||||
* This will not be included in backups.
|
||||
*/
|
||||
class EncryptedPreferenceDataStore(context: Context) : TeslaAvailabilityDetector.TokenStore {
|
||||
val sp = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"encrypted_prefs",
|
||||
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build(),
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
override var teslaRefreshToken: String?
|
||||
get() = sp.getString(
|
||||
"tesla_refresh_token", null
|
||||
)
|
||||
set(value) {
|
||||
sp.edit().putString("tesla_refresh_token", value).apply()
|
||||
}
|
||||
override var teslaAccessToken: String?
|
||||
get() = sp.getString("tesla_access_token", null)
|
||||
set(value) {
|
||||
sp.edit().putString("tesla_access_token", value).apply()
|
||||
}
|
||||
override var teslaAccessTokenExpiry: Long
|
||||
get() = sp.getLong("tesla_access_token_expiry", -1)
|
||||
set(value) {
|
||||
sp.edit().putLong("tesla_access_token_expiry", value).apply()
|
||||
}
|
||||
|
||||
var teslaEmail: String?
|
||||
get() = sp.getString("tesla_email", null)
|
||||
set(value) {
|
||||
sp.edit().putString("tesla_email", value).apply()
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun toChargerPhotoList(value: String): List<ChargerPhoto>? {
|
||||
return chargerPhotoListAdapter.fromJson(value)?.filterNotNull()
|
||||
return chargerPhotoListAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.view.View
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import net.vonforst.evmap.R
|
||||
import java.time.Duration
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -28,8 +29,8 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
private val dp = context.resources.displayMetrics.density
|
||||
private val sp = context.resources.displayMetrics.scaledDensity
|
||||
var zeroHeight = 4 * dp
|
||||
var barWidth = (16 * dp).roundToInt()
|
||||
var barMargin = (2 * dp).roundToInt()
|
||||
var barWidth = 16 * dp
|
||||
var barMargin = 2 * dp
|
||||
var legendWidth = 12 * dp
|
||||
var legendLineLength = 4 * dp
|
||||
var legendLineWidth = 1 * dp
|
||||
@@ -42,24 +43,27 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
var barDrawable =
|
||||
AppCompatResources.getDrawable(context, R.drawable.bar_graph)!!
|
||||
var colorAvailable = ContextCompat.getColor(context, R.color.available)
|
||||
var colorSomeAvailable = ContextCompat.getColor(context, R.color.some_available)
|
||||
var colorUnavailable = ContextCompat.getColor(context, R.color.unavailable)
|
||||
|
||||
var data: Map<ZonedDateTime, Int>? = null
|
||||
var data: Map<ZonedDateTime, Double>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
var maxValue: Int? = null
|
||||
var maxValue: Double? = null
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
var isPercentage: Boolean = false
|
||||
|
||||
var activeAlpha = 0.87f
|
||||
var inactiveAlpha = 0.60f
|
||||
|
||||
private val legendPaint = Paint().apply {
|
||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
|
||||
val ta =
|
||||
context.theme.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.colorControlNormal))
|
||||
color = ta.getColor(0, 0)
|
||||
strokeWidth = legendLineWidth
|
||||
textSize = legendWidth - legendLineLength
|
||||
@@ -110,22 +114,28 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
plusMinutes((minutesRound - minute).toLong())
|
||||
}
|
||||
data = (0..20).associate {
|
||||
now.plusMinutes(15L * it) to (Math.random() * 8).roundToInt()
|
||||
now.plusMinutes(15L * it) to (Math.random() * 8)
|
||||
}
|
||||
maxValue = 8
|
||||
maxValue = 8.0
|
||||
}
|
||||
val data = data?.toSortedMap() ?: return
|
||||
if (data.isEmpty()) return
|
||||
val maxValue = maxValue ?: data.maxOf { it.value }
|
||||
|
||||
val graphWidth = graphBounds?.width() ?: return
|
||||
val n = data.size
|
||||
val barMarginFactor = 0.1f
|
||||
barWidth = graphWidth / (n + barMarginFactor * (n - 1))
|
||||
barMargin = barWidth * barMarginFactor
|
||||
|
||||
drawGraph(canvas, data, maxValue)
|
||||
drawBubble(canvas, data, maxValue)
|
||||
}
|
||||
|
||||
private fun drawGraph(
|
||||
canvas: Canvas,
|
||||
data: SortedMap<ZonedDateTime, Int>,
|
||||
maxValue: Int
|
||||
data: SortedMap<ZonedDateTime, Double>,
|
||||
maxValue: Double
|
||||
) {
|
||||
val graphBounds = graphBounds ?: return
|
||||
|
||||
@@ -139,26 +149,30 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
)
|
||||
|
||||
legendPaint.textAlign = Paint.Align.CENTER
|
||||
|
||||
data.entries.forEachIndexed { i, (t, v) ->
|
||||
val height =
|
||||
zeroHeight + (graphBounds.height() - zeroHeight) * v.toFloat() / maxValue
|
||||
zeroHeight + (graphBounds.height() - zeroHeight) * v.toFloat() / maxValue.toFloat()
|
||||
val left = graphBounds.left + (barWidth + barMargin) * i
|
||||
|
||||
if (left + barWidth > graphBounds.right) return@forEachIndexed
|
||||
|
||||
barDrawable.setBounds(
|
||||
left,
|
||||
0,
|
||||
graphBounds.bottom - height.roundToInt(),
|
||||
left + barWidth,
|
||||
barWidth.roundToInt(),
|
||||
graphBounds.bottom
|
||||
)
|
||||
|
||||
canvas.translate(left, 0f)
|
||||
barDrawable.alpha =
|
||||
((if (i == selectedBar) activeAlpha else inactiveAlpha) * 255).roundToInt()
|
||||
barDrawable.setTint(getColor(v, maxValue))
|
||||
barDrawable.draw(canvas)
|
||||
canvas.translate(-left, 0f)
|
||||
|
||||
val center = left.toFloat() + barWidth / 2
|
||||
if (t.minute == 0) {
|
||||
val center = left + barWidth / 2
|
||||
if (shouldDrawLabel(t, data)) {
|
||||
drawLine(
|
||||
center, graphBounds.bottom.toFloat(),
|
||||
center, graphBounds.bottom + legendLineLength, legendPaint
|
||||
@@ -196,19 +210,44 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
)
|
||||
|
||||
legendPaint.textAlign = Paint.Align.LEFT
|
||||
drawText(
|
||||
this@BarGraphView.maxValue.toString(),
|
||||
graphBounds.right.toFloat() + legendLineLength,
|
||||
graphBounds.top + (legendWidth - legendLineLength) / 3,
|
||||
legendPaint
|
||||
)
|
||||
if (!isPercentage) {
|
||||
drawText(
|
||||
maxValue.roundToInt().toString(),
|
||||
graphBounds.right.toFloat() + legendLineLength,
|
||||
graphBounds.top + (legendWidth - legendLineLength) / 3,
|
||||
legendPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getColor(v: Int, maxValue: Int) =
|
||||
if (v < maxValue) colorAvailable else colorUnavailable
|
||||
private fun shouldDrawLabel(t: ZonedDateTime, data: SortedMap<ZonedDateTime, Double>): Boolean {
|
||||
val ts = data.keys.toList()
|
||||
return if (Duration.between(ts[0], ts[1]) > Duration.ofMinutes(31)) {
|
||||
// label every 6 hours
|
||||
t.hour % 6 == 0
|
||||
} else {
|
||||
// label every 15 minutes
|
||||
t.minute == 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
|
||||
private fun getColor(v: Double, maxValue: Double) =
|
||||
if (isPercentage) {
|
||||
when (v) {
|
||||
in 0.0..0.5 -> colorAvailable
|
||||
in 0.5..0.8 -> colorSomeAvailable
|
||||
else -> colorUnavailable
|
||||
}
|
||||
} else {
|
||||
if (v < maxValue) colorAvailable else colorUnavailable
|
||||
}
|
||||
|
||||
private fun drawBubble(
|
||||
canvas: Canvas,
|
||||
data: SortedMap<ZonedDateTime, Double>,
|
||||
maxValue: Double
|
||||
) {
|
||||
val bubbleBounds = bubbleBounds ?: return
|
||||
val graphBounds = graphBounds ?: return
|
||||
val d = data.toList()
|
||||
@@ -221,12 +260,16 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
R.string.prediction_time_colon,
|
||||
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)
|
||||
)
|
||||
val availableformat = context.resources.getQuantityString(
|
||||
R.plurals.prediction_number_available,
|
||||
maxValue - v,
|
||||
maxValue - v,
|
||||
maxValue
|
||||
)
|
||||
val availableformat = if (isPercentage) {
|
||||
"%.0f %%".format(v * 100)
|
||||
} else {
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.prediction_number_available,
|
||||
(maxValue - v).roundToInt(),
|
||||
(maxValue - v).roundToInt(),
|
||||
maxValue.roundToInt()
|
||||
)
|
||||
}
|
||||
val text = SpannableString("$tformat $availableformat").apply {
|
||||
setSpan(
|
||||
ForegroundColorSpan(getColor(v, maxValue)),
|
||||
@@ -297,7 +340,7 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
|
||||
private fun updateSelectedBar(x: Int) {
|
||||
val graphBounds = graphBounds ?: return
|
||||
val bar = (x - graphBounds.left) / (barWidth + barMargin)
|
||||
val bar = ((x - graphBounds.left) / (barWidth + barMargin)).roundToInt()
|
||||
if (bar != selectedBar) {
|
||||
selectedBar = bar
|
||||
invalidate()
|
||||
|
||||
@@ -110,9 +110,9 @@ private fun activeTint(
|
||||
val color = context.theme.obtainStyledAttributes(
|
||||
intArrayOf(
|
||||
if (isColored) {
|
||||
R.attr.colorPrimary
|
||||
androidx.appcompat.R.attr.colorPrimary
|
||||
} else {
|
||||
R.attr.colorControlNormal
|
||||
androidx.appcompat.R.attr.colorControlNormal
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -169,9 +169,10 @@ fun setBackgroundTintAvailability(view: View, available: List<ChargepointStatus>
|
||||
@BindingAdapter("selectableItemBackground")
|
||||
fun applySelectableItemBackground(view: View, apply: Boolean) {
|
||||
if (apply) {
|
||||
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
|
||||
view.background = it.getDrawable(0)
|
||||
}
|
||||
view.context.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.selectableItemBackground))
|
||||
.use {
|
||||
view.background = it.getDrawable(0)
|
||||
}
|
||||
} else {
|
||||
view.background = null
|
||||
}
|
||||
@@ -263,7 +264,8 @@ private fun availabilityColor(
|
||||
ContextCompat.getColor(context, R.color.charging)
|
||||
}
|
||||
} else {
|
||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
|
||||
val ta =
|
||||
context.theme.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.colorControlNormal))
|
||||
ta.getColor(0, 0)
|
||||
}
|
||||
|
||||
@@ -295,16 +297,16 @@ fun currency(currency: String): String {
|
||||
"GBP" -> "£"
|
||||
"HRK" -> "kn"
|
||||
"HUF" -> "Ft"
|
||||
"ISK" -> "Kr"
|
||||
"ISK" -> "kr"
|
||||
else -> currency
|
||||
}
|
||||
}
|
||||
|
||||
fun time(value: Int): String {
|
||||
val h = floor(value.toDouble() / 60).toInt();
|
||||
val min = ceil(value.toDouble() % 60).toInt();
|
||||
return if (h == 0 && min > 0) "$min min";
|
||||
else "%d:%02d h".format(h, min);
|
||||
val h = floor(value.toDouble() / 60).toInt()
|
||||
val min = ceil(value.toDouble() % 60).toInt()
|
||||
return if (h == 0 && min > 0) "$min min"
|
||||
else "%d:%02d h".format(h, min)
|
||||
}
|
||||
|
||||
fun distance(meters: Number?): String? {
|
||||
@@ -371,9 +373,10 @@ fun tariffBackground(context: Context, myTariff: Boolean, brandingColor: String?
|
||||
return drawable
|
||||
}
|
||||
else -> {
|
||||
context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
|
||||
return it.getDrawable(0)
|
||||
}
|
||||
context.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.selectableItemBackground))
|
||||
.use {
|
||||
return it.getDrawable(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,4 +423,9 @@ fun setImageTint(view: ImageView, @ColorInt tint: Int?) {
|
||||
} else {
|
||||
view.imageTintList = null
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("isPercentage")
|
||||
fun setIsPercentage(view: BarGraphView, value: Boolean) {
|
||||
view.isPercentage = value
|
||||
}
|
||||
@@ -15,9 +15,9 @@ class CheckableConstraintLayout(ctx: Context, attrs: AttributeSet) : ConstraintL
|
||||
|
||||
override fun setChecked(b: Boolean) {
|
||||
if (b != checked) {
|
||||
checked = b;
|
||||
refreshDrawableState();
|
||||
onCheckedChangeListener?.invoke(this, checked);
|
||||
checked = b
|
||||
refreshDrawableState()
|
||||
onCheckedChangeListener?.invoke(this, checked)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.vonforst.evmap.ui;
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.google.maps.android.clustering.ClusterItem
|
||||
|
||||
@@ -101,12 +101,12 @@ class HideOnExpandFabBehavior(context: Context, attrs: AttributeSet) :
|
||||
type,
|
||||
consumed
|
||||
)
|
||||
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
|
||||
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
|
||||
// User scrolled down and the FAB is currently visible -> hide the FAB
|
||||
child.hide();
|
||||
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
|
||||
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();
|
||||
child.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,12 +98,12 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
|
||||
type,
|
||||
consumed
|
||||
)
|
||||
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
|
||||
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
|
||||
// User scrolled down and the FAB is currently visible -> hide the FAB
|
||||
child.hide();
|
||||
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
|
||||
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();
|
||||
child.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,10 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
|
||||
)
|
||||
id = R.id.amu_text
|
||||
setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi)
|
||||
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat)
|
||||
TextViewCompat.setTextAppearance(
|
||||
this,
|
||||
androidx.appcompat.R.style.TextAppearance_AppCompat
|
||||
)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
}
|
||||
}
|
||||
@@ -64,7 +67,7 @@ class ChargerIconGenerator(
|
||||
// 340 items:
|
||||
// large: (21 sizes, 5 colors, multi on/off) + highlight + fault + fav (only with scale = 1)
|
||||
// mini: (11 sizes, 5 colors) + highlight (only with scale = 1)
|
||||
private val cacheSize = (scaleResolution + 8) * 5 * 2 + (scaleResolutionMini + 2) * 5;
|
||||
private val cacheSize = (scaleResolution + 8) * 5 * 2 + (scaleResolutionMini + 2) * 5
|
||||
private val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
|
||||
private val icon = R.drawable.ic_map_marker_charging
|
||||
private val multiIcon = R.drawable.ic_map_marker_charging_multiple
|
||||
|
||||
@@ -116,20 +116,18 @@ class ChargepriceViewModel(
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
value = state["chargePrices"] ?: Resource.loading(null)
|
||||
listOf(
|
||||
charger,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors,
|
||||
myTariffs, myTariffsAll
|
||||
myTariffs, myTariffsAll, charger
|
||||
).forEach {
|
||||
addSource(it.distinctUntilChanged()) {
|
||||
if (!batteryRangeSliderDragging.value!!) loadPrices()
|
||||
if (!batteryRangeSliderDragging.value!!) {
|
||||
loadPrices()
|
||||
state["chargePrices"] = this.value
|
||||
}
|
||||
}
|
||||
}
|
||||
observeForever {
|
||||
// persist data in case fragment gets recreated
|
||||
state["chargePrices"] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +151,7 @@ class ChargepriceViewModel(
|
||||
value = Resource.loading(null)
|
||||
} else {
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
value = Resource.success(cps.data!!.map { cp ->
|
||||
value = Resource.success(cps.data!!.mapNotNull { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == getChargepricePlugType(chargepoint) && it.power == chargepoint.power
|
||||
@@ -165,7 +163,7 @@ class ChargepriceViewModel(
|
||||
chargepointPrices = filteredPrices
|
||||
)
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
|
||||
@@ -7,9 +7,9 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Favorite
|
||||
import net.vonforst.evmap.model.FavoriteWithDetail
|
||||
@@ -19,6 +19,7 @@ import net.vonforst.evmap.utils.distanceBetween
|
||||
class FavoritesViewModel(application: Application) :
|
||||
AndroidViewModel(application) {
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private val availabilityRepo = AvailabilityRepository(application)
|
||||
|
||||
val favorites: LiveData<List<FavoriteWithDetail>> by lazy {
|
||||
db.favoritesDao().getAllFavorites()
|
||||
@@ -53,7 +54,7 @@ class FavoritesViewModel(application: Application) :
|
||||
|
||||
chargers.map { charger ->
|
||||
async {
|
||||
data[charger.id] = getAvailability(charger)
|
||||
data[charger.id] = availabilityRepo.getAvailability(charger)
|
||||
availability.value = data
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
@@ -14,8 +14,9 @@ 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.getAvailability
|
||||
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.api.fronyx.FronyxApi
|
||||
@@ -30,12 +31,16 @@ import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
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
|
||||
|
||||
@Parcelize
|
||||
@@ -53,12 +58,14 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
AndroidViewModel(application) {
|
||||
private val db = AppDatabase.getInstance(application)
|
||||
private val prefs = PreferenceDataSource(application)
|
||||
private val encryptedPrefs = EncryptedPreferenceDataStore(application)
|
||||
private val repo = ChargeLocationsRepository(
|
||||
createApi(prefs.dataSource, application),
|
||||
viewModelScope,
|
||||
db,
|
||||
prefs
|
||||
)
|
||||
private val availabilityRepo = AvailabilityRepository(application)
|
||||
|
||||
val apiId = repo.api.map { it.id }
|
||||
|
||||
@@ -202,7 +209,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
triggerAvailabilityRefresh.switchMap {
|
||||
liveData {
|
||||
emit(Resource.loading(null))
|
||||
emit(getAvailability(charger))
|
||||
emit(availabilityRepo.getAvailability(charger))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +237,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
val teslaPricing = availability.map {
|
||||
it.data?.extraData as? TeslaGraphQlApi.Pricing
|
||||
}
|
||||
|
||||
val predictionApi = FronyxApi(application.getString(R.string.fronyx_key))
|
||||
|
||||
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
|
||||
@@ -280,33 +291,45 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
val predictionGraph: LiveData<Map<ZonedDateTime, Int>?> by lazy {
|
||||
prediction.map {
|
||||
it.data?.let { responses ->
|
||||
if (responses.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val evseIds = responses.map { it.evseId }
|
||||
val groupByTimestamp = responses.flatMap { response ->
|
||||
response.predictions.map {
|
||||
Triple(
|
||||
it.timestamp,
|
||||
response.evseId,
|
||||
it.status
|
||||
)
|
||||
val predictionGraph: LiveData<Map<ZonedDateTime, Double>?> =
|
||||
MediatorLiveData<Map<ZonedDateTime, Double>?>().apply {
|
||||
listOf(prediction, availability).forEach {
|
||||
addSource(it) {
|
||||
val congestionHistogram = availability.value?.data?.congestionHistogram
|
||||
val prediction = prediction.value?.data
|
||||
value = if (congestionHistogram != null && prediction == null) {
|
||||
congestionHistogram.mapIndexed { i, value ->
|
||||
LocalTime.of(i, 0).atDate(LocalDate.now())
|
||||
.atZone(ZoneId.systemDefault()) to value
|
||||
}.toMap()
|
||||
} else {
|
||||
prediction?.let { responses ->
|
||||
if (responses.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val evseIds = responses.map { it.evseId }
|
||||
val groupByTimestamp = responses.flatMap { response ->
|
||||
response.predictions.map {
|
||||
Triple(
|
||||
it.timestamp,
|
||||
response.evseId,
|
||||
it.status
|
||||
)
|
||||
}
|
||||
}
|
||||
.groupBy { it.first } // group by timestamp
|
||||
.mapValues { it.value.map { it.second to it.third } } // only keep EVSEID and status
|
||||
.filterValues { it.map { it.first } == evseIds } // remove values where status is not given for all EVSEs
|
||||
.filterKeys { it > ZonedDateTime.now() } // only show predictions in the future
|
||||
|
||||
groupByTimestamp.mapValues {
|
||||
it.value.count {
|
||||
it.second == FronyxStatus.UNAVAILABLE
|
||||
}.toDouble()
|
||||
}.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
.groupBy { it.first } // group by timestamp
|
||||
.mapValues { it.value.map { it.second to it.third } } // only keep EVSEID and status
|
||||
.filterValues { it.map { it.first } == evseIds } // remove values where status is not given for all EVSEs
|
||||
.filterKeys { it > ZonedDateTime.now() } // only show predictions in the future
|
||||
|
||||
groupByTimestamp.mapValues {
|
||||
it.value.count {
|
||||
it.second == FronyxStatus.UNAVAILABLE
|
||||
}
|
||||
}.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,9 +349,25 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
val predictionMaxValue: LiveData<Int> by lazy {
|
||||
predictedChargepoints.map {
|
||||
it?.sumOf { it.count } ?: 0
|
||||
val predictionMaxValue: LiveData<Double> = MediatorLiveData<Double>().apply {
|
||||
listOf(prediction, availability).forEach {
|
||||
addSource(it) {
|
||||
value =
|
||||
if (availability.value?.data?.congestionHistogram != null && prediction.value?.data == null) {
|
||||
1.0
|
||||
} else {
|
||||
(predictedChargepoints.value?.sumOf { it.count } ?: 0).toDouble()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val predictionIsPercentage: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
|
||||
listOf(prediction, availability).forEach {
|
||||
addSource(it) {
|
||||
value =
|
||||
availability.value?.data?.congestionHistogram != null && prediction.value?.data == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,11 +428,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
private var hasTeslaLogin: MutableLiveData<Boolean> = state.getLiveData("hasTeslaLogin")
|
||||
|
||||
fun reloadPrefs() {
|
||||
filterStatus.value = prefs.filterStatus
|
||||
if (prefs.dataSource != apiId.value) {
|
||||
repo.api.value = createApi(prefs.dataSource, getApplication())
|
||||
}
|
||||
if (hasTeslaLogin.value != (encryptedPrefs.teslaAccessToken != null)) {
|
||||
hasTeslaLogin.value = encryptedPrefs.teslaAccessToken != null
|
||||
reloadAvailability()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFilters() {
|
||||
|
||||
@@ -110,7 +110,7 @@ fun <T> throttleLatest(
|
||||
suspend fun <T> LiveData<T>.await(): T {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T?) {
|
||||
override fun onChanged(value: T) {
|
||||
if (value == null) return
|
||||
removeObserver(this)
|
||||
continuation.resume(value, null)
|
||||
|
||||
@@ -4,35 +4,35 @@
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13.45,17.08a8.24,8.24 0,0 1,-3.11 0.6,8.34 8.34,0 0,1 -6,-14.18H16.3a8.35,8.35 0,0 1,1.07 10.33"
|
||||
android:pathData="M6.2,13.8C4.1,10.6 4.6,6.3 7.3,3.5h12c1.5,1.6 2.4,3.7 2.4,5.9c0,4.6 -3.8,8.3 -8.4,8.3c-1.1,0 -2.1,-0.2 -3.1,-0.6"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000" />
|
||||
android:strokeColor="#000000" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.34,9.34m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
|
||||
android:pathData="M13.3,9.3m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.35,9.34m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
|
||||
android:pathData="M8.3,9.3m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.84,13.51m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
|
||||
android:pathData="M10.8,13.5m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.84,13.51m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
|
||||
android:pathData="M15.8,13.5m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5.34,9.34m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
|
||||
android:pathData="M18.3,9.3m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.84,5.59m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
|
||||
android:pathData="M15.8,5.6m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.84,5.59m-1.04,0a1.04,1.04 0,1 1,2.08 0a1.04,1.04 0,1 1,-2.08 0" />
|
||||
android:pathData="M10.8,5.6m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18.18,22.23l1,-5.48c0.93,0 1.22,0.1 1.27,0.52a2.15,2.15 0,0 0,0.93 -0.7,6.91 6.91,0 0,0 -2.46,-0.6l-0.71,0.88h0L17.46,16a7,7 0,0 0,-2.46 0.6,2.22 2.22,0 0,0 0.94,0.7c0,-0.42 0.33,-0.52 1.26,-0.52l1,5.48" />
|
||||
android:pathData="M5.4,22.3l1,-5.5c0.9,0 1.3,0.1 1.3,0.5c0.4,-0.1 0.7,-0.4 0.9,-0.7C7.8,16.3 7,16 6.1,16l-0.8,0.8l0,0L4.7,16c-0.8,0 -1.7,0.3 -2.5,0.6c0.2,0.3 0.6,0.6 0.9,0.7c0.1,-0.4 0.3,-0.5 1.3,-0.5L5.4,22.3" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18.18,15.72a7.9,7.9 0,0 1,3.28 0.66,2.65 2.65,0 0,0 0.2,-0.4 9.24,9.24 0,0 0,-7 0,2.61 2.61,0 0,0 0.19,0.4 7.94,7.94 0,0 1,3.29 -0.66h0" />
|
||||
android:pathData="M5.5,15.7L5.5,15.7c1.1,0 2.3,0.2 3.3,0.7c0.1,-0.1 0.1,-0.3 0.2,-0.4c-2.2,-0.9 -4.8,-0.9 -7,0c0.1,0.1 0.1,0.3 0.2,0.4C3.2,15.9 4.3,15.7 5.5,15.7" />
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/ic_developer.xml
Normal file
10
app/src/main/res/drawable/ic_developer.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_link.xml
Normal file
10
app/src/main/res/drawable/ic_link.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z" />
|
||||
</vector>
|
||||
15
app/src/main/res/drawable/ic_tesla.xml
Normal file
15
app/src/main/res/drawable/ic_tesla.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="253.5"
|
||||
android:viewportWidth="254.58"
|
||||
android:width="24.10225dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#e82127"
|
||||
android:pathData="M127.31,253 L162.78,53.48c33.81,0 44.48,3.71 46.02,18.84 0,0 22.68,-8.46 34.13,-25.64C198.28,26 153.42,25.07 153.42,25.07l-26.18,31.88 0.06,-0 -26.18,-31.88c0,0 -44.86,0.93 -89.5,21.62 11.43,17.18 34.12,25.64 34.12,25.64 1.55,-15.14 12.2,-18.84 45.79,-18.87l35.76,199.55"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#e82127"
|
||||
android:pathData="m127.29,15.86c36.09,-0.28 77.4,5.58 119.69,24.01 5.65,-10.17 7.11,-14.67 7.11,-14.67C207.86,6.92 164.57,0.66 127.29,0.5 90.01,0.66 46.72,6.92 0.5,25.21c0,0 2.06,5.54 7.1,14.67 42.28,-18.43 83.6,-24.29 119.69,-24.01h0"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -45,11 +45,15 @@
|
||||
|
||||
<variable
|
||||
name="predictionGraph"
|
||||
type="Map<ZonedDateTime, Integer>" />
|
||||
type="Map<ZonedDateTime, Double>" />
|
||||
|
||||
<variable
|
||||
name="predictionMaxValue"
|
||||
type="Integer" />
|
||||
type="Double" />
|
||||
|
||||
<variable
|
||||
name="predictionIsPercentage"
|
||||
type="Boolean" />
|
||||
|
||||
<variable
|
||||
name="predictionDescription"
|
||||
@@ -59,6 +63,10 @@
|
||||
name="filteredAvailability"
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
|
||||
<variable
|
||||
name="teslaPricing"
|
||||
type="net.vonforst.evmap.api.availability.TeslaGraphQlApi.Pricing" />
|
||||
|
||||
<variable
|
||||
name="chargeCards"
|
||||
type="java.util.Map<Long, ChargeCard>" />
|
||||
@@ -279,7 +287,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
|
||||
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, teslaPricing, context)}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -311,10 +319,11 @@
|
||||
android:id="@+id/textView13"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="right|end"
|
||||
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : @string/realtime_data_unavailable}"
|
||||
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : availability.message == "not signed in" ? @string/realtime_data_login_needed : @string/realtime_data_unavailable}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnLogin"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors"
|
||||
tools:text="Echtzeitdaten nicht verfügbar" />
|
||||
@@ -330,20 +339,6 @@
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toTopOf="@+id/txtName" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnChargeprice"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
android:transitionName="@string/shared_element_chargeprice"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}"
|
||||
app:icon="@drawable/ic_chargeprice"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider1" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="match_parent"
|
||||
@@ -359,14 +354,15 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnChargeprice" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/buttonsScroller" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/utilization_prediction"
|
||||
android:text="@{predictionIsPercentage ? @string/average_utilization : @string/utilization_prediction}"
|
||||
tools:text="@string/utilization_prediction"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
@@ -381,7 +377,7 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{predictionDescription}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:goneUnless="@{predictionGraph != null && !predictionIsPercentage}"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
|
||||
app:layout_constraintStart_toEndOf="@+id/textView8"
|
||||
@@ -393,7 +389,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/help"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:goneUnless="@{predictionGraph != null && !predictionIsPercentage}"
|
||||
app:icon="@drawable/ic_help"
|
||||
app:iconTint="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textView8"
|
||||
@@ -411,6 +407,7 @@
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView8"
|
||||
app:maxValue="@{predictionMaxValue}"
|
||||
app:isPercentage="@{predictionIsPercentage}"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector"
|
||||
@@ -424,7 +421,7 @@
|
||||
android:adjustViewBounds="true"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:scaleType="fitCenter"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:goneUnless="@{predictionGraph != null && !predictionIsPercentage}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/prediction"
|
||||
app:srcCompat="@drawable/ic_powered_by_fronyx"
|
||||
@@ -501,6 +498,57 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/textView7" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/buttonsScroller"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider1"
|
||||
app:layout_constrainedWidth="true"
|
||||
android:fillViewport="true"
|
||||
app:goneUnless="@{charger.data != null && (ChargepriceApi.isChargerSupported(charger.data) || charger.data.chargerUrl != null)}">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnChargeprice"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
android:transitionName="@string/shared_element_chargeprice"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}"
|
||||
app:icon="@drawable/ic_chargeprice" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnChargerWebsite"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/charger_website"
|
||||
app:goneUnless="@{charger.data != null && charger.data.chargerUrl != null}"
|
||||
app:icon="@drawable/ic_link" />
|
||||
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnLogin"
|
||||
style="@style/Widget.Material3.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:text="@string/login"
|
||||
app:goneUnless="@{availability.status == Status.ERROR && availability.message == "not signed in"}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textView13"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textView13" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -199,12 +199,14 @@
|
||||
app:filteredAvailability="@{vm.filteredAvailability}"
|
||||
app:predictionGraph="@{vm.predictionGraph}"
|
||||
app:predictionMaxValue="@{vm.predictionMaxValue}"
|
||||
app:predictionIsPercentage="@{vm.predictionIsPercentage}"
|
||||
app:predictionDescription="@{vm.predictionDescription}"
|
||||
app:chargeCards="@{vm.chargeCardMap}"
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}"
|
||||
app:distance="@{vm.chargerDistance}"
|
||||
app:expanded="@{vm.bottomSheetExpanded}"
|
||||
app:apiName="@{vm.apiName}" />
|
||||
app:apiName="@{vm.apiName}"
|
||||
app:teslaPricing="@{vm.teslaPricing}" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
36
app/src/main/res/layout/fragment_oauth_login.xml
Normal file
36
app/src/main/res/layout/fragment_oauth_login.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:actionBarSize" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:indeterminate="true"
|
||||
app:hideAnimationBehavior="inward"
|
||||
app:indicatorColor="@color/colorSecondary"
|
||||
app:showAnimationBehavior="outward" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent" />
|
||||
</LinearLayout>
|
||||
@@ -78,7 +78,12 @@
|
||||
android:id="@+id/settings_data"
|
||||
android:name="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
|
||||
android:label="@string/settings_data_sources"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
tools:layout="@layout/fragment_preference">
|
||||
<argument
|
||||
android:name="startTeslaLogin"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settings_chargeprice"
|
||||
android:name="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
|
||||
@@ -89,6 +94,11 @@
|
||||
android:name="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
|
||||
android:label="@string/settings_android_auto"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/settings_developer"
|
||||
android:name="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
|
||||
android:label="@string/developer_options"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<navigation
|
||||
android:id="@+id/favs"
|
||||
app:startDestination="@id/favs_frag">
|
||||
@@ -164,4 +174,19 @@
|
||||
app:popUpTo="@id/onboarding"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/oauth_login"
|
||||
android:name="net.vonforst.evmap.fragment.oauth.OAuthLoginFragment"
|
||||
android:label="@string/login">
|
||||
<argument
|
||||
android:name="url"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="resultUrlPrefix"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="color"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
@@ -26,6 +26,7 @@
|
||||
<string name="amenities">Ausstattung</string>
|
||||
<string name="general_info">Allgemein</string>
|
||||
<string name="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
|
||||
<string name="realtime_data_login_needed">Tesla-Account für Echtzeitdaten benötigt</string>
|
||||
<string name="realtime_data_loading">Prüfe Echtzeitstatus…</string>
|
||||
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
|
||||
<string name="source">Quelle: %s</string>
|
||||
@@ -42,7 +43,6 @@
|
||||
<string name="settings_ui">Oberfläche</string>
|
||||
<string name="settings_map">Karte</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="copyright_summary">©2020–2023 Johan von Forstner</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="privacy">Datenschutzerklärung</string>
|
||||
<string name="fav_add">Als Favorit speichern</string>
|
||||
@@ -175,6 +175,7 @@
|
||||
<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">Anschluss auswählen</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Nur für Energiekunden</string>
|
||||
<string name="percent_format">%.0f%%</string>
|
||||
@@ -295,4 +296,21 @@
|
||||
<string name="developer_mode_disabled">Entwicklermodus deaktiviert</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Kompass</string>
|
||||
<string name="charger_website">Website</string>
|
||||
<string name="location_status">Standortdienste-Status</string>
|
||||
<string name="pref_tesla_account">Tesla-Account</string>
|
||||
<string name="pref_tesla_account_enabled">Angemeldet als %s</string>
|
||||
<string name="pref_tesla_account_disabled">Anmelden, um Echtzeitdaten für Tesla Supercharger zu sehen. Kein Tesla-Fahrzeug notwendig</string>
|
||||
<string name="logging_in">Anmelden…</string>
|
||||
<string name="log_out">Abmelden</string>
|
||||
<string name="logged_out">Abgemeldet</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_error">Login fehlgeschlagen</string>
|
||||
<string name="tesla_pricing_owners">Nur Tesla-Fahrzeuge:</string>
|
||||
<string name="tesla_pricing_members">Tesla-Fahrzeuge & Mitglieder:</string>
|
||||
<string name="tesla_pricing_others">Andere Kunden:</string>
|
||||
<string name="pricing_up_to">bis zu %s</string>
|
||||
<string name="tesla_pricing_other_times">Andere Zeiten:</string>
|
||||
<string name="tesla_pricing_blocking_fee">Blockiergebühr: %s</string>
|
||||
<string name="average_utilization">Durchschnittliche Auslastung</string>
|
||||
</resources>
|
||||
@@ -81,7 +81,7 @@
|
||||
<string name="verified">vérifié</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d mode de paiement compatible</item>
|
||||
<item quantity="many">%d modes de paiement compatibles</item>
|
||||
<item quantity="many">%d de modes de paiement compatibles</item>
|
||||
<item quantity="other">%d modes de paiement compatibles</item>
|
||||
</plurals>
|
||||
<string name="verified_desc">Le fonctionnement du chargeur a été confirmé au moins une fois par un membre de la communauté %s</string>
|
||||
@@ -104,7 +104,7 @@
|
||||
<string name="pref_data_source">Source des données</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d tarif sélectionné</item>
|
||||
<item quantity="many">%d tarifs sélectionnés</item>
|
||||
<item quantity="many">%d de tarifs sélectionnés</item>
|
||||
<item quantity="other">%d tarifs sélectionnés</item>
|
||||
</plurals>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
@@ -154,7 +154,6 @@
|
||||
<string name="fault_report_date">Rapport d\'anomalie (dernière mise à jour : %s)</string>
|
||||
<string name="menu_report_new_charger">Nouveau chargeur</string>
|
||||
<string name="filter_connectors">Connecteurs</string>
|
||||
<string name="copyright_summary">©2020–2023 Johan von Forstner</string>
|
||||
<string name="other">Autre</string>
|
||||
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance l’application de cartes à l’emplacement du chargeur</string>
|
||||
<string name="settings_map">Carte</string>
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
<string name="realtime_data_unavailable">Sanntidsstatus utilgjengelig</string>
|
||||
<string name="other">Andre</string>
|
||||
<string name="cost_detail"><b>Lading:</b> %1$s · <b>Parkering:</b> %2$s</string>
|
||||
<string name="copyright_summary">©2020–2023 Johan von Forstner</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter ruteveiledning på Google Maps</string>
|
||||
<string name="filter_free_parking">Kun ladere med gratis parkering</string>
|
||||
<string name="filter_min_power">Min. effekt</string>
|
||||
@@ -296,4 +295,6 @@
|
||||
<string name="developer_options">Utvikleralternativer</string>
|
||||
<string name="data_source_switched_to">Datakilde byttet til %s</string>
|
||||
<string name="developer_mode_enabled">Utviklermodus påslått</string>
|
||||
<string name="menu_reset">Tilbakestill filterinnstillinger</string>
|
||||
<string name="charger_website">Nettside</string>
|
||||
</resources>
|
||||
@@ -53,7 +53,6 @@
|
||||
<string name="settings_ui">Interface</string>
|
||||
<string name="settings_map">Kaart</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="copyright_summary">©2020–2023 Johan von Forstner</string>
|
||||
<string name="other">Andere</string>
|
||||
<string name="privacy">Privacy</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigatieknop opent de kaart app met de locatie van het laadstation</string>
|
||||
|
||||
303
app/src/main/res/values-pt/strings.xml
Normal file
303
app/src/main/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,303 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="menu_reset">Repor filtros</string>
|
||||
<string name="filter_custom">Filtro modificado</string>
|
||||
<string name="filter_favorites">Favoritos</string>
|
||||
<string name="reorder">reordenar</string>
|
||||
<string name="delete">Apagar</string>
|
||||
<string name="welcome_2_detail">Também pode encontrar esta informação em \"Sobre\" → \"Perguntas frequentes\"</string>
|
||||
<string name="chargeprice_donation_dialog_title">Você é um verdadeiro caçador de pechinchas!</string>
|
||||
<string name="donation_dialog_title">Obrigado por usar o EVMap</string>
|
||||
<string name="deleted_filterprofile">“%s” removido</string>
|
||||
<string name="undo">Refazer</string>
|
||||
<string name="rename">Renomear</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Você faz grande uso da comparação de preços. Ajude a cobrir os custos de acesso à informação apoiando o EVMap com uma doação.</string>
|
||||
<string name="verified">verificado</string>
|
||||
<string name="chargeprice_select_connector">Escolhe o conector</string>
|
||||
<string name="verified_desc">O carregador foi marcado como funcional por um membro da comunidade %s</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
|
||||
<string name="percent_format">%.0f%%</string>
|
||||
<string name="pref_my_vehicle">Os meus veículos</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">As empresas de serviços públicos às vezes oferecem planos especiais para os seus clientes</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d plano selecionado</item>
|
||||
<item quantity="many">%d planos selecionados</item>
|
||||
<item quantity="other">%d de planos selecionados</item>
|
||||
</plurals>
|
||||
<string name="data_sources_description">Escolha uma fonte para as estações de carregamento. Pode alterar mais tarde nas definições da app.</string>
|
||||
<string name="about_contributors_text">Obrigado a todos os contribuidores de código e traduções para o EVMap:</string>
|
||||
<string name="utilization_prediction">Previsão de utilização</string>
|
||||
<string name="prediction_time_colon">%s:</string>
|
||||
<string name="pref_applink_associate_summary">de goingelectric.de e openchargemap.org</string>
|
||||
<string name="none">nenhum</string>
|
||||
<string name="donate">Doar</string>
|
||||
<string name="show_less">menos…</string>
|
||||
<string name="all">todos</string>
|
||||
<string name="show_more">mais…</string>
|
||||
<string name="filters_deactivated">Filtros desativados</string>
|
||||
<string name="favorites_empty_state">Carregadores guardados aparecem aqui</string>
|
||||
<string name="donation_successful">Obrigado ❤️</string>
|
||||
<string name="category_car_dealership">Stand de carros</string>
|
||||
<string name="map_traffic">Trânsito</string>
|
||||
<string name="faq">Perguntas frequentes</string>
|
||||
<string name="menu_filters_active">Filtros ativos</string>
|
||||
<string name="menu_edit_filters">Editar filtros</string>
|
||||
<string name="filters_activated">Filtros ativados</string>
|
||||
<string name="menu_manage_filter_profiles">Gerir perfis de filtros</string>
|
||||
<string name="go_to_chargeprice">Comparar preços</string>
|
||||
<string name="filter_operators">Operadores</string>
|
||||
<string name="location_error">Localização não encontrada. Verifique se a app tem permissão para usar aceder à sua localização</string>
|
||||
<string name="filter_networks">Redes</string>
|
||||
<string name="fault_report">Com problemas</string>
|
||||
<string name="number_selected">%d selecionados</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="filter_barrierfree">Não necessita de registo</string>
|
||||
<string name="fault_report_date">Com problemas (atualizado: %s)</string>
|
||||
<string name="filter_chargecards">Formas de pagamento</string>
|
||||
<string name="pref_language">Língua da app</string>
|
||||
<string name="all_selected">Todos selecionados</string>
|
||||
<string name="edit">editar</string>
|
||||
<string name="pref_darkmode">Modo escuro</string>
|
||||
<string name="connection_error">Não foi possível carregar a lista de carregadores</string>
|
||||
<string name="retry">Tentar novamente</string>
|
||||
<string name="filter_open_247">Disponível 24/7</string>
|
||||
<string name="filter_exclude_faults">Excluir carregadores com relatos de falhas</string>
|
||||
<string name="charge_cards">Formas de pagamento</string>
|
||||
<string name="and_n_others">e %d outros</string>
|
||||
<string name="goingelectric_forum">Tópico no fórum GoingElectric.de</string>
|
||||
<string name="contact">Contato</string>
|
||||
<string name="menu_report_new_charger">Novo carregador</string>
|
||||
<string name="category_holiday_home">Casa de férias</string>
|
||||
<string name="pref_map_provider">Provedor do mapa</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="category_public_authorities">Autoridades públicas</string>
|
||||
<string name="category_private_charger">Carregador privado</string>
|
||||
<string name="category_rest_area">Área de descanso</string>
|
||||
<string name="edit_at_datasource">editado em %s</string>
|
||||
<string name="categories">Categorias</string>
|
||||
<string name="category_service_on_motorway">Área de serviço (autoestrada)</string>
|
||||
<string name="category_service_off_motorway">Área de serviço (fora da autoestrada)</string>
|
||||
<string name="category_railway_station">Estação de comboio</string>
|
||||
<string name="category_shopping_mall">Centro comercial</string>
|
||||
<string name="category_amusement_park">Parque de diversões</string>
|
||||
<string name="category_airport">Aeroporto</string>
|
||||
<string name="category_parking_multi">Garagem de estacionamento</string>
|
||||
<string name="category_camping">Parque de campismo</string>
|
||||
<string name="category_cinema">Cinema</string>
|
||||
<string name="category_hotel">Hotel</string>
|
||||
<string name="category_church">Igreja</string>
|
||||
<string name="category_hospital">Hospital</string>
|
||||
<string name="category_museum">Museu</string>
|
||||
<string name="category_parking">Parque de estacionamento</string>
|
||||
<string name="category_restaurant">Restaurante</string>
|
||||
<string name="save_profile_enter_name">Insira o nome do perfil com este filtro:</string>
|
||||
<string name="save_as_profile">Guardar como perfil</string>
|
||||
<string name="filterprofiles_empty_state">Não existem filtros guardados</string>
|
||||
<string name="welcome_2">Cada cor corresponde a potência máxima do carregador</string>
|
||||
<string name="welcome_to_evmap">Bem-vindo ao EVMap</string>
|
||||
<string name="pref_darkmode_always_off">Sempre desligado</string>
|
||||
<string name="welcome_2_title">Escolha a potência</string>
|
||||
<string name="navigate">Navegar</string>
|
||||
<string name="donation_dialog_detail">O EVMap é gratuito e de código aberto. Contribuições de código no GitHub são bem-vindas. Para ajudar a cobrir os custos de operação, por favor considere fazer uma doação ao criador da app.</string>
|
||||
<string name="charging_barrierfree">Não necessita de registo</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d forma de pagamento compatível</item>
|
||||
<item quantity="other">%d formas de pagamento compatíveis</item>
|
||||
<item quantity="many">%d de formas de pagamento compatíveis</item>
|
||||
</plurals>
|
||||
<string name="chargeprice_session_fee">custo da sessão</string>
|
||||
<string name="edit_on_goingelectric_info">Por favor faça o login em GoingElectric.de se esta página estiver vazia</string>
|
||||
<string name="chargeprice_blocking_fee">Taxa de bloqueio %s</string>
|
||||
<string name="chargeprice_per_kwh">por kWh</string>
|
||||
<string name="chargeprice_per_minute">por minuto</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Excluir planos com taxas mensais</string>
|
||||
<string name="chargeprice_no_tariffs_found">O Chargeprice.app não encontrou planos de carregamento para este carregador</string>
|
||||
<string name="chargeprice_min_spend">Gasto mínimo: %2$s%1$.2f/mês</string>
|
||||
<string name="powered_by_chargeprice">informação da Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Taxa base: %2$s%1$.2f/mês</string>
|
||||
<string name="settings_chargeprice">Comparação de preços</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Apenas para clientes com subscrição</string>
|
||||
<string name="chargeprice_battery_range">Carregar de %1$.0f%% até %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Carregar de</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Incluir planos de subscrição</string>
|
||||
<string name="chargeprice_select_car_first">Por favor escolha o modelo do seu carro nas definições primeiro</string>
|
||||
<string name="chargeprice_battery_range_to">até</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, %2$s aprox., ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_connection_error">Não foi possível carregar os preços</string>
|
||||
<string name="chargeprice_vehicle">Veículo</string>
|
||||
<string name="chargeprice_title">Preços</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Não existem conectores compatíveis nesta estação de carregamento</string>
|
||||
<string name="pref_chargeprice_currency">Moeda</string>
|
||||
<string name="got_it">Continuar</string>
|
||||
<string name="chargeprice_price_not_available">Preço não disponível</string>
|
||||
<string name="pref_my_tariffs">Os meus planos de carregamento</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
<item quantity="one" tools:ignore="ImpliedQuantity">(será destacado na comparação de preços)</item>
|
||||
<item quantity="other">(serão destacados na comparação de preços)</item>
|
||||
<item quantity="many">(serão destacados na comparação de preços)</item>
|
||||
</plurals>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="chargeprice_all_tariffs_selected">Todos os planos selecionados</string>
|
||||
<string name="license">Licença</string>
|
||||
<string name="unknown_operator">Operador desconhecido</string>
|
||||
<string name="settings_charger_data">Estações de carregamento</string>
|
||||
<string name="data_source_goingelectric_desc">Boa escolha para países de língua alemã. Descrições em alemão. Mantido pela comunidade.</string>
|
||||
<string name="pref_data_source">Fonte da informação</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="next">próximo</string>
|
||||
<string name="data_source_openchargemap_desc">Mundial, com vários níveis de qualidade. Descrições em inglês ou língua local. Mantido pela comunidade e usa informação governamental publica em alguns países (ex: América do Norte, Reino Unido, França, Noruega, etc).</string>
|
||||
<string name="get_started">Começar</string>
|
||||
<string name="lets_go">Vamos lá</string>
|
||||
<string name="crash_report_text">O EVMap encontrou um problema. Por favor envie um relatório do erro para o criador da app.</string>
|
||||
<string name="crash_report_comment_prompt">Pode adicionar um comentário abaixo:</string>
|
||||
<string name="pref_search_provider">Fornecedor da pesquisa</string>
|
||||
<string name="powered_by_mapbox">via Mapbox</string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Apoie o desenvolvimento do EVMap com uma única doação</string>
|
||||
<string name="pref_map_rotate_gestures_on">Use dois dedos para girar o mapa</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotação desligada (norte sempre para cima)</string>
|
||||
<string name="refresh_live_data">atualizar estado em tempo real</string>
|
||||
<string name="pref_search_provider_info">As pesquisas são caras, especialmente quando o Google Maps é usado. Por favor considere doar através de \"Sobre\" → \"Doar\".</string>
|
||||
<string name="github_sponsors_desc">Apoie o EVMap através do GitHub</string>
|
||||
<string name="unnamed_filter_profile">Filtro sem nome</string>
|
||||
<string name="deleted_recent_search_results">As pesquisas recentes foram eliminadas</string>
|
||||
<string name="help">Ajuda</string>
|
||||
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
|
||||
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
|
||||
<string name="pref_search_delete_recent">Apagar pesquisas recentes</string>
|
||||
<string name="required">obrigatório</string>
|
||||
<string name="settings_data_sources">Fontes de informação</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Rotação do mapa</string>
|
||||
<string name="autocomplete_connection_error">Não foi possível carregar as sugestões</string>
|
||||
<string name="pref_language_device_default">Língua do dispositivo</string>
|
||||
<string name="pref_darkmode_device_default">Padrão do dispositivo</string>
|
||||
<string name="pref_darkmode_always_on">Sempre ligado</string>
|
||||
<string name="pref_chargeprice_currency_chf">Franco suíço (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Coroa checa (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Coroa dinamarquesa (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Libra esterlina (GBP)</string>
|
||||
<string name="chargeprice_header_my_tariffs">Os meus planos</string>
|
||||
<string name="chargeprice_header_other_tariffs">Outros planos</string>
|
||||
<string name="developer_options">Opções de desenvolvedor</string>
|
||||
<string name="prediction_help">A previsão é baseada em fatores como dia da semana, hora do dia e uso anterior para que você evite carregadores superlotados. Sem garantias de estar correta.</string>
|
||||
<string name="disable_developer_mode">Desativar o modo de desenvolvedor</string>
|
||||
<string name="compass">Compasso</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">%1$d/%2$d disponível</item>
|
||||
<item quantity="many">%1$d/%2$d disponíveis</item>
|
||||
<item quantity="other">%1$d/%2$d disponíveis</item>
|
||||
</plurals>
|
||||
<string name="pref_prediction_enabled">Mostrar previsões de utilização</string>
|
||||
<string name="developer_mode_enabled">Modo de desenvolvedor ativado</string>
|
||||
<string name="pref_prediction_enabled_summary">para carregadores suportados
|
||||
\n(atualmente apenas CC/DC na Alemanha)</string>
|
||||
<string name="prediction_only">(apenas %s)</string>
|
||||
<string name="prediction_dc_plugs_only">Conectores CC/DC</string>
|
||||
<string name="pref_applink_associate">Abrir links suportados</string>
|
||||
<string name="data_source_switched_to">Fonte de dados alterada para %s</string>
|
||||
<string name="developer_mode_disabled">Modo de desenvolvedor desativado</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="no_maps_app_found">Instale a app de navegação primeiro</string>
|
||||
<string name="no_browser_app_found">Instale um navegador web primeiro</string>
|
||||
<string name="connectors">Conectores</string>
|
||||
<string name="address">Endereço</string>
|
||||
<string name="operator">Operador</string>
|
||||
<string name="network">Rede</string>
|
||||
<string name="hours">Horário de abertura</string>
|
||||
<string name="open_247"><b>Aberto 24/7</b></string>
|
||||
<string name="closed"><b>Fechado</b></string>
|
||||
<string name="open_closesat"><b>Aberto</b> · Fecha às %s</string>
|
||||
<string name="closed_opensat"><b>Fechado</b> · Abre às %s</string>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="closed_unfmt">Fechado</string>
|
||||
<string name="holiday">Feriado</string>
|
||||
<string name="cost">Custo</string>
|
||||
<string name="cost_detail"><b>Carregamento:</b> %1$s · <b>Parque:</b> %2$s</string>
|
||||
<string name="cost_detail_charging"><b>Carregamento %s</b></string>
|
||||
<string name="cost_detail_parking"><b>Parque %s</b></string>
|
||||
<string name="charging_free">Gratuito</string>
|
||||
<string name="charging_paid">Pago</string>
|
||||
<string name="parking_free">Gratuito</string>
|
||||
<string name="parking_paid">Pago</string>
|
||||
<string name="amenities">Facilidades</string>
|
||||
<string name="general_info">Informação geral</string>
|
||||
<string name="realtime_data_unavailable">Estado em tempo real não disponível</string>
|
||||
<string name="realtime_data_loading">Verificado estado em tempo real…</string>
|
||||
<string name="realtime_data_source">Fonte do estado em tempo real (beta): %s</string>
|
||||
<string name="source">Fonte: %s</string>
|
||||
<string name="search">Pesquisa</string>
|
||||
<string name="menu_map">Mapa</string>
|
||||
<string name="menu_favs">Favoritos</string>
|
||||
<string name="menu_filter">Filtro</string>
|
||||
<string name="not_implemented">ainda não implementado</string>
|
||||
<string name="about">Sobre</string>
|
||||
<string name="version">Versão</string>
|
||||
<string name="github_link_title">Código-fonte</string>
|
||||
<string name="oss_licenses">Licenças</string>
|
||||
<string name="settings_ui">Interface</string>
|
||||
<string name="settings_map">Mapa</string>
|
||||
<string name="copyright">Direitos de autor</string>
|
||||
<string name="other">Outro</string>
|
||||
<string name="privacy">Privacidade</string>
|
||||
<string name="settings">Definições</string>
|
||||
<string name="fav_add">Guardar como favorito</string>
|
||||
<string name="fav_remove">Remover dos favoritos</string>
|
||||
<string name="pref_navigate_use_maps">Navegar agora</string>
|
||||
<string name="pref_navigate_use_maps_on">O botão de navegação inicia a navegação com o Google Maps</string>
|
||||
<string name="pref_navigate_use_maps_off">O botão de navegação abre a app dos mapas com a localização do carregador</string>
|
||||
<string name="coordinates">Coordenadas</string>
|
||||
<string name="share">Partilhar</string>
|
||||
<string name="filter_free">Apenas carregadores gratuitos</string>
|
||||
<string name="filter_min_power">Potência minima</string>
|
||||
<string name="filter_free_parking">Apenas carregadores com parque gratuito</string>
|
||||
<string name="filter_min_connectors">Número mínimo de conectores</string>
|
||||
<string name="filter_connectors">Conectores</string>
|
||||
<string name="plug_type_1">Tipo 1</string>
|
||||
<string name="plug_type_2">Tipo 2</string>
|
||||
<string name="plug_type_3">Tipo 3A</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
<string name="plug_cee_blau">CEE Azul</string>
|
||||
<string name="plug_cee_rot">CEE Vermelho</string>
|
||||
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
|
||||
<string name="plug_supercharger">Supercarregador Tesla</string>
|
||||
<string name="donation_failed">Algo correu mal 😕</string>
|
||||
<string name="map_type_satellite">Satélite</string>
|
||||
<string name="map_type_terrain">Terreno</string>
|
||||
<string name="map_type">Tipo de mapa</string>
|
||||
<string name="map_details">Detalhes do mapa</string>
|
||||
<string name="map_type_normal">Padrão</string>
|
||||
<string name="category_swimming_pool">Piscina</string>
|
||||
<string name="category_supermarket">Supermercado</string>
|
||||
<string name="category_petrol_station">Posto de combustível</string>
|
||||
<string name="category_parking_underground">Parque de estacionamento subterrâneo</string>
|
||||
<string name="category_zoo">Zoo</string>
|
||||
<string name="category_caravan_site">Caravanas</string>
|
||||
<string name="menu_apply">Aplicar filtros</string>
|
||||
<string name="menu_save_profile">Guardar como perfil</string>
|
||||
<string name="no_filters">Sem filtros</string>
|
||||
<string name="welcome_1">Encontre carregadores elétricos perto de si</string>
|
||||
<string name="close">Fechar</string>
|
||||
<string name="edit_filter_profile">Editar “%s”</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Cuna croata (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Florim húngaro (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Coroa islandesa (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Coroa norueguesa (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Złoty polaco (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Coroa sueca (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Dólar americano (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Contribuidores</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Permitir carga não balanceada</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Permitir carregamento CA/AC monofásico (1 fase) com mais de 4.5 kW</string>
|
||||
<string name="charger_website">Website</string>
|
||||
</resources>
|
||||
301
app/src/main/res/values-ro/strings.xml
Normal file
301
app/src/main/res/values-ro/strings.xml
Normal file
@@ -0,0 +1,301 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Conectori</string>
|
||||
<string name="no_maps_app_found">Instalati o aplicatie de navigatie</string>
|
||||
<string name="no_browser_app_found">Instalati un browser web</string>
|
||||
<string name="address">Adresa</string>
|
||||
<string name="operator">Operator</string>
|
||||
<string name="network">Retea</string>
|
||||
<string name="hours">Program</string>
|
||||
<string name="open_247"><![CDATA[<b>Deschis nonstop</b>]]></string>
|
||||
<string name="closed"><![CDATA[<b>Inchis</b>]]></string>
|
||||
<string name="open_closesat"><![CDATA[<b>Deschis</b> · Inchide la %s]]></string>
|
||||
<string name="closed_opensat"><![CDATA[<b>Inchis</b> · Deschide la %s]]></string>
|
||||
<string name="closed_unfmt">Inchis</string>
|
||||
<string name="holiday">Sarbatoare</string>
|
||||
<string name="cost">Cost</string>
|
||||
<string name="cost_detail"><![CDATA[<b>Incarcare:</b> %1$s · <b>Parcare:</b> %2$s]]></string>
|
||||
<string name="cost_detail_charging"><![CDATA[<b>%s incarcare</b>]]></string>
|
||||
<string name="cost_detail_parking"><![CDATA[<b>%s parcare</b>]]></string>
|
||||
<string name="charging_free">Gratuit</string>
|
||||
<string name="charging_paid">Cu plata</string>
|
||||
<string name="parking_free">Gratuit</string>
|
||||
<string name="parking_paid">Cu plata</string>
|
||||
<string name="amenities">Facilitati</string>
|
||||
<string name="general_info">Informatii generale</string>
|
||||
<string name="realtime_data_unavailable">Stare in tip real indisponibila</string>
|
||||
<string name="realtime_data_loading">Verificare stare in timp real…</string>
|
||||
<string name="realtime_data_source">Sursa verificare in timp real (beta): %s</string>
|
||||
<string name="source">Sursa: %s</string>
|
||||
<string name="search">Cautare</string>
|
||||
<string name="menu_map">Harta</string>
|
||||
<string name="menu_favs">Favorite</string>
|
||||
<string name="menu_filter">Fitru</string>
|
||||
<string name="not_implemented">indisponibil momentan</string>
|
||||
<string name="about">Despre </string>
|
||||
<string name="version">Versiune</string>
|
||||
<string name="github_link_title">Cod sursa</string>
|
||||
<string name="oss_licenses">Licente</string>
|
||||
<string name="settings">Setari</string>
|
||||
<string name="settings_ui">Interfata</string>
|
||||
<string name="settings_map">Harta</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="other">Altele</string>
|
||||
<string name="privacy">Confidentialitate</string>
|
||||
<string name="fav_add">Salveaza ca favorit</string>
|
||||
<string name="fav_remove">Sterge din favorite</string>
|
||||
<string name="pref_navigate_use_maps">Indicatii navigare</string>
|
||||
<string name="pref_navigate_use_maps_on">Butonul de navigare porneste cu Google Maps</string>
|
||||
<string name="pref_navigate_use_maps_off">Butonul de navigare deschide aplicatia de harti cu locatia statiei</string>
|
||||
<string name="coordinates">Coordonate</string>
|
||||
<string name="share">Distribuie</string>
|
||||
<string name="filter_free">Doar statii gratuite</string>
|
||||
<string name="filter_min_power">Putere minima</string>
|
||||
<string name="filter_free_parking">Doar statii cu parcare gratuita</string>
|
||||
<string name="filter_min_connectors">Numar minim de conectori</string>
|
||||
<string name="filter_connectors">Conectori</string>
|
||||
<string name="plug_type_1">Type 1</string>
|
||||
<string name="plug_type_2">Type 2</string>
|
||||
<string name="plug_type_3">Type 3A</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 Blue</string>
|
||||
<string name="plug_cee_rot">CEE Red</string>
|
||||
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
|
||||
<string name="all">toate</string>
|
||||
<string name="none">niciunul</string>
|
||||
<string name="show_more">mai mult…</string>
|
||||
<string name="show_less">mai putin…</string>
|
||||
<string name="favorites_empty_state">Statiile salvate apar aici</string>
|
||||
<string name="donate">Doneaza</string>
|
||||
<string name="donation_successful">Multumesc ❤️</string>
|
||||
<string name="donation_failed">A aparut o eroare 😕</string>
|
||||
<string name="map_type_normal">Implicit</string>
|
||||
<string name="map_type_satellite">Satelit</string>
|
||||
<string name="map_type_terrain">Teren</string>
|
||||
<string name="map_type">Tip harta</string>
|
||||
<string name="map_details">Detalii harta</string>
|
||||
<string name="map_traffic">Trafic</string>
|
||||
<string name="faq">Intrebari frecvente</string>
|
||||
<string name="menu_filters_active">Filtre active</string>
|
||||
<string name="filters_activated">Filtre activate</string>
|
||||
<string name="filters_deactivated">Filtre dezactivate</string>
|
||||
<string name="menu_edit_filters">Modificare filtre</string>
|
||||
<string name="menu_manage_filter_profiles">Modifica profile filtre</string>
|
||||
<string name="go_to_chargeprice">Compara preturi</string>
|
||||
<string name="fault_report">Raport defectiune</string>
|
||||
<string name="fault_report_date">raport defectiune (ultima actualizare: %s)</string>
|
||||
<string name="filter_networks">Retele</string>
|
||||
<string name="filter_operators">Operatori</string>
|
||||
<string name="filter_chargecards">Metode de plata</string>
|
||||
<string name="all_selected">Selectate toate</string>
|
||||
<string name="number_selected">%d selectate</string>
|
||||
<string name="edit">modifica</string>
|
||||
<string name="cancel">Anulare</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Limba aplicatie</string>
|
||||
<string name="pref_darkmode">Mod intunecat</string>
|
||||
<string name="connection_error">Eroare conexiune</string>
|
||||
<string name="location_error">Locatia nu a putut fi detectata. Verificati setarile</string>
|
||||
<string name="retry">Reincearca</string>
|
||||
<string name="filter_open_247">Disponibile nonstop</string>
|
||||
<string name="filter_barrierfree">Disponibile fara inregistrare</string>
|
||||
<string name="filter_exclude_faults">Exclude statiile raportate defecte</string>
|
||||
<string name="charge_cards">Metode de plata</string>
|
||||
<string name="and_n_others">si %d altele</string>
|
||||
<string name="pref_map_provider">Furnizor harta</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="goingelectric_forum">Forum conversatii pe GoingElectric.de</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="menu_report_new_charger">Statie noua</string>
|
||||
<string name="edit_at_datasource">modificat la %s</string>
|
||||
<string name="categories">Categorii</string>
|
||||
<string name="category_car_dealership">Reprezentanta auto</string>
|
||||
<string name="category_service_on_motorway">Zona servicii (autostrada)</string>
|
||||
<string name="category_service_off_motorway">Zona servicii (in afara autostrazii)</string>
|
||||
<string name="category_railway_station">Statie tren</string>
|
||||
<string name="category_public_authorities">Autoritati locale</string>
|
||||
<string name="category_camping">Camping</string>
|
||||
<string name="category_shopping_mall">Mall</string>
|
||||
<string name="category_holiday_home">Casa de vacanta</string>
|
||||
<string name="category_airport">Aerport</string>
|
||||
<string name="category_amusement_park">Parc de distractii</string>
|
||||
<string name="category_hotel">Hotel</string>
|
||||
<string name="category_cinema">Cinema</string>
|
||||
<string name="category_church">Biserica</string>
|
||||
<string name="category_hospital">Spital</string>
|
||||
<string name="category_museum">Museu</string>
|
||||
<string name="category_parking_multi">Parcare etajata</string>
|
||||
<string name="category_parking">Parcare</string>
|
||||
<string name="category_private_charger">Statie de incarcare privata</string>
|
||||
<string name="category_rest_area">Zona de odihna</string>
|
||||
<string name="category_restaurant">Restaurant</string>
|
||||
<string name="category_swimming_pool">Piscina</string>
|
||||
<string name="category_supermarket">Supermarket</string>
|
||||
<string name="category_petrol_station">Benzinarie</string>
|
||||
<string name="category_parking_underground">Parcare subterana</string>
|
||||
<string name="category_zoo">Gradina Zoo</string>
|
||||
<string name="category_caravan_site">Camping rulote</string>
|
||||
<string name="menu_apply">Aplica filtre</string>
|
||||
<string name="menu_save_profile">Salveaza profil</string>
|
||||
<string name="menu_reset">Sterge setari filtre</string>
|
||||
<string name="no_filters">Fara filtre</string>
|
||||
<string name="filter_custom">Filtre personalizate</string>
|
||||
<string name="filter_favorites">Favorite</string>
|
||||
<string name="reorder">reordonare</string>
|
||||
<string name="delete">Sterge</string>
|
||||
<string name="save_as_profile">Salveaza ca profil</string>
|
||||
<string name="save_profile_enter_name">Completati nume profil:</string>
|
||||
<string name="filterprofiles_empty_state">Nu sunt profile salvate</string>
|
||||
<string name="welcome_to_evmap">Bine ati venit la EVMap</string>
|
||||
<string name="welcome_1">Cauta statii de incarcare in apropiere</string>
|
||||
<string name="welcome_2_title">Esti mereu la curent</string>
|
||||
<string name="welcome_2">Fiecare culoare a statiei corespunde puterii maxime de incarcare</string>
|
||||
<string name="welcome_2_detail">Puteti gasi si in sectiunea “Despre” → “Intrebari frecvente”</string>
|
||||
<string name="donation_dialog_title">Multumim ca utilizati EVMap</string>
|
||||
<string name="donation_dialog_detail">EVMap este libera si gratuita. Contributiile pe GitHub sunt apreciate. Pentru a acoperi costurile pentru acces la date, va rugam sa donati orice suma pentru dezvoltator.</string>
|
||||
<string name="chargeprice_donation_dialog_title">Stii sa cauti ofertele cele mai bune!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Stii sa folosesti optiunea de comparare preturi. Puteti ajuta pentru a acoperi costurile pentru accesul la aceste date donand pentru EVMap.</string>
|
||||
<string name="deleted_filterprofile">“%s” a fost sters</string>
|
||||
<string name="undo">Anuleaza</string>
|
||||
<string name="rename">Redenumeste</string>
|
||||
<string name="charging_barrierfree">Utilizabile fara inregistrare</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d metoda de plata compatibila</item>
|
||||
<item quantity="other">%d metode de plata compatibile</item>
|
||||
<item quantity="few">%d metode de plata compatibile</item>
|
||||
</plurals>
|
||||
<string name="navigate">Navigare</string>
|
||||
<string name="verified">verificat</string>
|
||||
<string name="verified_desc">Statia de incarcare a fost confirmata functionala de un mebru din comunitatea %s</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
|
||||
<string name="chargeprice_select_connector">Alege conector</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Doar pentru clienti</string>
|
||||
<string name="edit_on_goingelectric_info">Va rugam autentificati-va la GoingElectric.de daca pagina e goala</string>
|
||||
<string name="percent_format">%.0f%%</string>
|
||||
<string name="chargeprice_session_fee">taxa sesiune</string>
|
||||
<string name="chargeprice_per_kwh">pe kWh</string>
|
||||
<string name="chargeprice_per_minute">pe min</string>
|
||||
<string name="chargeprice_blocking_fee">Taxa blocare >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Fara pret pentru aceasta statie in Chargeprice.app</string>
|
||||
<string name="powered_by_chargeprice">oferit de Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Pret de baza: %2$s%1$.2f/luna</string>
|
||||
<string name="chargeprice_min_spend">Suma minima: %2$s%1$.2f/luna</string>
|
||||
<string name="settings_chargeprice">Comparatie preturi</string>
|
||||
<string name="pref_my_vehicle">Masinile mele</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Exclude abonamente cu plata lunara</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Include abonament clienti</string>
|
||||
<string name="chargeprice_select_car_first">Configurati modelul masinii in setari</string>
|
||||
<string name="chargeprice_battery_range">Incarcare de la %1$.0f%% la %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Incarcare de la</string>
|
||||
<string name="chargeprice_battery_range_to">la</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, aprox. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_vehicle">Masina</string>
|
||||
<string name="chargeprice_price_not_available">Pret indisponibil</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Furnizorii de utilitati ofera uneori abonamente speciale pentru clientii lor</string>
|
||||
<string name="close">Inchidere</string>
|
||||
<string name="chargeprice_title">Preturi</string>
|
||||
<string name="chargeprice_connection_error">Preturi nu au putut fi incarcate</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Nu sunt conectori compatibili la aceasta statie</string>
|
||||
<string name="pref_chargeprice_currency">Moneda</string>
|
||||
<string name="pref_my_tariffs">Abonamentele mele</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
<item quantity="one">(va fi evidentiat in comparatia preturilor)</item>
|
||||
<item quantity="other">(vor fi evidentiate in comparatia preturilor)</item>
|
||||
<item quantity="few">(vor fi evidentiate in comparatia preturilor)</item>
|
||||
</plurals>
|
||||
<string name="chargeprice_all_tariffs_selected">toate abonamentele selectate</string>
|
||||
<string name="license">Licenta</string>
|
||||
<string name="settings_charger_data">Statii de incarcare</string>
|
||||
<string name="pref_data_source">Sursa date</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d abonament selectat</item>
|
||||
<item quantity="other">%d abonamente selectate</item>
|
||||
<item quantity="few">%d abonamente selectate</item>
|
||||
</plurals>
|
||||
<string name="unknown_operator">Operator necunoscut</string>
|
||||
<string name="data_sources_description">Alegeti o sursa pentru statiile de incarcare. Puteti modifica ulterior in setarile aplicatiei.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Recomandat in tarile vorbitaore de limba germana. Descrieri in limba germana. Actualizat de comunitate.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[International, calitate variata. Descrieri in engleza sau in limba locala. Actualizat de comunitate si de autoritati in unele tari (ex. America de Nord, UK, Franta, Norvegia).]]></string>
|
||||
<string name="next">urmatorul</string>
|
||||
<string name="get_started">Incepe</string>
|
||||
<string name="got_it">Am inteles</string>
|
||||
<string name="lets_go">Sa incepem</string>
|
||||
<string name="crash_report_text">Eroare EVMap. Trimiteti raportul de eroare la dezvoltator.</string>
|
||||
<string name="crash_report_comment_prompt">Puteti adauga un comentariu aici:</string>
|
||||
<string name="powered_by_mapbox">furnizat de Mapbox</string>
|
||||
<string name="pref_search_provider">Furnizor cautare</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Datele de cautare sunt costisitoare, in special de la Google Maps. Va rugam sa luati in considerare o donatie in sectiunea “Despre” → “Doneaza”.]]></string>
|
||||
<string name="github_sponsors">Sponsori GitHub</string>
|
||||
<string name="donate_desc">Sprijina dezvoltarea EVMap\'s cu o donatie</string>
|
||||
<string name="github_sponsors_desc">Sprijina EVMap pe GitHub</string>
|
||||
<string name="unnamed_filter_profile">Profile filtre fara nume</string>
|
||||
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
|
||||
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
|
||||
<string name="required">obligatoriu</string>
|
||||
<string name="edit_filter_profile">Modifica “%s”</string>
|
||||
<string name="pref_search_delete_recent">Sterge rezultate cautare recenta</string>
|
||||
<string name="deleted_recent_search_results">Rezultate cautare recenta au fost sterse</string>
|
||||
<string name="settings_data_sources">Surse date</string>
|
||||
<string name="help">Ajutor</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Include incarcare nebalansata</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Include incarcare monofazata AC cu mai mult de 4.5 kW</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Rotire harta</string>
|
||||
<string name="pref_map_rotate_gestures_on">Foloseste doua degete pentru a roti harta</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotire dezactivata (nordul mereu sus)</string>
|
||||
<string name="refresh_live_data">actualizare stare in timp real</string>
|
||||
<string name="autocomplete_connection_error">Sugestiile nu au putut fi incarcate</string>
|
||||
<string name="pref_language_device_default">Implicit dispozitiv</string>
|
||||
<string name="pref_darkmode_device_default">Implicit dispozitiv</string>
|
||||
<string name="pref_darkmode_always_on">permanent</string>
|
||||
<string name="pref_darkmode_always_off">dezactivat</string>
|
||||
<string name="pref_chargeprice_currency_chf">Franci elvetieni (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Coroane cehe (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Coroane daneze (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Lire sterline (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Croatian kuna (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Forinti maghiari (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Coroane islandeze (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Coroane norvegiene (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Zloti polonezi (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Coroane suedeze (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Dolari americani (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Contribuitori</string>
|
||||
<string name="about_contributors_text">Multumiri contribuitorilor pentru cod si traduceri pentru EVMap:</string>
|
||||
<string name="utilization_prediction">Estimare utilizare</string>
|
||||
<string name="prediction_help">Estimarea se bazeaza pe diversi factori cum ar fi ziua saptamanii, ora, utilizarile anterioare, astfel incat sa se evite supra aglomerarea. Fara garantie.</string>
|
||||
<string name="prediction_time_colon">%s:</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">%1$d/%2$d disponibil</item>
|
||||
<item quantity="other">%1$d/%2$d disponibile</item>
|
||||
<item quantity="few">%1$d/%2$d disponibile</item>
|
||||
</plurals>
|
||||
<string name="pref_prediction_enabled">Arata estimare utilizare</string>
|
||||
<string name="pref_prediction_enabled_summary">pentru statiile de incarcare suportate\n(momentan doar DC in Germania)</string>
|
||||
<string name="prediction_only">(doar %s)</string>
|
||||
<string name="prediction_dc_plugs_only">prize DC</string>
|
||||
<string name="data_source_switched_to">Sursa date schimbat la %s</string>
|
||||
<string name="pref_applink_associate">Linkuri suportate</string>
|
||||
<string name="pref_applink_associate_summary">de la goingelectric.de si openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">Abonamentele mele</string>
|
||||
<string name="chargeprice_header_other_tariffs">Alte abonamente</string>
|
||||
<string name="developer_mode_enabled">Activat mod dezvoltator</string>
|
||||
<string name="developer_options">Optiuni dezvoltator</string>
|
||||
<string name="disable_developer_mode">Dezactivare mod dezvoltator</string>
|
||||
<string name="developer_mode_disabled">Mod dezvoltator dezactivat</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Busola</string>
|
||||
</resources>
|
||||
@@ -7,6 +7,8 @@
|
||||
<item>@string/pref_language_fr</item>
|
||||
<item>@string/pref_language_nb_rNO</item>
|
||||
<item>@string/pref_language_nl</item>
|
||||
<item>@string/pref_language_pt</item>
|
||||
<item>@string/pref_language_ro</item>
|
||||
</string-array>
|
||||
<string-array name="pref_language_values" translatable="false">
|
||||
<item>default</item>
|
||||
@@ -15,6 +17,8 @@
|
||||
<item>fr</item>
|
||||
<item>nb-NO</item>
|
||||
<item>nl</item>
|
||||
<item>pt</item>
|
||||
<item>ro</item>
|
||||
</string-array>
|
||||
<string-array name="pref_darkmode_names">
|
||||
<item>@string/pref_darkmode_device_default</item>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<color name="charger_low">#607d8b</color>
|
||||
<color name="available">#4caf50</color>
|
||||
<color name="charging">#00bcd4</color>
|
||||
<color name="some_available">#ffc107</color>
|
||||
<color name="unavailable">#f44336</color>
|
||||
<color name="unknown">#9e9e9e</color>
|
||||
<color name="status_bar_scrim">#C3000000</color>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<string name="pref_language_fr">Français</string>
|
||||
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
|
||||
<string name="pref_language_nl">Nederlands</string>
|
||||
<string name="pref_language_pt">Português</string>
|
||||
<string name="pref_language_ro">Romana</string>
|
||||
<string name="about_contributors_list">
|
||||
Danilo Bargen\n
|
||||
Altonss\n
|
||||
@@ -21,9 +23,12 @@
|
||||
Maximilian Goldschmidt\n
|
||||
Wim Lamotte\n
|
||||
Licaon_Kter\n
|
||||
Celso Azevedo\n
|
||||
pt2121\n
|
||||
nautilusx
|
||||
nautilusx\n
|
||||
Bobby Galati
|
||||
</string>
|
||||
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
|
||||
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
|
||||
<string name="copyright_summary">©2020–2023 Johan von Forstner and contributors</string>
|
||||
</resources>
|
||||
@@ -26,6 +26,7 @@
|
||||
<string name="amenities">Amenities</string>
|
||||
<string name="general_info">General info</string>
|
||||
<string name="realtime_data_unavailable">Real-time status unavailable</string>
|
||||
<string name="realtime_data_login_needed">Tesla account needed for real-time data</string>
|
||||
<string name="realtime_data_loading">Checking real-time status…</string>
|
||||
<string name="realtime_data_source">Real-time status source (beta): %s</string>
|
||||
<string name="source">Source: %s</string>
|
||||
@@ -42,7 +43,6 @@
|
||||
<string name="settings_ui">Interface</string>
|
||||
<string name="settings_map">Map</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="copyright_summary">©2020–2023 Johan von Forstner</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="privacy">Privacy</string>
|
||||
<string name="fav_add">Save as favorite</string>
|
||||
@@ -175,6 +175,7 @@
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_minute_format">%2$s%1$.2f/min</string>
|
||||
<string name="chargeprice_select_connector">Choose connector</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Only for tie-in customers</string>
|
||||
<string name="edit_on_goingelectric_info">Please log in at GoingElectric.de if this page is empty</string>
|
||||
@@ -295,4 +296,21 @@
|
||||
<string name="developer_mode_disabled">Developer mode disabled</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Compass</string>
|
||||
<string name="charger_website">Website</string>
|
||||
<string name="location_status">Location provider status</string>
|
||||
<string name="pref_tesla_account">Tesla account</string>
|
||||
<string name="pref_tesla_account_enabled">Logged in as %s</string>
|
||||
<string name="pref_tesla_account_disabled">Log in to see real-time data for Tesla Superchargers. No Tesla vehicle necessary</string>
|
||||
<string name="logging_in">Logging in…</string>
|
||||
<string name="log_out">Log out</string>
|
||||
<string name="logged_out">Logged out</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_error">Login failed</string>
|
||||
<string name="tesla_pricing_owners">Tesla vehicles only:</string>
|
||||
<string name="tesla_pricing_members">Tesla vehicles & members:</string>
|
||||
<string name="tesla_pricing_others">Other customers:</string>
|
||||
<string name="pricing_up_to">up to %s</string>
|
||||
<string name="tesla_pricing_other_times">Other times:</string>
|
||||
<string name="tesla_pricing_blocking_fee">Blocking fee: %s</string>
|
||||
<string name="average_utilization">Average Utilization</string>
|
||||
</resources>
|
||||
12
app/src/main/res/xml/backup_rules.xml
Normal file
12
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
<exclude
|
||||
domain="sharedpref"
|
||||
path="encrypted_prefs.xml" />
|
||||
<include
|
||||
domain="database"
|
||||
path="evmap.db" />
|
||||
</full-backup-content>
|
||||
25
app/src/main/res/xml/backup_rules_api31.xml
Normal file
25
app/src/main/res/xml/backup_rules_api31.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
<exclude
|
||||
domain="sharedpref"
|
||||
path="encrypted_prefs.xml" />
|
||||
<include
|
||||
domain="database"
|
||||
path="evmap.db" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
<exclude
|
||||
domain="sharedpref"
|
||||
path="encrypted_prefs.xml" />
|
||||
<include
|
||||
domain="database"
|
||||
path="evmap.db" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<Preference
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.UiSettingsFragment"
|
||||
android:title="@string/settings_ui"
|
||||
@@ -12,4 +13,9 @@
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
|
||||
android:title="@string/settings_chargeprice"
|
||||
android:icon="@drawable/ic_chargeprice" />
|
||||
<Preference
|
||||
android:key="developer_options"
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
|
||||
android:title="@string/developer_options"
|
||||
android:icon="@drawable/ic_developer" />
|
||||
</PreferenceScreen>
|
||||
@@ -15,6 +15,10 @@
|
||||
android:title="@string/pref_prediction_enabled"
|
||||
android:defaultValue="true"
|
||||
android:summary="@string/pref_prediction_enabled_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="tesla_account"
|
||||
android:title="@string/pref_tesla_account" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/settings_map">
|
||||
|
||||
|
||||
9
app/src/main/res/xml/settings_developer.xml
Normal file
9
app/src/main/res/xml/settings_developer.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<Preference
|
||||
android:key="location_status"
|
||||
android:title="@string/location_status" />
|
||||
<Preference
|
||||
android:key="disable_developer_mode"
|
||||
android:title="@string/disable_developer_mode" />
|
||||
</PreferenceScreen>
|
||||
10
app/src/release/java/net/vonforst/evmap/DebugInits.kt
Normal file
10
app/src/release/java/net/vonforst/evmap/DebugInits.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
fun addDebugInterceptors(context: Context) {
|
||||
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder = this
|
||||
102
app/src/test/java/net/vonforst/evmap/FakeAndroidKeyStore.kt
Normal file
102
app/src/test/java/net/vonforst/evmap/FakeAndroidKeyStore.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.Key
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreSpi
|
||||
import java.security.Provider
|
||||
import java.security.SecureRandom
|
||||
import java.security.Security
|
||||
import java.security.cert.Certificate
|
||||
import java.security.spec.AlgorithmParameterSpec
|
||||
import java.util.Date
|
||||
import java.util.Enumeration
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.KeyGeneratorSpi
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
|
||||
/**
|
||||
* https://proandroiddev.com/testing-jetpack-security-with-robolectric-9f9cf2aa4f61
|
||||
*
|
||||
* To use, add to your test:
|
||||
*
|
||||
* companion object {
|
||||
* @JvmStatic
|
||||
* @BeforeClass
|
||||
* fun beforeClass() {
|
||||
* FakeAndroidKeyStore.setup
|
||||
* }
|
||||
* }
|
||||
* */
|
||||
object FakeAndroidKeyStore {
|
||||
|
||||
val setup by lazy {
|
||||
Security.addProvider(object : Provider("AndroidKeyStore", 1.0, "") {
|
||||
init {
|
||||
put("KeyStore.AndroidKeyStore", FakeKeyStore::class.java.name)
|
||||
put("KeyGenerator.AES", FakeAesKeyGenerator::class.java.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
class FakeKeyStore : KeyStoreSpi() {
|
||||
private val wrapped = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
|
||||
override fun engineIsKeyEntry(alias: String?): Boolean = wrapped.isKeyEntry(alias)
|
||||
override fun engineIsCertificateEntry(alias: String?): Boolean =
|
||||
wrapped.isCertificateEntry(alias)
|
||||
|
||||
override fun engineGetCertificate(alias: String?): Certificate =
|
||||
wrapped.getCertificate(alias)
|
||||
|
||||
override fun engineGetCreationDate(alias: String?): Date = wrapped.getCreationDate(alias)
|
||||
override fun engineDeleteEntry(alias: String?) = wrapped.deleteEntry(alias)
|
||||
override fun engineSetKeyEntry(
|
||||
alias: String?,
|
||||
key: Key?,
|
||||
password: CharArray?,
|
||||
chain: Array<out Certificate>?
|
||||
) =
|
||||
wrapped.setKeyEntry(alias, key, password, chain)
|
||||
|
||||
override fun engineSetKeyEntry(
|
||||
alias: String?,
|
||||
key: ByteArray?,
|
||||
chain: Array<out Certificate>?
|
||||
) = wrapped.setKeyEntry(alias, key, chain)
|
||||
|
||||
override fun engineStore(stream: OutputStream?, password: CharArray?) =
|
||||
wrapped.store(stream, password)
|
||||
|
||||
override fun engineSize(): Int = wrapped.size()
|
||||
override fun engineAliases(): Enumeration<String> = wrapped.aliases()
|
||||
override fun engineContainsAlias(alias: String?): Boolean = wrapped.containsAlias(alias)
|
||||
override fun engineLoad(stream: InputStream?, password: CharArray?) =
|
||||
wrapped.load(stream, password)
|
||||
|
||||
override fun engineGetCertificateChain(alias: String?): Array<Certificate> =
|
||||
wrapped.getCertificateChain(alias)
|
||||
|
||||
override fun engineSetCertificateEntry(alias: String?, cert: Certificate?) =
|
||||
wrapped.setCertificateEntry(alias, cert)
|
||||
|
||||
override fun engineGetCertificateAlias(cert: Certificate?): String =
|
||||
wrapped.getCertificateAlias(cert)
|
||||
|
||||
override fun engineGetKey(alias: String?, password: CharArray?): Key? =
|
||||
wrapped.getKey(alias, password)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
class FakeAesKeyGenerator : KeyGeneratorSpi() {
|
||||
private val wrapped = KeyGenerator.getInstance("AES")
|
||||
|
||||
override fun engineInit(random: SecureRandom?) = Unit
|
||||
override fun engineInit(params: AlgorithmParameterSpec?, random: SecureRandom?) = Unit
|
||||
override fun engineInit(keysize: Int, random: SecureRandom?) = Unit
|
||||
override fun engineGenerateKey(): SecretKey = wrapped.generateKey()
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ class NewMotionAvailabilityDetectorTest {
|
||||
"nm/markers" -> {
|
||||
val urlTail = segments.subList(2, segments.size).joinToString("/")
|
||||
val id = when (urlTail) {
|
||||
"9.56608/9.576080000000001/54.5066/54.516600000000004" -> 2105
|
||||
"9.539283999999999/9.549284/54.471699/54.481699000000006" -> 18284
|
||||
"9.56608/9.576080000000001/54.5066/54.516600000000004/22" -> 2105
|
||||
"9.539283999999999/9.549284/54.471699/54.481699000000006/22" -> 18284
|
||||
else -> -1
|
||||
}
|
||||
return okResponse("/newmotion/$id/markers.json")
|
||||
|
||||
@@ -35,16 +35,18 @@ class ChargepriceApiTest {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val segments = request.requestUrl!!.pathSegments
|
||||
val urlHead = segments.subList(0, 2).joinToString("/")
|
||||
when (urlHead) {
|
||||
return when (urlHead) {
|
||||
"ge/chargepoints" -> {
|
||||
val id = request.requestUrl!!.queryParameter("ge_id")
|
||||
return okResponse("/chargers/$id.json")
|
||||
okResponse("/chargers/$id.json")
|
||||
}
|
||||
|
||||
"cp/charge_prices" -> {
|
||||
val body = request.body.readUtf8()
|
||||
return okResponse("/chargeprice/2105.json")
|
||||
okResponse("/chargeprice/2105.json")
|
||||
}
|
||||
else -> return notFoundResponse
|
||||
|
||||
else -> notFoundResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class FronyxApiTest {
|
||||
val ids = request.requestUrl!!.queryParameter("evseIds")!!.split(",")
|
||||
return okResponse(
|
||||
"/fronyx/${
|
||||
ids.map { it.replace("*", "_") }.joinToString(",")
|
||||
ids.joinToString(",") { it.replace("*", "_") }
|
||||
}.json"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.car.app.testing.TestCarContext
|
||||
import androidx.car.app.testing.TestScreenManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import net.vonforst.evmap.FakeAndroidKeyStore
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
@@ -22,6 +24,11 @@ class CarAppTest {
|
||||
updateHandshakeInfo(HandshakeInfo("auto.testing", 1))
|
||||
}
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
FakeAndroidKeyStore.setup
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onCreateScreen_returnsExpectedScreen() {
|
||||
val service = Robolectric.setupService(CarAppService::class.java)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.21'
|
||||
ext.kotlin_version = '1.8.21'
|
||||
ext.about_libs_version = '8.9.4'
|
||||
ext.nav_version = '2.5.3'
|
||||
repositories {
|
||||
@@ -10,7 +10,7 @@ buildscript {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.1'
|
||||
classpath 'com.android.tools.build:gradle:8.0.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
|
||||
7
fastlane/metadata/android/de-DE/changelogs/168.txt
Normal file
7
fastlane/metadata/android/de-DE/changelogs/168.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Verbesserungen:
|
||||
- Neue Übersetzungen: Portugiesisch, Rumänisch
|
||||
- Open Charge Map: Links zu Betreiber hinzugefügt
|
||||
|
||||
Fehler behoben:
|
||||
- Open Charge Map: Gelöschte Ladestationen nicht anzeigen
|
||||
- Open Charge Map: Filter "Störung ausschließen" korrigiert
|
||||
10
fastlane/metadata/android/de-DE/changelogs/170.txt
Normal file
10
fastlane/metadata/android/de-DE/changelogs/170.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Neue Funktionen:
|
||||
- Echtzeitdaten zu Verfügbarkeit und Preisen für Tesla Supercharger (Login mit Tesla-Account erforderlich)
|
||||
|
||||
Verbesserungen:
|
||||
- Übersetzungen aktualisiert
|
||||
|
||||
Fehler behoben:
|
||||
- Datenquelle NewMotion für Echtzeitdaten funktionierte nicht mehr
|
||||
- Verschieben der Karte zur eigenen Position funktionierte auf manchen Geräten nicht
|
||||
- Abstürze auf älteren Geräten behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/172.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/172.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user