mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-27 00:57:45 -05:00
Compare commits
64 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 | ||
|
|
8405f4f4fa | ||
|
|
f435180c03 | ||
|
|
c2c3e96e97 | ||
|
|
9100a6f442 | ||
|
|
5403549e0a | ||
|
|
c95f1e7c24 | ||
|
|
f8d5b78112 | ||
|
|
246d456851 | ||
|
|
3d303b6535 | ||
|
|
135fce43c3 | ||
|
|
ee354d2cd1 |
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 164
|
||||
versionName "1.4.8"
|
||||
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>
|
||||
@@ -8,6 +8,9 @@ package net.vonforst.evmap.auto
|
||||
private val models = mapOf(
|
||||
"Audi" to mapOf(
|
||||
"516 (G4x)" to "e-tron"
|
||||
),
|
||||
"Renault" to mapOf(
|
||||
"BCB" to "Megane E-Tech"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
@@ -352,7 +352,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
} else if (vehicles.size > 1) {
|
||||
if (modelName != null) {
|
||||
vehicles = vehicles.filter {
|
||||
it.name.startsWith(modelName)
|
||||
it.name.lowercase().startsWith(modelName.lowercase())
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
throw VehicleUnknownException()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(getLabel(item))
|
||||
.apply { getDetails(item)?.let { addText(it) } }
|
||||
.setImage(if (isSelected(item)) checkedIcon else uncheckedIcon)
|
||||
.setOnClickListener {
|
||||
toggleSelected(item)
|
||||
@@ -130,5 +131,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
|
||||
|
||||
abstract fun getLabel(it: T): String
|
||||
|
||||
open fun getDetails(it: T): String? = null
|
||||
|
||||
abstract suspend fun loadData(): List<T>
|
||||
}
|
||||
@@ -390,6 +390,8 @@ class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<Chargepric
|
||||
|
||||
override fun getLabel(it: ChargepriceCar) = "${it.brand} ${it.name}"
|
||||
|
||||
override fun getDetails(it: ChargepriceCar) = it.formatSpecs()
|
||||
|
||||
override suspend fun loadData(): List<ChargepriceCar> {
|
||||
return api.getVehicles()
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -35,4 +35,6 @@
|
||||
<string name="settings_android_auto_chargeprice_range">Plage de charge pour la comparaison des prix</string>
|
||||
<string name="welcome_android_auto_detail">Vous pouvez également utiliser EVMap à partir d\'Android Auto sur les voitures prises en charge. Il suffit de sélectionner l\'application EVMap dans le menu Android Auto.</string>
|
||||
<string name="loading">Chargement…</string>
|
||||
<string name="auto_multipage_goto">Page %d</string>
|
||||
<string name="auto_multipage">(%d/%d)</string>
|
||||
</resources>
|
||||
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>?,
|
||||
|
||||
@@ -5,14 +5,14 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
|
||||
@@ -37,27 +37,43 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val id = getItem(position).id
|
||||
val url = getItem(position).getUrl(height = holder.view.height)
|
||||
val item = getItem(position)
|
||||
|
||||
holder.view.load(
|
||||
url
|
||||
) {
|
||||
size(SizeResolver(OriginalSize))
|
||||
allowHardware(false)
|
||||
listener(
|
||||
onSuccess = { _, metadata ->
|
||||
memoryKeys[id] = metadata.memoryCacheKey
|
||||
if (holder.view.height == 0) {
|
||||
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
loadImage(item, holder)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
loadImage(item, holder)
|
||||
}
|
||||
|
||||
if (itemClickListener != null) {
|
||||
holder.view.setOnClickListener {
|
||||
itemClickListener.onItemClick(holder.view, position, memoryKeys[id])
|
||||
itemClickListener.onItemClick(holder.view, position, memoryKeys[item.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImage(
|
||||
item: ChargerPhoto,
|
||||
holder: ViewHolder
|
||||
) {
|
||||
val url = item.getUrl(height = holder.view.height)
|
||||
|
||||
holder.view.load(
|
||||
url
|
||||
) {
|
||||
listener(
|
||||
onSuccess = { _, metadata ->
|
||||
memoryKeys[item.id] = metadata.memoryCacheKey
|
||||
}
|
||||
)
|
||||
allowHardware(!BuildConfig.DEBUG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChargerPhotoDiffCallback : DiffUtil.ItemCallback<ChargerPhoto>() {
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -114,8 +114,26 @@ data class ChargepriceCar(
|
||||
val brand: String,
|
||||
|
||||
@Json(name = "dc_charge_ports")
|
||||
val dcChargePorts: List<String>
|
||||
val dcChargePorts: List<String>,
|
||||
|
||||
@Json(name = "usable_battery_size")
|
||||
val usableBatterySize: Float,
|
||||
|
||||
@Json(name = "ac_max_power")
|
||||
val acMaxPower: Float,
|
||||
|
||||
@Json(name = "dc_max_power")
|
||||
val dcMaxPower: Float?
|
||||
) : Equatable, Parcelable {
|
||||
fun formatSpecs(): String = buildString {
|
||||
append("%.0f kWh".format(usableBatterySize))
|
||||
append(" | ")
|
||||
append("AC %.0f kW".format(acMaxPower))
|
||||
dcMaxPower?.let {
|
||||
append(" | ")
|
||||
append("DC %.0f kW".format(it))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val acConnectors = listOf(
|
||||
@@ -139,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)
|
||||
@@ -210,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()
|
||||
@@ -229,7 +247,7 @@ internal object RelationshipsParceler : Parceler<Relationships?> {
|
||||
Relationship.ToMany(ris)
|
||||
}
|
||||
key to value
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
return Relationships(members)
|
||||
}
|
||||
@@ -281,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()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.squareup.moshi.*
|
||||
import java.lang.reflect.Type
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
|
||||
internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
|
||||
@@ -13,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +139,12 @@ internal class HoursAdapter {
|
||||
val end = if (match.groupValues[2] == "24:00") {
|
||||
LocalTime.MAX
|
||||
} else {
|
||||
LocalTime.parse(match.groupValues[2])
|
||||
try {
|
||||
LocalTime.parse(match.groupValues[2])
|
||||
} catch (e: DateTimeParseException) {
|
||||
// got a rare bug report where the value is 24:0000
|
||||
LocalTime.MIN
|
||||
}
|
||||
}
|
||||
return GEHours(start, end)
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -147,7 +149,7 @@ data class GEChargerPhoto(val id: String) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
class GEChargerPhotoAdapter(override val id: String, val apikey: String) :
|
||||
ChargerPhoto(id) {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?): String {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
|
||||
return "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}&id=$id" +
|
||||
when {
|
||||
size != null -> "&size=$size"
|
||||
@@ -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) {
|
||||
@@ -251,14 +260,13 @@ class OCMChargerPhotoAdapter(
|
||||
val largeUrl: String,
|
||||
val thumbUrl: String
|
||||
) : ChargerPhoto(id) {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?): String {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
|
||||
val maxSize = size ?: max(height, width)
|
||||
val mediumUrl = thumbUrl.replace(".thmb.", ".medi.")
|
||||
return when (maxSize) {
|
||||
0 -> mediumUrl
|
||||
in 1..100 -> thumbUrl
|
||||
in 0..100 -> thumbUrl
|
||||
in 101..400 -> mediumUrl
|
||||
else -> largeUrl
|
||||
else -> if (allowOriginal) largeUrl else mediumUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -753,12 +761,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val photos = vm.charger.value?.data?.photos ?: return
|
||||
|
||||
viewer = StfalconImageViewer.Builder(context, photos) { imageView, photo ->
|
||||
imageView.load(photo.getUrl(size = 1000)) {
|
||||
imageView.load(photo.getUrl(size = 1000, allowOriginal = true)) {
|
||||
if (photo == photos[position] && imageCacheKey != null) {
|
||||
placeholderMemoryCacheKey(imageCacheKey)
|
||||
}
|
||||
size(SizeResolver(OriginalSize))
|
||||
allowHardware(false)
|
||||
}
|
||||
}
|
||||
.withTransitionFrom(view as ImageView)
|
||||
@@ -810,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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -959,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)
|
||||
@@ -981,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)
|
||||
@@ -1091,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
|
||||
)
|
||||
)
|
||||
@@ -1112,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
|
||||
@@ -1131,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))
|
||||
@@ -1185,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),
|
||||
@@ -1232,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(
|
||||
@@ -1262,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]
|
||||
@@ -1290,8 +1303,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
// else unknown ID -> wait for filterProfiles to update
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
popup.setTouchModal(false)
|
||||
popup.show()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
|
||||
companion object {
|
||||
fun getInstance(
|
||||
title: String,
|
||||
data: Map<String, String>,
|
||||
data: Map<String, CharSequence>,
|
||||
selected: Set<String>,
|
||||
commonChoices: Set<String>?,
|
||||
showAllButton: Boolean = true
|
||||
@@ -55,7 +55,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
|
||||
|
||||
override fun initView(view: View, savedInstanceState: Bundle?) {
|
||||
val args = requireArguments()
|
||||
val data = args.getSerializable("data") as HashMap<String, String>
|
||||
val data = args.getSerializable("data") as HashMap<String, CharSequence>
|
||||
val selected = args.getSerializable("selected") as HashSet<String>
|
||||
val title = args.getString("title")
|
||||
val commonChoices = if (args.containsKey("commonChoices")) {
|
||||
@@ -71,7 +71,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
|
||||
binding.btnAll.visibility = if (showAllButton) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value.lowercase(Locale.getDefault()) }
|
||||
.sortedBy { it.value.toString().lowercase(Locale.getDefault()) }
|
||||
.sortedBy {
|
||||
when {
|
||||
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
|
||||
@@ -117,7 +117,7 @@ private fun search(
|
||||
): List<MultiSelectItem> {
|
||||
return items.filter { item ->
|
||||
// search for string within name
|
||||
text.lowercase(Locale.getDefault()) in item.name.lowercase(Locale.getDefault())
|
||||
text.lowercase(Locale.getDefault()) in item.name.toString().lowercase(Locale.getDefault())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,4 +125,5 @@ class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
|
||||
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
|
||||
}
|
||||
|
||||
data class MultiSelectItem(val key: String, val name: String, var selected: Boolean) : Equatable
|
||||
data class MultiSelectItem(val key: String, val name: CharSequence, var selected: Boolean) :
|
||||
Equatable
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@ package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
|
||||
class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
|
||||
@@ -22,8 +26,8 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
}
|
||||
})
|
||||
|
||||
private lateinit var myVehiclePreference: MultiSelectListPreference
|
||||
private lateinit var myTariffsPreference: MultiSelectListPreference
|
||||
private lateinit var myVehiclePreference: MultiSelectDialogPreference
|
||||
private lateinit var myTariffsPreference: MultiSelectDialogPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -34,8 +38,16 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
res.data?.let { cars ->
|
||||
val sortedCars = cars.sortedBy { it.brand }
|
||||
myVehiclePreference.entryValues = sortedCars.map { it.id }.toTypedArray()
|
||||
myVehiclePreference.entries =
|
||||
sortedCars.map { "${it.brand} ${it.name}" }.toTypedArray()
|
||||
myVehiclePreference.entries = sortedCars.map {
|
||||
SpannableStringBuilder().apply {
|
||||
appendLine("${it.brand} ${it.name}")
|
||||
append(
|
||||
it.formatSpecs(),
|
||||
RelativeSizeSpan(0.86f),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}.toTypedArray()
|
||||
myVehiclePreference.isEnabled = true
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
@@ -78,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(" · ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +334,19 @@ data class Hours(
|
||||
}
|
||||
|
||||
abstract class ChargerPhoto(open val id: String) : Parcelable {
|
||||
abstract fun getUrl(height: Int? = null, width: Int? = null, size: Int? = null): String
|
||||
/**
|
||||
* Gets a URL of the image corresponding to a given size.
|
||||
*
|
||||
* If the data source supports accessing the image in its original (potentially unlimited) size,
|
||||
* this size will only be returned if allowOriginal is set to true. Otherwise, only scaled
|
||||
* versions of the images will be returned.
|
||||
*/
|
||||
abstract fun getUrl(
|
||||
height: Int? = null,
|
||||
width: Int? = null,
|
||||
size: Int? = null,
|
||||
allowOriginal: Boolean = false
|
||||
): String
|
||||
}
|
||||
|
||||
data class ChargeLocationCluster(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,7 +38,7 @@ class MultiSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
|
||||
val dialog =
|
||||
MultiSelectDialog.getInstance(
|
||||
title.toString(),
|
||||
entryValues.map { it.toString() }.zip(entries.map { it.toString() }).toMap(),
|
||||
entryValues.map { it.toString() }.zip(entries).toMap(),
|
||||
if (all) entryValues.map { it.toString() }.toSet() else values,
|
||||
emptySet(),
|
||||
showAllButton
|
||||
|
||||
@@ -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>
|
||||
@@ -158,7 +158,7 @@
|
||||
<string name="welcome_2">Die Farbe einer Ladestation zeigt dir die maximale Ladeleistung</string>
|
||||
<string name="welcome_2_detail">Die Farben kannst du unter “Über EVMap → Häufig gestellte Fragen” erneut ansehen</string>
|
||||
<string name="donation_dialog_title">Danke, dass du EVMap nutzt!</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Um die laufenden Kosten für den für die Datenquellen zu decken, freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Um die laufenden Kosten für die Datenquellen zu decken, freut sich der Entwickler über Spenden mit einem Betrag deiner Wahl.</string>
|
||||
<string name="chargeprice_donation_dialog_title">Du bist ein richtiger Sparfuchs!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Anscheinend nutzt du den Preisvergleich sehr gern. Mit einer Spende für EVMap kannst du helfen, die Kosten für den Datenzugriff zu decken.</string>
|
||||
<string name="deleted_filterprofile">„%s” gelöscht</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>
|
||||
@@ -273,4 +272,30 @@
|
||||
<string name="charging_free">gratuite</string>
|
||||
<string name="about_contributors">Contributeurs</string>
|
||||
<string name="about_contributors_text">Merci à tous les contributeurs pour leur contribution au codage et à la traduction d\'EVMap :</string>
|
||||
<string name="location_error">Localisation non détectée. Veuillez vérifier les paramètres du système</string>
|
||||
<string name="developer_mode_enabled">Mode développeur activé</string>
|
||||
<string name="pref_prediction_enabled">Afficher les prévisions d\'utilisation</string>
|
||||
<string name="pref_prediction_enabled_summary">pour les chargeurs pris en charge
|
||||
\n(actuellement seulement chargeurs rapides en Allemagne)</string>
|
||||
<string name="pref_applink_associate">Ouvrir les liens pris en charge</string>
|
||||
<string name="pref_applink_associate_summary">de goingelectric.de et openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">Mes tarifs</string>
|
||||
<string name="chargeprice_header_other_tariffs">Autres tarifs</string>
|
||||
<string name="disable_developer_mode">Désactiver le mode développeur</string>
|
||||
<string name="developer_mode_disabled">Mode développeur désactivé</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Boussole</string>
|
||||
<string name="prediction_dc_plugs_only">Prises DC</string>
|
||||
<string name="data_source_switched_to">Source de données basculée vers %s</string>
|
||||
<string name="menu_reset">Réinitialiser le filtre</string>
|
||||
<string name="chargeprice_price_not_available">Prix non disponible</string>
|
||||
<string name="utilization_prediction">Prévision d\'utilisation</string>
|
||||
<string name="prediction_help">La prédiction est basée sur des facteurs tels que le jour de la semaine, l\'heure de la journée et l\'utilisation passée, ce qui vous permet d\'éviter les chargeurs surchargés. Pas de garantie.</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">%1$d/%2$d disponible</item>
|
||||
<item quantity="many">%1$d/%2$d disponibles</item>
|
||||
<item quantity="other">%1$d/%2$d disponibles</item>
|
||||
</plurals>
|
||||
<string name="developer_options">Paramètres développeur</string>
|
||||
<string name="prediction_time_colon">%s :</string>
|
||||
</resources>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user